
    Fj^                    F   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
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mZ ddlmZ ddlmZmZmZmZ  ej        e          ZdZde Zd	e d
Z dZ!dZ"ee#e#e#f         Z$i Z%de&d<   dZ'dVdWdZ(dXdZ)	 dVdYdZ*	 dVdZd Z+e G d! d"                      Z,e G d# d$                      Z-d[d%Z.d&d'd\d*Z/d]d+Z0d]d,Z1d&d-d^d/Z2d_d2Z3d`d5Z4dad7Z5dbd;Z6dcd=Z7dd>d?d@ddAdddIZ8	 dedfdKZ9dgdMZ:dNd@d&d>d?d@ddOdhdTZ;dVdidUZ<dS )juB  Bitwarden Secrets Manager (`bws` CLI) integration.

Hermes pulls API keys from Bitwarden Secrets Manager at process startup
so they don't have to live in plaintext in ``~/.hermes/.env``.

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

* The ``bws`` binary is auto-installed into ``<hermes_home>/bin/bws`` on
  first use.  Hermes pins one version (``_BWS_VERSION``) and downloads
  the matching asset from the official GitHub Releases page, verifying
  the SHA-256 against the release's published checksum file.
* The access token is stored in ``~/.hermes/.env`` as
  ``BWS_ACCESS_TOKEN`` (or whatever name the user picked in
  ``secrets.bitwarden.access_token_env``).  This is the one
  bootstrap secret — every other provider key can live in Bitwarden.
* Pulling secrets is a single ``bws secret list <project_id>
  --output json`` call.  We cache the result in-process for
  ``cache_ttl_seconds`` so back-to-back ``hermes`` invocations don't
  hammer the API.
* Failures NEVER block Hermes startup.  Missing binary, no network,
  expired token, etc. all emit a one-line warning and continue with
  whatever credentials ``.env`` already had.

The module is intentionally subprocess-driven rather than going through
the ``bitwarden-sdk-secrets`` Python package: one cross-platform binary
is easier to lazy-install than a wheels-with-Rust-extension dependency.
    )annotationsN)	dataclassfield)Path)DictListOptionalTuplez2.0.0z;https://github.com/bitwarden/sdk-sm/releases/download/bws-vzbws-sha256-checksums-z.txt<      zDict[_CacheKey, '_CachedFetch']_CACHEzbws_cache.json	home_pathOptional[Path]returnr   c                    | 6t          t          j        dt          j                    dz                      } | dz  t          z  S )zReturn the disk cache path under hermes_home/cache/.

    `home_path` is what `load_hermes_dotenv()` already resolved; falling back
    to `$HERMES_HOME` / `~/.hermes` keeps direct callers working too.
    NHERMES_HOMEz.hermescache)r   osgetenvhome_DISK_CACHE_BASENAMEr   s    =/usr/local/lib/hermes-agent/agent/secret_sources/bitwarden.py_disk_cache_pathr   X   s?     =$)++	2IJJKK	w!555    	cache_key	_CacheKeystrc                "    | \  }}}| d| d| S )z:Serialize a cache key to a stable string for JSON storage.| )r   token_fp
project_id
server_urls       r   _cache_key_strr%   c   s*    '0$Hj*2222j222r   ttl_secondsfloatOptional['_CachedFetch']c                   |dk    rdS t          |          }	 t          |dd          5 }t          j        |          }ddd           n# 1 swxY w Y   n# t          t          j        f$ r Y dS w xY wt          |t                    sdS |                    d          t          |           k    rdS |                    d          }|                    d          }t          |t                    rt          |t          t          f          sdS d	 |                                D             }t          |t          |          
          }	|	                    |          sdS |	S )zReturn a cached entry from disk if fresh, else None.

    Best-effort: any I/O or parse error returns None and we re-fetch.
    r   Nrutf-8encodingkeysecrets
fetched_atc                n    i | ]2\  }}t          |t                    t          |t                    /||3S r!   )
isinstancer   ).0kvs      r   
<dictcomp>z$_read_disk_cache.<locals>.<dictcomp>   sP     % % %AJq#,>,>%CMaQTCUCU%	1% % %r   r/   r0   )r   openjsonloadOSErrorJSONDecodeErrorr2   dictgetr%   intr'   items_CachedFetchis_fresh)
r   r&   r   pathfpayloadr/   r0   typed_secretsentrys
             r   _read_disk_cacherH   i   s    atI&&D$g... 	#!illG	# 	# 	# 	# 	# 	# 	# 	# 	# 	# 	# 	# 	# 	# 	#T)*   ttgt$$ t{{5^I6666tkk)$$G\**Jgt$$ JzC<,P,P t% % % % %M 5;L;LMMME>>+&& tLs4   A A A AA AA A21A2rG   '_CachedFetch'Nonec                Z   t          |          }	 |j                            dd           t          |           |j        |j        d}t          j        ddt          |j                            \  }}	 t          j
        |dd	          5 }t          j        ||           d
d
d
           n# 1 swxY w Y   t          j        |d           t          j        ||           d
S # t          $ r( 	 t          j        |           n# t"          $ r Y nw xY w w xY w# t"          $ r Y d
S w xY w)zPersist a cache entry to disk atomically with mode 0600.

    Best-effort: any I/O error is swallowed (the next invocation will just
    re-fetch). We never want disk cache failures to break startup.
    Tparentsexist_ok)r.   r/   r0   z.bws_cache_z.tmp)prefixsuffixdirwr+   r,   Ni  )r   parentmkdirr%   r/   r0   tempfilemkstempr   r   fdopenr9   dumpchmodreplaceBaseExceptionunlinkr;   )r   rG   r   rC   rE   fdtmprD   s           r   _write_disk_cacher_      s    I&&D$666!),,}*
 
 " S5E5E
 
 
C
	2sW555 &	'1%%%& & & & & & & & & & & & & & &HS%   JsD!!!!! 	 	 		#   	    sl   A%D 7C' B0$C' 0B44C' 7B48-C' '
D2DD
DDDDD 
D*)D*c                  ,    e Zd ZU ded<   ded<   d
dZd	S )rA   Dict[str, str]r/   r'   r0   r&   r   boolc                P    |dk    rdS t          j                     | j        z
  |k     S )Nr   F)timer0   )selfr&   s     r   rB   z_CachedFetch.is_fresh   s*    !5	do-<<r   N)r&   r'   r   rb   )__name__
__module____qualname____annotations__rB   r!   r   r   rA   rA      sB         = = = = = =r   rA   c                      e Zd ZU dZ ee          Zded<    ee          Z	ded<    ee          Z
ded<    ee          Zded<   d	Zd
ed<   d	Zded<   edd            Zd	S )FetchResultzOutcome of a single BSM pull.)default_factoryra   r/   z	List[str]appliedskippedwarningsNzOptional[str]errorr   binary_pathr   rb   c                    | j         d u S N)rp   )re   s    r   okzFetchResult.ok   s    zT!!r   )r   rb   )rf   rg   rh   __doc__r   r=   r/   ri   listrm   rn   ro   rp   rq   propertyrt   r!   r   r   rk   rk      s         ''#eD999G9999t444G4444t444G4444%555H5555E"&K&&&&" " " X" " "r   rk   c                 (    ddl m}   |             dz  S )z9Where Hermes stores its managed binaries.  Profile-aware.r   get_hermes_homebin)hermes_constantsrz   ry   s    r   _hermes_bin_dirr}      s'    000000?u$$r   Finstall_if_missingr   rb   c                |   t                      t                      z  }|                                r!t          j        |t          j                  r|S t          j        d          }|rt          |          S | rB	 t                      S # t          $ r&}t                              d|           Y d}~dS d}~ww xY wdS )uR  Return a path to a usable ``bws`` binary, or None.

    Resolution order:
      1. ``<hermes_home>/bin/bws``  (our managed copy — preferred)
      2. ``shutil.which("bws")``    (system PATH)

    When ``install_if_missing`` is True and neither resolves, this calls
    :func:`install_bws` to download and verify the pinned version.
    bwszbws auto-install failed: %sN)r}   _platform_binary_nameexistsr   accessX_OKshutilwhichr   install_bws	Exceptionloggerwarning)r   managedsystemexcs       r   find_bwsr      s     "7"9"99G~~ BIgrw77 \%  F F|| 	==  	 	 	NN8#>>>44444	 4s   ;B	 	
B9B44B9c                 8    t          j                    dk    rdndS )NWindowszbws.exer   )platformr   r!   r   r   r   r      s     ))Y6699EAr   c                    t          j                    } t          j                                                    }| dk    rdt           dS | dk    r|dv rdnd}d| d	t           dS | d
k    rx|dv rdnd}d}	 t          j        ddgddd          }d|j        |j        z                                   v rd}n# t          t
          j
        f$ r Y nw xY wd| d| dt           dS t          d|  d|           )u   Map (uname, arch, libc) → the upstream asset filename.

    Asset names follow Rust's target triple convention.  Linux defaults
    to gnu (glibc); we switch to musl only if ldd --version says so.
    Darwinzbws-macos-universal-z.zipr   )arm64aarch64r   x86_64zbws-z-pc-windows-msvc-Linuxgnulddz	--versionT   )capture_outputtexttimeoutmuslz-unknown-linux--z+Unsupported platform for bws auto-install:  )r   r   machinelower_BWS_VERSION
subprocessrunstdoutstderrr;   TimeoutExpiredRuntimeError)r   r   archlibcress        r   _platform_asset_namer      sg    _F  &&((G 9l8888#';;;yy?d??\????#';;;yy
	.$#	  C #*sz188::::23 	 	 	D	DdDD4DD,DDDD
HfHHwHH  s   7?B7 7CC)forcer   c           	     p   t                      }|                    dd           |t                      z  }|                                r| s|S t	                      }t
           d| }t
           dt           }t          j        d          5 }t          |          }||z  }|t          z  }	t                              d|           t          ||           t          ||	           t          |	|          }
t          |          }|
                                |                                k    rt!          d| d|
 d	|           t#          j        |          5 }t'          |t                                }|                    ||           ||z  }d
d
d
           n# 1 swxY w Y   t          j        t-          |          d          \  }}t/          j        |           t3          j        ||           t/          j        |t8          j        t8          j        z  t8          j        z  t8          j         z  t8          j!        z  t8          j"        z  t8          j#        z             t/          j$        ||           d
d
d
           n# 1 swxY w Y   t                              dtJ          |           |S )uZ  Download, verify, and install the pinned ``bws`` binary.

    Returns the path to the installed executable.  Raises on any
    failure (network, checksum, extraction) — callers in the auto-install
    path catch these; the user-facing ``hermes secrets bitwarden setup``
    surface lets them propagate so the wizard can show a clear error.
    TrL   /zhermes-bws-)rO   zDownloading %szChecksum mismatch for z: expected z, got Nz.bws_)rQ   rO   zInstalled bws %s at %s)&r}   rT   r   r   r   _BWS_RELEASE_BASE_BWS_CHECKSUM_NAMErU   TemporaryDirectoryr   r   info_http_download_expected_sha256_sha256_filer   r   zipfileZipFile_pick_zip_memberextractrV   r   r   closer   copy2rY   statS_IRUSRS_IWUSRS_IXUSRS_IRGRPS_IXGRPS_IROTHS_IXOTHrZ   r   )r   bin_dirtarget
asset_name	asset_urlchecksum_urltmpdirr^   zip_pathchecksum_pathexpectedactualzfmember	extractedr]   stageds                    r   r   r   !  s    GMM$M...,...F}} u %''J$33z33I'>>*<>>L		$M	:	:	: !#f6ll#00$i000y(+++|]333#M:>>h''>>v||~~--5 5 5$5 5,25 5  
 _X&& 	%"%b*?*A*ABBFJJvs###fI	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% %#g,,wGGG
F
Y'''
L4<'$,6l!\*l!\*	
 	
 	
 	
66"""C!# !# !# !# !# !# !# !# !# !# !# !# !# !# !#F KK(,???Ms8   CJ
8F"J
"F&	&J
)F&	*CJ

JJurldestc                   t           j                            | ddi          }	 t           j                            |t                    5 }t          |d          5 }t          j        ||           d d d            n# 1 swxY w Y   d d d            d S # 1 swxY w Y   d S # t           j        j	        $ r}t          d|  d|           |d }~ww xY w)Nz
User-Agentzhermes-agent)headers)r   wbzFailed to download : )urllibrequestRequesturlopen_BWS_DOWNLOAD_TIMEOUTr8   r   copyfileobjrp   URLErrorr   )r   r   reqresprD   r   s         r   r   r   [  sd   
.
 
 |^.L
 
M
MCH^##C1F#GG 	,4dD!! ,Q"4+++, , , , , , , , , , , , , , ,	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, <  H H H=====>>CGHs_   &B# BA>2B>B	BB	B	B# BB# BB# #C7CCchecksum_filer   c                0   |                      dd          }|                                D ]Q}|                                                                }t	          |          dk    r|d         |k    r
|d         c S Rt          d| d| j                   )	zParse the upstream ``bws-sha256-checksums-X.Y.Z.txt`` file.

    Format is the standard ``sha256sum`` output: ``<hex>  <filename>``,
    one per line.
    r+   rZ   )r-   errorsr   r   zNo checksum entry for z in )	read_text
splitlinesstripsplitlenr   name)r   r   r   linepartss        r   r   r   e  s     ""GI"FFD!!  

""$$u::??uRyJ668OOO
EEE1CEE  r   rC   c                    t          j                    }t          | d          5 t          fdd          D ]}|                    |           	 d d d            n# 1 swxY w Y   |                                S )Nrbc                 .                          d          S )Ni   )read)rD   s   r   <lambda>z_sha256_file.<locals>.<lambda>x  s    !&&-- r   r   )hashlibsha256r8   iterupdate	hexdigest)rC   hchunkrD   s      @r   r   r   u  s    A	dD		 Q////55 	 	EHHUOOOO	               ;;==s   ,AA"%A"r   zipfile.ZipFilebinary_namec                    fd|                                  D             }|s0t          d d|                                  dd          d          |                    t                     |d         S )	zFind the binary inside the upstream zip.

    Historically the archive has been flat (``bws`` at the root) but we
    tolerate a top-level directory just in case upstream changes.
    c                R    g | ]#}|                     d           d         k    !|$S )r   r   )r   )r3   nr   s     r   
<listcomp>z$_pick_zip_member.<locals>.<listcomp>  s2    NNNaggcll2.>+.M.M!.M.M.Mr   zCould not find z% inside downloaded archive (members: N   z...))r.   r   )namelistr   sortr   )r   r   
candidatess    ` r   r   r   }  s     ONNNR[[]]NNNJ 
1k 1 1rr*1 1 1
 
 	

 OOOa=r   tokenc                    t          j        |                     d                                                    dd         S )uE   SHA-256 prefix used as a cache key — never logged, never displayed.r+   N   )r   r   encoder   )r  s    r   _token_fingerprintr
    s3    >%,,w//00::<<SbSAAr   i,  T )binarycache_ttl_seconds	use_cacher$   r   access_tokenr#   r  r  r  r$    Tuple[Dict[str, str], List[str]]c                (   | st          d          |st          d          t          |           ||pdf}|r`t                              |          }|r|                    |          r	|j        g fS t          |||          }	|	|	t          |<   |	j        g fS |pt          d          }
|
t          d          t          |
| ||          \  }}t          |t          j
                              }|t          |<   |rt          |||           ||fS )	uO  Pull the secrets for ``project_id`` from Bitwarden Secrets Manager.

    Returns ``(secrets_dict, warnings_list)``.

    Set ``server_url`` to point at a non-default Bitwarden region or a
    self-hosted instance — e.g. ``https://vault.bitwarden.eu`` for EU
    Cloud accounts.  When empty, ``bws`` uses its built-in default
    (``https://vault.bitwarden.com``, US Cloud).  This is plumbed into
    the subprocess as ``BWS_SERVER_URL``.

    Caching is a two-layer LRU: an in-process dict (for hot-reload paths
    inside one process) and a disk-persisted JSON file under
    ``<hermes_home>/cache/bws_cache.json`` (for back-to-back CLI invocations).
    Both share the same TTL.  Pass ``home_path`` so disk cache lookups find
    the right directory in tests / non-standard installs; otherwise we fall
    back to ``$HERMES_HOME`` / ``~/.hermes``.

    Raises :class:`RuntimeError` for fatal conditions (missing binary,
    auth failure, unparseable output).  Callers in the env_loader path
    catch this and emit a single warning; callers in the user-facing
    setup wizard let it propagate.
    zBitwarden access token is emptyzBitwarden project_id is emptyr  NTr~   u   bws binary not available — auto-install failed and `bws` is not on PATH.  Install manually from https://github.com/bitwarden/sdk-sm/releases or re-run `hermes secrets bitwarden setup`.r7   )r   r
  r   r>   rB   r/   rH   r   _run_bws_listrA   rd   r_   )r  r#   r  r  r  r$   r   r   cacheddisk_cachedr   r/   ro   rG   s                 r   fetch_bitwarden_secretsr    sR   @  ><=== <:;;;#L11:z?ORPI 
+I&& 	&foo&788 	&>2%%&y2CYOO" !,F9&**

5H555C
{0
 
 	
 &c<ZPPGXTY[[AAAEF9 7)UI666Hr   r   c                   t          |           dd|ddg}t          j                                        }||d<   |                    dd           |r||d<   	 t          j        ||d	d	t          
          }nP# t
          j        $ r}t          dt           d          |d }~wt          $ r}t          d|           |d }~ww xY w|j        dk    rX|j        p|j        pd                                                    dd          }t          d|j         d|d d                    |j                                        }	|	si dgfS 	 t!          j        |	          }
n*# t           j        $ r}t          d|           |d }~ww xY wt'          |
t(                    s$t          dt+          |
          j                   i }g }|
D ]}t'          |t.                    s|                    d          }|                    d          }t'          |t                     rt'          |t                     smt3          |          s|                    d|d           |||<   ||fS )Nsecretrv   z--outputr9   BWS_ACCESS_TOKENNO_COLOR1BWS_SERVER_URLT)envr   r   r   zbws timed out after zs fetching secretszfailed to invoke bws: r   r  zbws exited r      z'bws returned no output (empty project?)zbws returned non-JSON output: zbws returned unexpected shape: r.   valuezSkipping secret z: not a valid env-var name)r   r   environcopy
setdefaultr   r   _BWS_RUN_TIMEOUTr   r   r;   
returncoder   r   r   rZ   r9   loadsr<   r2   rv   typerf   r=   r>   _is_valid_env_nameappend)r   r  r#   r$   cmdr  procr   errrawrE   r/   ro   itemr.   r  s                   r   r  r    s    s88Xvz:v
FC
*//

C*CNN:s###  + *D~$
 
 
 $   G#3GGG
 
	  D D D9C99::CD ! {/dk/R6688@@LL8$/88S#Y88
 
 	
 +



C ?=>>>L*S// L L LACAABBKL gt$$ 
Fd7mm.DFF
 
 	
 !GH  $%% 	hhuoo!!#s## 	:eS+A+A 	!#&& 	OOD3DDD   Hs<   A6 6CBC+B>>C
E F.FFr   c                    | sdS | d                                          s| d         dk    sdS t          d | D                       S )NFr   _c              3  J   K   | ]}|                                 p|d k    V  dS )r/  N)isalnum)r3   cs     r   	<genexpr>z%_is_valid_env_name.<locals>.<genexpr>'  s3      551qyy{{&a3h555555r   )isalphaall)r   s    r   r'  r'  "  sS     uGOO aCu55555555r   r  )access_token_envr#   override_existingr  auto_installr$   r   enabledr6  r7  r8  c                   t                      }| s|S t          j                            |d                                          }	|	sd| d|_        |S |s	d|_        |S t          |          }
|
|_        |
	d|_        |S 	 t          |	||
|||          \  }}n-# t          $ r }t          |          |_        |cY d}~S d}~ww xY w||_        |j                            |           |                                D ]\  }}||k    r|j                            |           &|s:t          j                            |          r|j                            |           b|t          j        |<   |j                            |           |S )	u8  Pull secrets from BSM and set them on ``os.environ``.

    This is the function ``load_hermes_dotenv()`` calls after the .env
    files have loaded.  It is intentionally defensive — any failure
    returns a :class:`FetchResult` with ``error`` set; it never raises.

    ``server_url`` selects the Bitwarden region or self-hosted endpoint
    (e.g. ``https://vault.bitwarden.eu`` for EU Cloud).  Empty string
    means use ``bws``'s default (US Cloud).

    Parameters mirror the ``secrets.bitwarden.*`` config keys so the
    caller can just splat the dict in.
    r  z&secrets.bitwarden.enabled is true but z3 is not set.  Run `hermes secrets bitwarden setup`.zMsecrets.bitwarden.project_id is empty.  Run `hermes secrets bitwarden setup`.r~   Nzhbws binary not available and auto-install is disabled.  Run `hermes secrets bitwarden setup` to install.)r  r#   r  r  r$   r   )rk   r   r   r>   r   rp   r   rq   r  r   r   r/   ro   extendr@   rn   r(  rm   )r9  r6  r#   r7  r  r8  r$   r   resultr  r  r/   ro   r   r.   r  s                   r   apply_bitwarden_secretsr=  /  s   0 ]]F :>>"2B77==??L >5E > > > 	  4 	 666FF~? 	 3%!/!
 
 
    3xx FN
O8$$$mmoo # #
U""" N!!#&&&  	RZ^^C%8%8 	N!!#&&&
3c""""Ms   B 
C%C :C Cc                    t                                            	 t          |                                            dS # t          t
          f$ r Y dS w xY w)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 cache
    writer itself.
    N)r   clearr   r\   FileNotFoundErrorr;   r   s    r   _reset_cache_for_testsrA    s]     LLNNN##**,,,,,w'   s   !> AArs   )r   r   r   r   )r   r   r   r   )r   r   r&   r'   r   r   r   r(   )r   r   rG   rI   r   r   r   rJ   )r   r   )r   rb   r   r   )r   r   )r   rb   r   r   )r   r   r   r   r   rJ   )r   r   r   r   r   r   )rC   r   r   r   )r   r   r   r   r   r   )r  r   r   r   )r  r   r#   r   r  r   r  r'   r  rb   r$   r   r   r   r   r  )r  )
r   r   r  r   r#   r   r$   r   r   r  )r   r   r   rb   )r9  rb   r6  r   r#   r   r7  rb   r  r'   r8  rb   r$   r   r   r   r   rk   )r   r   r   rJ   )=ru   
__future__r   r   r9   loggingr   r   r   r   r   rU   rd   urllib.errorr   urllib.requestr   dataclassesr   r   pathlibr   typingr   r   r	   r
   	getLoggerrf   r   r   r   r   r   r#  r   r   r   ri   r   r   r%   rH   r_   rA   rk   r}   r   r   r   r   r   r   r   r   r
  r  r  r'  r=  rA  r!   r   r   <module>rJ     s    : # " " " " "    				                   ( ( ( ( ( ( ( (       . . . . . . . . . . . .		8	$	$  Q,PP  @\???     #sC- 	*, , , , , ( 6 6 6 6 63 3 3 3 26    B 37         F = = = = = = = = " " " " " " " "(% % % % ,1      6B B B B' ' ' 'T "' 7 7 7 7 7 7tH H H H          ,B B B B "" $@ @ @ @ @ @H FHD D D D DN6 6 6 6  /#" $R R R R R Rt      r   