
    epjK                    2   d Z ddlmZ ddlZddlZddlZddlZddlZddl	Z	ddl
mZ ddlmZmZ ddlmZmZ ddlmZ ddlmZ g d	Zd
ZdZ ej        d          Zd-dZd.dZd/dZd0dZd1dZeed d2d"Zeed d3d#Z d$eedd%d4d*Z! G d+ d,e          Z"dS )5u  ``command`` secret source — resolve secrets via a user-configured helper.

Ports the security semantics of the desktop app's TypeScript
``CommandSecretsProvider`` (hermes-desktop ``src/main/secrets/commandProvider.ts``)
to the Python agent.  The helper command (e.g. ``keepassxc-cli``,
``secret-tool``, or a script that cats a tmpfs env file) comes from
``secrets.command`` in ``config.yaml`` — NEVER from ``.env``, which holds
only secret values.

Security model (mirrors the TS provider line-for-line where it matters):

* The command string is the USER'S OWN configuration (same trust level as
  the ``.env`` file they control), so it is run via ``/bin/sh -c <command>``.
* The requested key is passed to the child ONLY via the ``HERMES_SECRET_KEY``
  environment variable — it is NEVER interpolated into the shell string, so
  a hostile key name (e.g. ``"; rm -rf ~``) is inert data, not code.
* Hard timeout (default 3s) + output cap (default 1 MiB); any failure
  (non-zero exit, timeout, spawn failure, oversized output) degrades to
  "no value" rather than raising.
* Failures log ONLY structured fields (exit code / signal / errno) to
  stderr — never the command string, the helper's stderr, or any secret
  value.  The helper's stderr is captured via a pipe and DISCARDED so its
  diagnostics (which can carry secret material) never reach our stderr.
* The startup/apply path runs the helper exactly ONCE (with an empty
  ``HERMES_SECRET_KEY``) — it is never called per-key in a loop, so a
  helper that blocks (e.g. on a vault unlock prompt) can't be spawned
  dozens of times.
* PLATFORM: the provider is POSIX-only (needs ``/bin/sh``).  On Windows it
  degrades to an empty result with a warning; Windows users stay on the
  default ``env`` provider.
    )annotationsN)Path)DictOptional)	ErrorKindSecretSource)get_source_environment)FetchResult)r
   apply_command_secretsget_command_secretlist_command_secretsparse_secret_outputunquote_dotenv_valueg      @i   z^([A-Za-z_][A-Za-z0-9_]*)=(.*)$returnboolc                 P    t           j        dk    pt          j                    dk    S )NntWindows)osnameplatformsystem     B/home/thesage/.hermes/hermes-agent/agent/secret_sources/command.py_is_windowsr   J   s     7d?<ho//9<<r   rawstrc                   |                                  }t          |          dk    r^|                    d          r|                    d          s*|                    d          r|                    d          r
|dd         S |S )a=  Strip a single layer of matching surrounding quotes from a dotenv value.

    Requires length >= 2 so a lone quote (``"``) is left intact rather than
    collapsing to empty, and ``""``/``''`` correctly yield an empty string.
    Shared by the single-key parser and the list path so both unquote
    identically.
       "'   )striplen
startswithendswith)r   ts     r   r   r   N   s     			A
1vv{{	
c		  zz# LL "#**S//  2wHr   stdout
wanted_keyOptional[str]c                   |                      dd          }|                    d          }d d |D             D             }|D ]y}t                              |          }|J |                    d          |k    r@t          |                    d                    }|                                dk    r|ndc S zt          |          dk    rdS |                                }|dk    rdS t                              |          }|rU|                    d          |k    r<t          j	        d	|                    d                                                    dS |S )
as  Parse a secret-fetch helper's stdout.  Supports BOTH shapes:

    * a bare value (single secret): the whole trimmed stdout is the value.
    * a dotenv blob (KEY=VALUE lines): parse them and return the entry for
      ``wanted_key``.

    Mirrors the TS ``parseSecretOutput`` exactly, including the cross-key
    misroute guard and the base64-padding disambiguation.
    

c                t    g | ]5}||                     d           t                              |          3|6S )#)r'   	_ENV_LINEmatch).0lines     r   
<listcomp>z'parse_secret_output.<locals>.<listcomp>n   sZ        ,, 2;1F1F  r   c              3  >   K   | ]}|                                 V  d S )N)r%   )r4   r   s     r   	<genexpr>z&parse_secret_output.<locals>.<genexpr>p   s*      22SSYY[[222222r   Nr#   r     z=*)
replacesplitr2   r3   groupr   r%   r&   re	fullmatch)	r*   r+   textlinesdotenv_linesr5   mvalue
env_shapeds	            r   r   r   _   su    >>&$''DJJtE 22E222  L
  : :OOD!!}}}771::##(44E "KKMMR//55T999 $ <1t
 JJLLE{{t ''JQ:--L
 0 0 3 3 9 9 ; ;<<DtLr   command
secret_keytimeout_secondsfloatmax_output_bytesintc                   t                      rt          dt          j                   dS t	                      }|t
          j        u rddlm}  |dd          }nt          |          }||d<   	 t          j        d	d
| g|t          j        t          j        t          j        d          }n;# t          $ r.}t          d|j         t          j                   Y d}~dS d}~ww xY w	 |                    |          \  }	}
n# t          j        $ r 	 t          j        t          j        |j                  t*          j                   n1# t.          t0          t          f$ r |                                 Y nw xY w	 |                    d           n"# t          j        t4          t          f$ r Y nw xY wt          d|ddt          j                   Y dS w xY w|j        dk    r|j        dk     rK	 t+          j        |j                   j        }n%# t4          $ r t=          |j                   }Y nw xY wd|}}nt=          |j                  d}}t          d| d| t          j                   dS t?          |	          |k    r!t          d| dt          j                   dS |	                     dd          S )u^  Run the helper via ``/bin/sh -c`` and return its stdout, or None.

    The key is passed as DATA via ``HERMES_SECRET_KEY`` — never interpolated
    into the command string.  Both stdout and stderr are captured via pipes
    (never inherited); stderr is discarded.  Any failure logs structured
    fields only and returns None — never raises.
    ze[secrets:command] the 'command' provider is POSIX-only (needs /bin/sh); resolving no value on Windows)fileNr   )build_subprocess_envF)scrub_secretsinherit_profile_homeHERMES_SECRET_KEYz/bin/shz-cT)envstdinr*   stderrstart_new_sessionzD[secrets:command] helper failed to spawn; resolving no value: errno=)timeoutg      ?z)[secrets:command] helper timed out after gzs; resolving no value?nonez:[secrets:command] helper failed; resolving no value: code=z signal=z-[secrets:command] helper output exceeded the z-byte cap; resolving no valuezutf-8r:   )errors)!r   printsysrS   r	   r   environtools.environments.localrM   dict
subprocessPopenDEVNULLPIPEOSErrorerrnocommunicateTimeoutExpiredkillpggetpgidpid_signalSIGKILLProcessLookupErrorPermissionErrorkill
ValueError
returncodeSignalsr   r   r&   decode)rE   rF   rG   rI   
source_envrM   rQ   procexcstdout_bytes_stderr_discardedsigcodesignames                 r   _run_helperr{      s    }} =	
 	
 	
 	

 t ())JRZ 	BAAAAA""USSS
 :)Cg&$??"
 
 
    !Y! !	
 	
 	
 	

 ttttt*.*:*:?*:*S*S'''$   
	Ibj**GO<<<<"OW= 	 	 	IIKKKKK		S))))):w? 	 	 	D	"N " " "	
 	
 	
 	

 tt%( ! ?Q,ot&677< , , ,4?*++,'DD00&'D,, ,"), ,	
 	
 	
 	

 t
<+++?? ? ?	
 	
 	
 	

 twy999s   1;B- -
C%7#C  C%)D G6E
	G
+E85G7E88G<FGF2/G1F22#GG3H H54H5Dict[str, str]c                d   i }|                      dd                              d          D ]}|                                }|r|                    d          r.t                              |          }|sKt          |                    d                    ||                    d          <   |S )u  Parse a KEY=VALUE blob into a map (the list/enumerate path).

    Mirrors the TS ``list()``: only env-shaped lines contribute; comments
    and non-matching lines are skipped.  A bare-value helper yields ``{}``
    — per-key resolution via :func:`get_command_secret` still works.
    r.   r/   r1   r    r#   )r:   r;   r%   r'   r2   r3   r   r<   )r*   outr   r5   rB   s        r   _parse_dotenv_mapr   
  s     C~~fd++11$77 ; ;yy{{ 	ts++ 	OOD!! 	.qwwqzz::AGGAJJJr   )rG   rI   keyc                    | pd                                 } | sdS t          | |||          }|dS t          ||          S )u   Resolve a single secret by running the helper with the key in
    ``HERMES_SECRET_KEY``.  Returns None on any failure — never raises.r9   N)r%   r{   r   )rE   r   rG   rI   r*   s        r   r   r     sT     }"##%%G t#8HIIF~tvs+++r   c                    | pd                                 } | si S t          | d||          }|i S t          |          S )zEnumerate secrets by running the helper ONCE with an empty key.

    Returns the dotenv map ONLY when the helper emits a KEY=VALUE blob;
    a bare-value helper returns ``{}``.  Never raises.
    r9   )r%   r{   r   )rE   rG   rI   r*   s       r   r   r   /  sR     }"##%%G 	"o7GHHF~	V$$$r   F)override_existingrG   rI   	home_pathr   r   Optional[Path]r
   c                   t                      }| pd                                } | s	d|_        |S t                      r|j                            d           |S t          | d||          }||j                            d           |S t          |          }||_        |s|j                            d           |S |	                                D ]\  }}	|	                                dk    r|j
                            |           8|s:t          j                            |          r|j
                            |           t|	t          j        |<   |j                            |           |S )a5  Run the helper once at startup and set its KEY=VALUE output on
    ``os.environ``.

    LEGACY shim retained for API symmetry with ``apply_bitwarden_secrets``;
    the startup path goes through :class:`CommandSource` + the registry
    orchestrator instead (which owns precedence and the environ writes).
    r9   msecrets.command.enabled is true but secrets.command.command is empty.  Set the helper command in config.yaml.Nthe 'command' secret source is POSIX-only (needs /bin/sh); skipping on WindowsNzahelper command failed at startup; no secrets applied (process env / .env values remain in effect)z|helper output was not a KEY=VALUE map; nothing applied at startup (a bare-value helper still resolves single keys on demand))r
   r%   errorr   warningsappendr{   r   secretsitemsskippedr   r\   getapplied)
rE   r   rG   rI   r   resultr*   r   r   rC   s
             r   r   r   H  s    ]]F}"##%%G = 	 }} "	
 	
 	
  "o7GHHF~;	
 	
 	
 ''GFN Q	
 	
 	
 mmoo # #
U;;==B N!!#&&&  	RZ^^C%8%8 	N!!#&&&
3c""""Mr   c                  @     e Zd ZdZdZdZdZddZddZd fdZ	 xZ
S )CommandSourceu  User-configured helper command as a registered secret source.

    Composes with the other sources (Bitwarden, 1Password, plugins) through
    the ``apply_all()`` orchestrator — enable any combination simultaneously;
    there is deliberately NO single-provider selector.  ``fetch()`` only
    fetches: precedence, ``override_existing`` semantics, conflict warnings,
    and the ``os.environ`` writes are the orchestrator's job.

    Bulk shape: the helper enumerates a KEY=VALUE blob in one run.  Config::

        secrets:
          command:
            enabled: true
            command: "cat /run/user/1000/hermes-secrets.env"
            # or per-vault CLIs: keepassxc-cli / secret-tool / pass / gpg —
            # anything fast and NON-interactive.
    rE   zCommand helperbulkr   r^   c                2    dddddddt           dddddS )	NzMaster switchF)descriptiondefaultz@Helper run via /bin/sh -c; must print a KEY=VALUE blob on stdoutr9   zHard timeout for one helper runz)Helper values overwrite .env/shell values)enabledrE   helper_timeout_secondsr   )_COMMAND_TIMEOUT_SECONDS)selfs    r   config_schemazCommandSource.config_schema  sT    '65II :   A3' '
  K " "
 
 	
r   cfgr   r   r
   c                   t          |t                    r|ni }t                      }t          |                    d          pd                                          }|sd|_        t          j        |_	        |S t                      rd|_        t          j        |_	        |S 	 t          |                    dt                              }n# t          t          f$ r
 t          }Y nw xY wt          |d|t                     }|d|_        t          j        |_	        |S t%          |          }|s|j                            d           |S ||_        |S )NrE   r9   r   r   r   zGhelper command failed (see structured fields above); no secrets appliedz7helper output was not a KEY=VALUE map; nothing to apply)
isinstancer^   r
   r   r   r%   r   r   NOT_CONFIGURED
error_kindr   rH   r   	TypeErrorro   r{   _MAX_OUTPUT_BYTESINTERNALr   r   r   r   )r   r   r   r   rE   rU   r*   r   s           r   fetchzCommandSource.fetch  sq   T**2cccggi((.B//5577 	D L !* 8FM== 	& L !* 8FM	/CGG$<$<> > ? ?GG:& 	/ 	/ 	/.GGG	/ Wb'3DEE>% L !* 2FM#F++ 	O""I   M s   #(C C'&C'r   c                    |t           j        k    r	 dS |t           j        k    r	 dS t                                          ||          S )NziSet secrets.command.command in config.yaml to a fast, non-interactive helper that prints KEY=VALUE lines.u   Run the helper manually in a shell to see its real error — Hermes discards helper stderr so diagnostics can't leak secret material.)r   r   r   superremediation)r   kindr   	__class__s      r   r   zCommandSource.remediation  s\    9+++F  9%%%# 
 ww""4---r   )r   r^   )r   r^   r   r   r   r
   )r   r^   r   r   )__name__
__module____qualname____doc__r   labelshaper   r   r   __classcell__)r   s   @r   r   r     s         $ DEE
 
 
 
$- - - -^. . . . . . . . . .r   r   )r   r   )r   r   r   r   )r*   r   r+   r   r   r,   )
rE   r   rF   r   rG   rH   rI   rJ   r   r,   )r*   r   r   r|   )
rE   r   r   r   rG   rH   rI   rJ   r   r,   )rE   r   rG   rH   rI   rJ   r   r|   )rE   r   r   r   rG   rH   rI   rJ   r   r   r   r
   )#r   
__future__r   r   r   r=   signalrj   r_   r[   pathlibr   typingr   r   agent.secret_sources.baser   r   r	   agent.secret_sources.bitwardenr
   __all__r   r   compiler2   r   r   r   r{   r   r   r   r   r   r   r   r   <module>r      s&   @ # " " " " " 				  				         



       ! ! ! ! ! ! ! ! > = = = = = = = < < < < < < 6 6 6 6 6 6     
 BJ9::	= = = =   "@ @ @ @Fe: e: e: e:P   . 6-, , , , , ,* 6-	% % % % % %8 $5- $A A A A A ARd. d. d. d. d.L d. d. d. d. d.r   