
    NQj&]                       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%dFdZ& ee%e&          Z'de$d<   dGdHdZ(dIdZ)dJd Z*dKd"Z+dLdMd%Z,dNd'Z-dOd)Z.d#d#d*dPd.Z/d#edd#d/d0dd1dQd7Z0dd#ed#d/d0dd8dRd>Z1 G d? d@e          Z2dSdCZ3dGdTdEZ4dS )Uu  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_TOKENz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        D/home/alina/.hermes/hermes-agent/agent/secret_sources/onepassword.py_disk_key_strr,   v   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).)r/   pathr0   s    r+   _disk_cache_pathr5      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)r6   validwarningsnamerefcleaneds         r+   _validate_referencesrE      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                   dt           j                            | 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``, 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   OP_SESSION_=
utf-8N   )osenvirongetsortedr?   r<   joinhashlibsha256encode	hexdigest)rF   partskeymaterials       r+   _auth_fingerprintrZ      s     	1	2..0052:>>,3355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.rK   c              3  2   K   | ]}| d |          V  dS )rJ   Nr&   ).0rB   r6   s     r+   	<genexpr>z$_refs_fingerprint.<locals>.<genexpr>   s6      UU$D55:d#355UUUUUUr-   rL   NrM   )rR   rQ   rS   rT   rU   rV   )r6   rY   s   ` r+   _refs_fingerprintr`      sb    yyUUUU&BTBTUUUUUH>(//'2233==??DDr-   rH   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   existsrN   accessX_OKshutilwhich)ra   pinnedfounds      r+   find_oprk      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.rH   )_ANSI_CSI_REsubreplacer>   )rl   s    r+   _scrubrr      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.NrI   r   1NO_COLOR)_OP_ENV_ALLOWLISTrN   rO   rP   r;   r?   )rs   envrX   vals       r+   _op_child_envrz      s    C   jnnS!!?CHJ$$&&  S>>-(( 	CH  6*5&'C
OJr-   r(   rs   rc   	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--TrL   rq   )rx   capture_outputrl   encodingerrorstimeoutzop read timed out after zs for Nzfailed to invoke op: r   rH      zop read failed for r:   zop read exited z for z
z$op read returned an empty value for )r#   
subprocessrunrz   _OP_RUN_TIMEOUTTimeoutExpiredRuntimeErrorOSError
returncoderr   stderrstdoutrstripr>   )	rc   r|   r(   rs   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(   rF   binaryra   	use_cachecache_ttl_secondsr0   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.
    rH   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)rE   rN   rO   rP   r>   rZ   r#   r`   r   is_freshdictr   r/   r~   rk   r   rQ   r   r<   r	   timewrite)r6   r(   rF   r   ra   r   r   r0   r@   rA   rs   r    cacheddisk_cachedrc   r   read_errorsrB   r   entrys                       r+   fetch_onepassword_secretsr   6  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)rx   r(   service_account_token_envra   override_existingr   r0   enabledrx   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.r6   r(   rF   r   r   r0   )r   rE   rA   extendr;   skippedr<   rN   rO   rP   rk   ra   errorr   r   r#   r   applied)r   rx   r(   r   ra   r   r   r0   resultr@   rA   refs_to_fetchrB   rC   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                  B    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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mappedrc   cfgr   r"   r   c                r    t          t          |t                    o|                    dd                    S )Nr   T)r   r=   r   rP   )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#   rP   	frozenset)r   r   rF   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)rH   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   rx   r(   r   ra   r   r   )r   )r   s    r+   config_schemazOnePasswordSource.config_schema  s    '65IIR 
  R 
 G-* *  N 
  C" "
  M" "/
 
 	
r-   r0   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 )Nrx   zksecrets.onepassword.enabled is true but the env: map is empty.  Add ENV_VAR: op://vault/item/field entries.ra   rH   r   r   r   r   r   g     r@r(   r   r   )r=   r   r   rP   rE   rA   r   r   r   NOT_CONFIGURED
error_kindr#   rk   ra   BINARY_MISSINGr   	TypeError
ValueErrorr   r   r   _classify_op_errorr   )r   r   r0   r   env_mapr@   rA   ra   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.N)r   r   r"   r   )r   r   )r"   r   )r   r   r0   r   r"   r   )__name__
__module____qualname____doc__rB   labelshapeschemer   r   r   r   r&   r-   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>k  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>q  s7       . .c3'> . . . . . .r-   )network
connectionzresolve hostdns)	lowerr   TIMEOUTr   anyAUTH_FAILEDEMPTY_VALUENETWORKINTERNAL)r   r   s    @r+   r   r   c  s    mmooGg  g%%)<)G)G!W,,''
 5 5 5 5 &4 5 5 5 5 5 % $$$$
 . . . . &- . . . . . !  r-   Nonec                l    t                                            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   clearr/   r4   s    r+   _reset_cache_for_testsr   |  s+     LLNNNi     r-   )r    r!   r"   r#   r   )r0   r1   r"   r   )r6   r7   r"   r8   )rF   r#   r"   r#   )r6   r[   r"   r#   )rH   )ra   r#   r"   r1   )rl   r#   r"   r#   )rs   r#   r"   r[   )
rc   r   r|   r#   r(   r#   rs   r#   r"   r#   )r6   r[   r(   r#   rF   r#   r   r1   ra   r#   r   r   r   r   r0   r1   r"   r8   )r   r   rx   r7   r(   r#   r   r#   ra   r#   r   r   r   r   r0   r1   r"   r   )r   r#   r"   r   )r0   r1   r"   r   )5r   
__future__r   rS   loggingrN   rerg   r   r   pathlibr   typingr   r   r   r   agent.secret_sources._cacher	   r
   r   r   agent.secret_sources.baser   r   	getLoggerr   loggerr   r   compilero   rw   r#   r!   r   __annotations___DISK_CACHE_BASENAMEr,   r/   r5   rE   rZ   r`   rk   rr   rz   r   r   r   r   r   r   r&   r-   r+   <module>r      sP  % % %N # " " " " "   				 				             . . . . . . . . . . . .            > = = = = = = =		8	$	$  0 
 rz455 4 #sC$%	') ) ) ) )& , , , , #      
' ' ' ' '   8E E E E*E E E E* * * * *,B B B B
   . 4 4 4 4 4 4~ '!" $C C C C C C\ %)%7"" $] ] ] ] ] ]Jz z z z z z z zz   2! ! ! ! ! ! !r-   