
    .cjH                    &   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 g dZd	Zd
Z ej        d          Zd,dZd-dZd.dZd/dZd0dZeedd1d!Zeedd2d"Zd#eedd$d3d)Z  G d* d+e          Z!dS )4u  ``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)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     >/home/ice/.hermes/hermes-agent/agent/secret_sources/command.py_is_windowsr   I   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   M   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>m   sZ        ,, 2;1F1F  r   c              3  >   K   | ]}|                                 V  d S )N)r$   )r3   r   s     r   	<genexpr>z&parse_secret_output.<locals>.<genexpr>o   s*      22SSYY[[222222r   Nr"   r    z=*)
replacesplitr1   r2   groupr   r$   r%   re	fullmatch)	r)   r*   textlinesdotenv_linesr4   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          j                                        }||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(          t*          t          f$ r |                                 Y nw xY w	 |                    d
           n"# t          j        t.          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%# t.          $ r t7          |j                   }	Y nw xY wd|	}}
nt7          |j                  d}}
t          d|
 d| t          j                   dS t9          |          |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)fileN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 valuer   ?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-8r9   )errors)r   printsysrO   r   environcopy
subprocessPopenDEVNULLPIPEOSErrorerrnocommunicateTimeoutExpiredkillpggetpgidpid_signalSIGKILLProcessLookupErrorPermissionErrorkill
ValueError
returncodeSignalsr   r   r%   decode)rD   rE   rF   rH   rM   procexcstdout_bytes_stderr_discardedsigcodesignames               r   _run_helperru      sJ    }} =	
 	
 	
 	

 t
*//

C)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   ;B 
C#B??CC" "F826D)(F8)+EF8EF8E21F82FF8F#F87F8G2 2HH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.   r0   r   r"   )r9   r:   r$   r&   r1   r2   r   r;   )r)   outr   r4   rA   s        r   _parse_dotenv_mapry      s     C~~fd++11$77 ; ;yy{{ 	ts++ 	OOD!! 	.qwwqzz::AGGAJJJr   )rF   rH   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.r8   N)r$   ru   r   )rD   rz   rF   rH   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.
    r8   )r$   ru   ry   )rD   rF   rH   r)   s       r   r   r   "  sR     }"##%%G 	"o7GHHF~	V$$$r   F)override_existingrF   rH   	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).
    r8   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appendru   ry   secretsitemsskippedr   rX   getapplied)
rD   r}   rF   rH   r~   resultr)   r   rz   rB   s
             r   r
   r
   ;  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.
    rD   zCommand helperbulkr   dictc                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 stdoutr8   zHard timeout for one helper runz)Helper values overwrite .env/shell values)enabledrD   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 )NrD   r8   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   rG   r   	TypeErrorrj   ru   _MAX_OUTPUT_BYTESINTERNALry   r   r   r   )r   r   r~   r   rD   rQ   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+   )
rD   r   rE   r   rF   rG   rH   rI   r   r+   )r)   r   r   rv   )
rD   r   rz   r   rF   rG   rH   rI   r   r+   )rD   r   rF   rG   rH   rI   r   rv   )rD   r   r}   r   rF   rG   rH   rI   r~   r   r   r	   )"r   
__future__r   r   r   r<   signalre   rZ   rW   pathlibr   typingr   r   agent.secret_sources.baser   r   agent.secret_sources.bitwardenr	   __all__r   r   compiler1   r   r   r   ru   ry   r   r   r
   r   r   r   r   <module>r      s   @ # " " " " " 				  				         



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