
    epjd                       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	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 ddlmZ  ej        e          Zd	Z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Z.dd#ed#d/d0dd8dSd>Z/ G d? d@e          Z0dTdCZ1dHdUdEZ2dHdUdFZ3d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)get_source_environment   OP_SERVICE_ACCOUNT_TOKEN)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        F/home/thesage/.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).)r1   pathr2   s    r-   _disk_cache_pathr7      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)r8   validwarningsnamerefcleaneds         r-   _validate_referencesrG      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                   t                      }d|                    | d           d|                    dd           d|                    dd           d|                    dd           g}t          |          D ]7}|                    d	          r |                    | d
||                     8d                    |          }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   )
r   getsortedrA   r>   joinhashlibsha256encode	hexdigest)rH   
source_envpartskeymaterials        r-   _auth_fingerprintr[      s    ())J0	2..005:>>,3355?
'8"==??A(:B??AA	E j!! 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.rM   c              3  2   K   | ]}| d |          V  dS )rL   Nr(   ).0rD   r8   s     r-   	<genexpr>z$_refs_fingerprint.<locals>.<genexpr>   s6      UU$D55:d#355UUUUUUr/   rN   NrO   )rR   rQ   rS   rT   rU   rV   )r8   rZ   s   ` r-   _refs_fingerprintra      sb    yyUUUU&BTBTUUUUUH>(//'2233==??DDr/   rJ   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   existsosaccessX_OKshutilwhich)rb   pinnedfounds      r-   find_oprm      sl      k""==?? 	ry99 	MtLE)4;;;T)r/   textc                p    ddl m}  ||                               dd                                          S )zCRemove ANSI control sequences and trim, for safe message surfacing.r   )
strip_ansirJ   )tools.ansi_striprp   replacer@   )rn   rp   s     r-   _scrubrt      sB    ++++++ :d##FB//55777r/   token_valuec                    t                      }i }t          D ]}|                    |          }||||<   |                                D ]\  }}|                    d          r|||<    | r| |d<   d|d<   |S )zEBuild a minimal allowlisted environment for the ``op`` child process.NrK   r   1NO_COLOR)r   _OP_ENV_ALLOWLISTrP   r=   rA   )ru   rW   envrY   vals        r-   _op_child_envr|      s    '))JC   nnS!!?CH$$&&  S>>-(( 	CH  6*5&'C
OJr/   r*   ru   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--TrN   rs   )rz   capture_outputrn   encodingerrorstimeoutzop read timed out after zs for Nzfailed to invoke op: r   rJ      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
returncodert   stderrstdoutrstripr@   )	rd   r~   r*   ru   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*   rH   binaryrb   	use_cachecache_ttl_secondsr2   r   r   boolr   floatc                   t          |           \  }}	|si |	fS t                                          |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.
    rJ   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)rG   r   rP   r@   r[   r%   ra   r!   is_freshdictr   r1   r   rm   r   rQ   r   r>   r	   timewrite)r8   r*   rH   r   rb   r   r   r2   rB   rC   ru   r"   cacheddisk_cachedrd   r   read_errorsrD   r   entrys                       r-   fetch_onepassword_secretsr   A  s@   * +:66OE8 8|(**..y"==CCEEK)$$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)rz   r*   service_account_token_envrb   override_existingr   r2   enabledrz   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.r8   r*   rH   r   r   r2   )r   rG   rC   extendr=   skippedr>   rf   environrP   rm   rb   errorr   r   r%   r   applied)r   rz   r*   r   rb   r   r   r2   resultrB   rC   refs_to_fetchrD   rE   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   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   rH   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)rJ   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   rz   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/   r2   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 )Nrz   zksecrets.onepassword.enabled is true but the env: map is empty.  Add ENV_VAR: op://vault/item/field entries.rb   rJ   r   r   r   r   r   g     r@r*   r   r   )r?   r   r   rP   rG   rC   r   r   r   NOT_CONFIGURED
error_kindr%   rm   rb   BINARY_MISSINGr   	TypeError
ValueErrorr   r   r   _classify_op_errorr   )r   r   r2   r   env_maprB   rC   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%   rP   r   superremediation)r   kindr   rH   	__class__s       r-   r   zOnePasswordSource.remediationm  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   r2   r   r$   r   )r   r   r$   r%   )__name__
__module____qualname____doc__rD   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!   clearr1   r6   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   r6   s    r-   _reset_cache_for_testsr     s     r/   )r"   r#   r$   r%   r   )r2   r3   r$   r   )r8   r9   r$   r:   )rH   r%   r$   r%   )r8   r\   r$   r%   )rJ   )rb   r%   r$   r3   )rn   r%   r$   r%   )ru   r%   r$   r\   )
rd   r   r~   r%   r*   r%   ru   r%   r$   r%   )r8   r\   r*   r%   rH   r%   r   r3   rb   r%   r   r   r   r   r2   r3   r$   r:   )r   r   rz   r9   r*   r%   r   r%   rb   r%   r   r   r   r   r2   r3   r$   r   )r   r%   r$   r   )r2   r3   r$   r   )4r   
__future__r   rS   loggingrf   ri   r   r   pathlibr   typingr   r   r   r   agent.secret_sources._cacher	   r
   r   r   agent.secret_sources.baser   r   r   	getLoggerr   loggerr   r   ry   r%   r#   r!   __annotations___DISK_CACHE_BASENAMEr.   r1   r7   rG   r[   ra   rm   rt   r|   r   r   r   r   r   r   r   r(   r/   r-   <module>r     sZ  % % %N # " " " " "   				             . . . . . . . . . . . .            > = = = = = = = < < < < < <		8	$	$  0  : #sC$%	') ) ) ) )& , , , , #      
' ' ' ' '   8E E E E2E E E E* * * * *,8 8 8 8   0 4 4 4 4 4 4~ '!" $C C C C C C\ %)%7"" $] ] ] ] ] ]JL. L. L. L. L. L. L. L.^   2! ! ! ! !      r/   