
    .cj7c                       U 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	Z	ddl
mZ ddlmZmZmZmZ ddlmZmZmZmZ ddlmZmZ  ej        e          ZdZd	Z ej        d
          ZdZ ee!e!e!e!f         Z"i Z#de$d<   dZ%dGdZ& ee%e&          Z'de$d<   dHdIdZ(dJdZ)dKd Z*dLd"Z+dMdNd%Z,dOd'Z-dPd)Z.d#d#d*dQd.Z/d#edd#d/d0dd1dRd7Z0dd#ed#d/d0dd8dSd>Z1 G d? d@e          Z2dTdCZ3dHdUdEZ4dHdUdFZ5dS )Vu  1Password (`op` CLI) secret source.

Resolve provider credentials from 1Password ``op://vault/item/field``
references at process startup so they don't have to live in plaintext in
``~/.hermes/.env``.

Design summary
--------------

* Users map environment-variable names to official 1Password secret
  references in ``secrets.onepassword.env``::

      secrets:
        onepassword:
          enabled: true
          env:
            OPENAI_API_KEY: "op://Private/OpenAI/api key"
            ANTHROPIC_API_KEY: "op://Private/Anthropic/credential"

* After ``.env`` loads, each reference is resolved with a single
  ``op read -- <reference>`` call and injected into ``os.environ`` (the
  same point in startup as the Bitwarden source).
* Authentication is whatever the user's ``op`` CLI already uses — a
  service-account token (``OP_SERVICE_ACCOUNT_TOKEN``) for headless boxes,
  or a desktop/interactive session (``OP_SESSION_*``).  Hermes never
  authenticates on the user's behalf; it shells out to an already-trusted,
  already-authenticated CLI.
* Failures NEVER block startup.  A missing ``op`` binary, expired auth, a
  bad reference, or a permission error each surface a one-line warning and
  Hermes continues with whatever credentials ``.env`` already had.

The atomic-write / ``0600`` / TTL cache mechanics are shared with the other
backends via :mod:`agent.secret_sources._cache` — successful, complete pulls
are cached in-process and on disk under ``<hermes_home>/cache/op_cache.json``
so back-to-back short-lived ``hermes`` invocations don't re-shell ``op`` for
every reference.  The disk file holds only resolved secret *values*; auth
material is fingerprinted, never stored.
    )annotationsN)Path)DictListOptionalTuple)CachedFetch	DiskCacheFetchResultis_valid_env_name)	ErrorKindSecretSource   OP_SERVICE_ACCOUNT_TOKENz\x1b\[[0-?]*[ -/]*[@-~])PATHHOMEUSERPROFILEAPPDATALOCALAPPDATA
SystemRootTMPDIRTMPTEMPXDG_CONFIG_HOMEXDG_RUNTIME_DIR
OP_ACCOUNTOP_CONNECT_HOSTOP_CONNECT_TOKENOP_LOAD_DESKTOP_APP_SETTINGSzDict[_CacheKey, CachedFetch]_CACHEzop_cache.json	cache_key	_CacheKeyreturnstrc                $    | \  }}}}| d| d| S )a  Serialize a cache key for on-disk storage, omitting home_path.

    The disk file is already partitioned by home (it lives under
    ``<home>/cache/``), so the path provides the home dimension; folding it
    into the key string too would be redundant.
    | )r!   auth_fpaccount_homerefs_fps        B/home/ice/.hermes/hermes-agent/agent/secret_sources/onepassword.py_disk_key_strr-   y   s.     (1$GWeW++++'+++    )key_serializerr
   _DISK_CACHE	home_pathOptional[Path]r   c                6    t                               |           S )zAPath to the on-disk cache (exposed for tests and direct callers).)r0   pathr1   s    r,   _disk_cache_pathr6      s    I&&&r.   
referencesOptional[Dict[str, str]] Tuple[Dict[str, str], List[str]]c                   i }g }| pi                                  D ]\  }}t          |          s|                    d|d           .t          |t                    s|                    d|d           ]|                                }|                    d          s|                    d|d|d           |||<   ||fS )u  Return ``(valid_refs, warnings)`` from an ``env`` mapping.

    A reference is kept only if its target env-var name is a valid POSIX
    name and the value is a stripped ``op://…`` reference string.  Everything
    else produces a warning and is dropped (never fatal).
    z	Skipping z: not a valid env-var namez: reference is not a stringzop://: z! is not an op:// secret reference)itemsr   append
isinstancer$   strip
startswith)r7   validwarningsnamerefcleaneds         r,   _validate_referencesrF      s     EH &B--//  	c && 	OOJJJJKKK#s## 	OOKKKKLLL))++!!'** 	OONDNNcNNN   d(?r.   	token_envc                x   dt           j                            | d           dt           j                            dd           dt           j                            dd           dt           j                            dd           g}t          t           j                  D ]A}|                    d	          r*|                    | d
t           j        |                     Bd                    |          }t          j        |	                    d                    
                                dd         S )u)  SHA-256 prefix over the auth material `op` would use.

    Folds in the service-account token, ``OP_ACCOUNT``, the 1Password Connect
    ``OP_CONNECT_HOST``/``OP_CONNECT_TOKEN``, and *all* ``OP_SESSION_*`` vars
    (the names `op` actually exports for interactive sessions —
    ``OP_SESSION_<account_shorthand>``).  Signing out and into a different
    identity therefore changes the cache key, so a value cached under a
    previous identity is never served under a new one.  Never logged or
    displayed; the raw token never leaves this hash.
    ztoken= zaccount=r   zconnect_host=r   zconnect_token=r   OP_SESSION_=
utf-8N   )osenvirongetsortedr@   r=   joinhashlibsha256encode	hexdigest)rG   partskeymaterials       r,   _auth_fingerprintr[      s    	1	2..0052:>>,3355?
'8"==??A(:B??AA	E bj!! 5 5>>-(( 	5LLC33"*S/33444yyH>(//'2233==??DDr.   Dict[str, str]c                     d                      fdt                     D                       }t          j        |                    d                                                    dd         S )u<   SHA-256 prefix over the configured name→reference mapping.rL   c              3  2   K   | ]}| d |          V  dS )rK   Nr'   ).0rC   r7   s     r,   	<genexpr>z$_refs_fingerprint.<locals>.<genexpr>   s6      UU$D55:d#355UUUUUUr.   rM   NrN   )rS   rR   rT   rU   rV   rW   )r7   rZ   s   ` r,   _refs_fingerprintra      sb    yyUUUU&BTBTUUUUUH>(//'2233==??DDr.   rI   binary_pathc                    | rFt          |           }|                                r!t          j        |t          j                  r|S dS t          j        d          }|rt          |          ndS )u[  Resolve a usable ``op`` binary, or None.

    When ``binary_path`` is set it is used verbatim and PATH is NOT consulted
    — pinning an absolute path is a way to avoid trusting whatever ``op`` shows
    up first on ``PATH``.  A pinned-but-missing path returns None (the caller
    surfaces a clear error) rather than silently falling back.
    Nop)r   existsrO   accessX_OKshutilwhich)rb   pinnedfounds      r,   find_oprl      sl      k""==?? 	ry99 	MtLE)4;;;T)r.   textc                    t                               d|                               dd                                          S )zCRemove ANSI control sequences and trim, for safe message surfacing.rI   )_ANSI_CSI_REsubreplacer?   )rm   s    r,   _scrubrs      s4    B%%--fb99??AAAr.   token_valuec                    i }t           D ](}t          j                            |          }||||<   )t          j                                        D ]\  }}|                    d          r|||<    | r| |d<   d|d<   |S )zEBuild a minimal allowlisted environment for the ``op`` child process.NrJ   r   1NO_COLOR)_OP_ENV_ALLOWLISTrO   rP   rQ   r<   r@   )rt   envrY   vals       r,   _op_child_envr{      s    C   jnnS!!?CHJ$$&&  S>>-(( 	CH  6*5&'C
OJr.   r)   rt   rd   	referencer)   c          	        t          |           dg}|r|d|gz  }|d|gz  }	 t          j        |t          |          ddddt                    }nR# t          j        $ r }t          dt           d	|          |d
}~wt          $ r}t          d|           |d
}~ww xY w|j        dk    rOt          |j
        pd          d
d         }|rt          d|d|           t          d|j         d|          |j        pd                    d          }|                                st          d|          |S )u   Resolve a single ``op://`` reference to its value.

    Raises :class:`RuntimeError` on any failure — including a ``returncode 0``
    with empty output, which would otherwise silently clobber a good
    ``.env``/shell credential with ``""``.
    readz	--accountz--TrM   rr   )ry   capture_outputrm   encodingerrorstimeoutzop read timed out after zs for Nzfailed to invoke op: r   rI      zop read failed for r;   zop read exited z for z
z$op read returned an empty value for )r$   
subprocessrunr{   _OP_RUN_TIMEOUTTimeoutExpiredRuntimeErrorOSError
returncoders   stderrstdoutrstripr?   )	rd   r}   r)   rt   cmdprocexcerrvalues	            r,   _run_op_readr      s    "ggv&C &W%% D)CC~k**#
 
 
 $   KKKiKK
 
	  C C C838899sBC !T[&B''- 	KIYIICIIJJJAdoAAIAA
 
 	
 [B&&v..E;;== QO)OOPPPLs#   -A B  A;;B BB T,  )r)   rG   binaryrb   	use_cachecache_ttl_secondsr1   r   r   boolr   floatc                   t          |           \  }}	|si |	fS t          j                            |d                                          }
t          |          |pd|t          |          ndt          |          f}|rt                              |          }|r+|	                    |          rt          |j                  |	fS t                              |||          }| |t          |<   t          |j                  |	fS |pt          |          }|t          d          i }d}t!          |          D ]]}	 t#          |||         ||
          ||<    # t          $ r1}|	                    t          |                     |dz  }Y d}~Vd}~ww xY w|rZ|sX|rVt'          t          |          t)          j                              }|t          |<   t                              ||||           ||	fS )u  Resolve ``references`` (name → ``op://…``) to ``(secrets, warnings)``.

    Raises :class:`RuntimeError` only when no ``op`` binary is available — a
    fatal "can't fetch anything" condition.  Per-reference failures (expired
    auth, bad reference, empty value) are collected as warnings and the
    reference is dropped, so one bad entry never sinks the rest.

    Only a complete, error-free pull is cached, so a transient auth failure
    isn't frozen in for the whole TTL window.
    rI   Nzop CLI not found.  Install the 1Password CLI (https://developer.1password.com/docs/cli/get-started/) or set secrets.onepassword.binary_path to its absolute location.r   r|      )secrets
fetched_at)rF   rO   rP   rQ   r?   r[   r$   ra   r    is_freshdictr   r0   r   rl   r   rR   r   r=   r	   timewrite)r7   r)   rG   r   rb   r   r   r1   rA   rB   rt   r!   cacheddisk_cachedrd   r   read_errorsrC   r   entrys                       r,   fetch_onepassword_secretsr   <  s:   * +:66OE8 8|*..B//5577K)$$2#/IR%  	I  7I&& 	2foo&788 	2''11!&&y2CYOO" +F9+,,h66		'7;''B	zH
 
 	
 !GKu  	(E$Kk  GDMM  	 	 	OOCHH%%%1KKKKKK	  J J JDMMdikkJJJ!y)U,=yIIIHs   =E
F$'FF)ry   r)   service_account_token_envrb   override_existingr   r1   enabledry   r   r   r   c                   t                      }| s|S t          |          \  }	}
|j                            |
           i }|	                                D ]g\  }}||k    r|j                            |           &|s:t          j        	                    |          r|j                            |           b|||<   h|s|S t          |          }||_        ||rd|d|_        nd|_        |S 	 t          ||||||          \  }}n-# t          $ r }t          |          |_        |cY d}~S d}~ww xY w||_        |j                            |           |                                D ]\  }}||k    r$||j        vr|j                            |           /|sCt          j        	                    |          r$||j        vr|j                            |           t|t          j        |<   |j                            |           |S )u^  Resolve configured ``op://`` references and set them on ``os.environ``.

    Called by ``load_hermes_dotenv()`` after the .env files have loaded.
    Intentionally defensive — any failure returns a :class:`FetchResult` with
    ``error`` set (or surfaces warnings); it never raises.

    Parameters mirror the ``secrets.onepassword.*`` config keys so the caller
    can splat the dict in.  References that are already satisfied by the
    current environment (when ``override_existing`` is false) are skipped
    *before* fetching, so ``op`` is never invoked for a value that would be
    discarded.
    N!secrets.onepassword.binary_path (!) is not an executable op binary.secrets.onepassword.enabled is true but the op CLI was not found on PATH.  Install it (https://developer.1password.com/docs/cli/get-started/) or set secrets.onepassword.binary_path.r7   r)   rG   r   r   r1   )r   rF   rB   extendr<   skippedr=   rO   rP   rQ   rl   rb   errorr   r   r$   r   applied)r   ry   r)   r   rb   r   r   r1   resultrA   rB   refs_to_fetchrC   rD   r   r   fetch_warningsr   r   s                      r,   apply_onepassword_secretsr     sv   . ]]F *3//OE8
O8$$$ %'M[[]] " "	c,,,N!!$'''  	RZ^^D%9%9 	N!!$'''!d [!!FF~ 	(K ( ( ( LL3 L ";$//#
 #
 #
    3xx FN
O>***}} $ $e ,,,6>))%%d+++  	RZ^^D%9%9 	6>))%%d+++ 
4d####Ms   1D
 

D4D/)D4/D4c                  T     e Zd ZdZdZdZdZdZdd
ZddZ	ddZ
ddZd fdZ xZS )OnePasswordSourceu  1Password as a registered secret source.

    Thin adapter over the module's fetch machinery.  ``fetch()`` only
    *fetches* — precedence, override semantics, conflict warnings, and
    the ``os.environ`` writes are the orchestrator's job
    (see ``agent.secret_sources.registry.apply_all``).

    1Password is a **mapped** source: the user explicitly binds each env
    var to an ``op://`` reference under ``secrets.onepassword.env``, so
    its claims outrank bulk sources (e.g. a Bitwarden project dump) on
    contested vars.
    onepassword	1Passwordmappedrd   cfgr   r#   r   c                r    t          t          |t                    o|                    dd                    S )Nr   T)r   r>   r   rQ   )selfr   s     r,   r   z#OnePasswordSource.override_existing  s0    
 JsD))Pcgg6I4.P.PQQQr.   c                    t           }t          |t                    r$t          |                    d          p|          }t          |h          S )Nr   )_DEFAULT_TOKEN_ENVr>   r   r$   rQ   	frozenset)r   r   rG   s      r,   protected_env_varsz$OnePasswordSource.protected_env_vars  sI    &	c4   	OCGG$?@@MINNI)%%%r.   c           	     J    ddddi dddddt           ddddd	d
dddddS )NzMaster switchF)descriptiondefaultz1Map of ENV_VAR -> op://vault/item/field referencez0op --account shorthand (empty = default account)rI   zOEnv var holding the service-account token (unset = desktop/interactive session)z,Pin the op binary (empty = resolve via PATH)z!Disk+memory cache TTL; 0 disablesr   z+Resolved values overwrite .env/shell valuesT)r   ry   r)   r   rb   r   r   )r   )r   s    r,   config_schemazOnePasswordSource.config_schema  s    '65IIR 
  R 
 G-* *  N 
  C" "
  M" "/
 
 	
r.   r1   r   r   c           	     (   t          |t                    r|ni }t                      }|                    d          }t	          t          |t                    r|nd           \  }}|j                            |           |s|sd|_        t          j	        |_
        |S t          |                    d          pd          }t          |          }||_        |(|rd|d|_        nd|_        t          j        |_
        |S 	 t          |                    dd	                    }	n# t           t"          f$ r d
}	Y nw xY w	 t%          |t          |                    d          pd          t          |                    d          pt&                    ||	|          \  }
}nN# t(          $ rA}t          |          |_        t+          t          |                    |_
        |cY d }~S d }~ww xY w|
|_        |j                            |           |S )Nry   zksecrets.onepassword.enabled is true but the env: map is empty.  Add ENV_VAR: op://vault/item/field entries.rb   rI   r   r   r   r   r   g     r@r)   r   r   )r>   r   r   rQ   rF   rB   r   r   r   NOT_CONFIGURED
error_kindr$   rl   rb   BINARY_MISSINGr   	TypeError
ValueErrorr   r   r   _classify_op_errorr   )r   r   r1   r   env_maprA   rB   rb   r   ttlr   r   r   s                r,   fetchzOnePasswordSource.fetch*  sO   T**2cc''%...!'400:GGd
 
x 	x((( 	 =J  %.$<!M#''-006B77%%#> 3 3 3 3 >  !* 8FM	 3S99::CC:& 	 	 	CCC		&? CGGI..4"55GG788N<N  "%#	' 	' 	'#G^^  	 	 	s88FL 23s88 < <FMMMMMM	
 !~...s1   #D' 'D=<D=A!F# #
G.-6G)#G.)G.r$   c                .   |t           j        t           j        fv rFt          }t	          |t
                    r$t          |                    d          p|          }d| dS |t           j        k    r	 dS t                      
                    ||          S )Nr   zORun `hermes secrets onepassword token` to paste a fresh service-account token (z-), or `op signin` for an interactive session.zyInstall the 1Password CLI (https://developer.1password.com/docs/cli/get-started/) or set secrets.onepassword.binary_path.)r   AUTH_FAILEDAUTH_EXPIREDr   r>   r   r$   rQ   r   superremediation)r   kindr   rG   	__class__s       r,   r   zOnePasswordSource.remediationh  s    I)9+ABBB*I#t$$ S(C D D Q	RR	'*3' ' '
 9+++7 
 ww""4---r.   )r   r   r#   r   )r   r   )r#   r   )r   r   r1   r   r#   r   )r   r   r#   r$   )__name__
__module____qualname____doc__rC   labelshapeschemer   r   r   r   r   __classcell__)r   s   @r,   r   r     s          DEEFR R R R& & & &
 
 
 
<< < < <|. . . . . . . . . .r.   r   messager   c                P   |                                  dv rt          j        S dv sdv sdv rt          j        S t	          fddD                       rt          j        S dv rt          j        S t	          fdd	D                       rt          j        S t          j        S )
z@Best-effort mapping of op failure text onto the shared taxonomy.z	timed outznot found on pathznot an executablezfailed to invokec              3      K   | ]}|v V  	d S Nr'   r_   toklowereds     r,   r`   z%_classify_op_error.<locals>.<genexpr>  s7       5 5c3'> 5 5 5 5 5 5r.   )unauthorizedznot signed inzsession expiredauthentication401403zempty valuec              3      K   | ]}|v V  	d S r   r'   r   s     r,   r`   z%_classify_op_error.<locals>.<genexpr>  s7       . .c3'> . . . . . .r.   )network
connectionzresolve hostdns)	lowerr   TIMEOUTr   anyr   EMPTY_VALUENETWORKINTERNAL)r   r   s    @r,   r   r   {  s    mmooGg  g%%)<)G)G!W,,''
 5 5 5 5 &4 5 5 5 5 5 % $$$$
 . . . . &- . . . . . !  r.   Nonec                l    t                                            t                              |            dS )zDrop in-process AND disk caches.

    Used after a token rotation (`hermes secrets onepassword token`) so
    the next startup resolves fresh with the new credential instead of
    serving values cached under the old token's fingerprint.
    N)r    clearr0   r5   s    r,   clear_cachesr     s+     LLNNNi     r.   c                $    t          |            dS )zClear in-process AND disk caches.

    Tests can pass ``home_path`` to scope the disk cleanup to a tmpdir.
    Without it we fall back to the same default resolution as the writer.
    N)r   r5   s    r,   _reset_cache_for_testsr     s     r.   )r!   r"   r#   r$   r   )r1   r2   r#   r   )r7   r8   r#   r9   )rG   r$   r#   r$   )r7   r\   r#   r$   )rI   )rb   r$   r#   r2   )rm   r$   r#   r$   )rt   r$   r#   r\   )
rd   r   r}   r$   r)   r$   rt   r$   r#   r$   )r7   r\   r)   r$   rG   r$   r   r2   rb   r$   r   r   r   r   r1   r2   r#   r9   )r   r   ry   r8   r)   r$   r   r$   rb   r$   r   r   r   r   r1   r2   r#   r   )r   r$   r#   r   )r1   r2   r#   r   )6r   
__future__r   rT   loggingrO   rerh   r   r   pathlibr   typingr   r   r   r   agent.secret_sources._cacher	   r
   r   r   agent.secret_sources.baser   r   	getLoggerr   loggerr   r   compilerp   rx   r$   r"   r    __annotations___DISK_CACHE_BASENAMEr-   r0   r6   rF   r[   ra   rl   rs   r{   r   r   r   r   r   r   r   r'   r.   r,   <module>r     si  % % %N # " " " " "   				 				             . . . . . . . . . . . .            > = = = = = = =		8	$	$  0 
 rz455 : #sC$%	') ) ) ) )& , , , , #      
' ' ' ' '   8E E E E0E E E E* * * * *,B B B B
   . 4 4 4 4 4 4~ '!" $C C C C C C\ %)%7"" $] ] ] ] ] ]JL. L. L. L. L. L. L. L.^   2! ! ! ! !      r.   