
    `gjj                       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 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jD                  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.d1dZ/ ee.e/      Z0de-d<   d2d3dZ1d4dZ2ddd5dZ3d6dZ4d6dZ5ddd7dZ6d8dZ7d9dZ8d:d Z9d;d!Z:	 	 	 	 	 	 	 	 d<d"Z;d=d#Z<dd$d%d&dd'	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d>d(Z=	 d?	 	 	 	 	 	 	 	 	 d@d)Z>d*d&dd$d%d&dd+	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dAd,Z? G d- d.e!      Z@dBd/ZAd2dCd0ZBy)DuB  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)Path)DictListOptionalTuple)CachedFetch	DiskCacheFetchResultis_valid_env_name)	ErrorKindSecretSourcez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.jsonc                "    | \  }}}| d| d| S )z:Serialize a cache key to a stable string for JSON storage.| )	cache_keytoken_fp
project_id
server_urls       Q/root/.hermes/venv/lib/python3.12/site-packages/agent/secret_sources/bitwarden.py_cache_key_strr   `   s%    '0$Hj*ZqAj\22    )key_serializerr
   _DISK_CACHEc                ,    t         j                  |       S )zReturn the disk cache path under hermes_home/cache/.

    Thin wrapper over the shared DiskCache, kept for tests and any direct
    callers; falls back to `$HERMES_HOME` / `~/.hermes` when home is None.
    )r   path	home_paths    r   _disk_cache_pathr"   k   s     I&&r   c                 "    ddl m}   |        dz  S )z9Where Hermes stores its managed binaries.  Profile-aware.r   get_hermes_homebin)hermes_constantsr%   r$   s    r   _hermes_bin_dirr(   y   s    0u$$r   Finstall_if_missingc                P   t               t               z  }|j                         r&t        j                  |t        j
                        r|S t        j                  d      }|rt        |      S | r	 t               S y# t        $ r }t        j                  d|       Y d}~yd}~ww xY w)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existsosaccessX_OKshutilwhichr   install_bws	Exceptionloggerwarning)r*   managedsystemexcs       r   find_bwsr;      s     "7"99G~~BIIgrww7\\% FF|	=    	NN8#>	s   1	A< <	B%B  B%c                 8    t        j                         dk(  rdS dS )NWindowszbws.exer,   )platformr9   r   r   r   r-   r-      s     )Y69AEAr   c                 
   t        j                         } t        j                         j                         }| dk(  r
dt         dS | dk(  r|dv rdnd}d| d	t         dS | d
k(  rp|dv rdnd}d}	 t        j                  ddgdddt
        j                        }d|j                  |j                  z   j                         v rd}d| d| dt         dS t        d|  d|       # t        t
        j                  f$ r Y <w xY w)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aarch64rB   x86_64zbws-z-pc-windows-msvc-Linuxgnulddz	--versionT   )capture_outputtexttimeoutstdinmuslz-unknown-linux--z+Unsupported platform for bws auto-install:  )r>   r9   machinelower_BWS_VERSION
subprocessrunDEVNULLstdoutstderrOSErrorTimeoutExpiredRuntimeError)r9   rO   archlibcress        r   _platform_asset_namer]      s4    __F &&(G &l^488#';;ydV,\N$??#';;y	..$# ((C #**szz188:: dV?4&,tDD

5fXQwiH 	 223 		s   0AC& &DD)forcec           	        t               }|j                  dd       |t               z  }|j                         r| s|S t	               }t
         d| }t
         dt         }t        j                  d      5 }t        |      }||z  }|t        z  }	t        j                  d|       t        ||       t        ||	       t        |	|      }
t        |      }|
j                         |j                         k7  rt!        d| d|
 d	|       t#        j$                  |      5 }t'        |t                     }t)        |||      }d
d
d
       t        j*                  t-        |      d      \  }}t/        j0                  |       t3        j4                  |       t/        j6                  |t8        j:                  t8        j<                  z  t8        j>                  z  t8        j@                  z  t8        jB                  z  t8        jD                  z  t8        jF                  z         t/        jH                  ||       d
d
d
       t        j                  dtJ        |       |S # 1 sw Y   xY w# 1 sw Y   3xY w)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.
    T)parentsexist_ok/zhermes-bws-)prefixzDownloading %szChecksum mismatch for z: expected z, got Nz.bws_)dirrc   zInstalled bws %s at %s)&r(   mkdirr-   r.   r]   _BWS_RELEASE_BASE_BWS_CHECKSUM_NAMEtempfileTemporaryDirectoryr   r6   info_http_download_expected_sha256_sha256_filerP   rY   zipfileZipFile_pick_zip_member_safe_extract_membermkstempstrr/   closer2   copy2chmodstatS_IRUSRS_IWUSRS_IXUSRS_IRGRPS_IXGRPS_IROTHS_IXOTHreplacerQ   )r^   bin_dirtarget
asset_name	asset_urlchecksum_urltmpdirtmpzip_pathchecksum_pathexpectedactualzfmember	extractedfdstageds                    r   r4   r4      s    GMM$M.,..F}}u%'J$%Qzl3I'(*<)=>L		$	$M	: $#f6l#00$i0y(+|]3#M:>h'>>v||~-( 5$:VF85 
 __X& 	>"%b*?*ABF
 -R=I	> %%#g,wG
F
Y'
LL4<<'$,,6ll!\\*ll!\\*	
 	

66"I$#L KK(,?M-	> 	>#$# $#s&   9B)I,""IC6I,I)	$I,,I5c                   t         j                  j                  | ddi      }	 t         j                  j                  |t              5 }t        |d      5 }t        j                  ||       d d d        d d d        y # 1 sw Y   xY w# 1 sw Y   y xY w# t         j                  j                  $ r}t        d|  d|       |d }~ww xY w)Nz
User-Agentzhermes-agent)headers)rJ   wbzFailed to download : )urllibrequestRequesturlopen_BWS_DOWNLOAD_TIMEOUTopenr2   copyfileobjerrorURLErrorrY   )urldestreqrespfr:   s         r   rk   rk     s    
..
 
 |^.L
 
MCH^^##C1F#G 	,4dD! ,Q""4+,	, 	,, ,	, 	, <<   H0Ru=>CGHsM   %B 
BA?.B6B ?B	BBB B C4CCc                    | j                  dd      }|j                         D ]>  }|j                         j                         }t	        |      dk\  s0|d   |k(  s9|d   c S  t        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.
    utf-8r   )encodingerrorsrG   r   zNo checksum entry for z in )	read_text
splitlinesstripsplitlenrY   name)checksum_filer   rI   linepartss        r   rl   rl     s     ""GI"FD! 

""$u:?uRyJ68O 
 D1C1C0DE r   c                    t        j                         }t        | d      5 t        fdd      D ]  }|j	                  |        	 d d d        |j                         S # 1 sw Y   |j                         S xY w)Nrbc                 &     j                  d      S )Ni   )read)r   s   r   <lambda>z_sha256_file.<locals>.<lambda>$  s    !&&- r   r   )hashlibsha256r   iterupdate	hexdigest)r   hchunkr   s      @r   rm   rm   !  si    A	dD	 Q/5 	EHHUO	 ;;= ;;=s   &A!!A9c                    | j                         D cg c]  }|j                  d      d   |k(  s| }}|s#t        d| d| j                         dd  d      |j                  t               |d	   S c c}w )
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.
    rb   r   zCould not find z% inside downloaded archive (members: N   z...))keyr   )namelistr   rY   sortr   )r   binary_namen
candidatess       r   rp   rp   )  s      [[]Naggcl2.>+.M!NJNk] +r*+41
 	

 OOOa= Os
   A6A6c                   t         j                  j                  |      }t         j                  j                  t         j                  j                  ||            }	 t         j                  j	                  ||g      |k(  }|r||k(  rt        d|d      | j                  ||       t        |      S # t
        $ r d}Y @w xY w)aM  Extract a single archive member, refusing path traversal.

    ``ZipFile.extract`` will happily honour member names containing
    ``../`` or absolute paths, letting a malicious archive write outside
    ``dest_dir`` (a "zip-slip").  We resolve the would-be target and
    confirm it stays within ``dest_dir`` before extracting.
    Fz*Refusing to extract unsafe archive member z%: it escapes the extraction directory)	r/   r   realpathjoin
commonpath
ValueErrorrY   extractr   )r   r   dest_dir	dest_rootr   	containeds         r   rq   rq   :  s       *IWWbggll9f=>FGG&&	6':;yH	 )+8
 C2 3
 	
 JJvy!<  	s   $B5 5CCc                l    t        j                  | j                  d            j                         dd S )uE   SHA-256 prefix used as a cache key — never logged, never displayed.r   N   )r   r   encoder   )tokens    r   _token_fingerprintr   Z  s*    >>%,,w/0::<SbAAr   ,  T )binarycache_ttl_seconds	use_cacher   r!   c                   | st        d      |st        d      t        |       ||xs df}|rft        j                  |      }|r|j	                  |      r|j
                  g fS t        j                  |||      }	|	|	t        |<   |	j
                  g fS |xs t        d      }
|
t        d      t        |
| ||      \  }}t        |t        j                               }|t        |<   |rt        j                  ||||       ||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   Tr)   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`.)secrets
fetched_at)rY   r   r   getis_freshr   r   r   r;   _run_bws_list_CachedFetchtimewrite)access_tokenr   r   r   r   r   r!   r   cacheddisk_cachedr,   r   warningsentrys                 r   fetch_bitwarden_secretsr   _  s   @ <==:;;#L1:z?ORPII&foo&78>>2%%!&&y2CYO" !,F9&&**

5H5C
{0
 	
 &c<ZPGXTYY[AEF9)U,=yIHr   c                   t        |       dd|ddg}t        j                  j                         }||d<   |j	                  dd       |r||d<   	 t        j                  ||d	d	t        t
        j                  
      }|j                  dk7  rZ|j                  xs |j                  xs dj                         j!                  dd      }t        d|j                   d|d d        |j                  j                         }	|	si dgfS 	 t#        j$                  |	      }
t)        |
t*              s!t        dt-        |
      j.                         i }g }|
D ]|  }t)        |t0              s|j3                  d      }|j3                  d      }t)        |t               rt)        |t               sWt5        |      s|j7                  d|d       x|||<   ~ ||fS # t
        j                  $ r}t        dt         d      |d }~wt        $ r}t        d|       |d }~ww xY w# t"        j&                  $ r}t        d|       |d }~ww xY w)Nsecretlistz--outputjsonBWS_ACCESS_TOKENNO_COLOR1BWS_SERVER_URLT)envrH   rI   rJ   rK   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)rs   r/   environcopy
setdefaultrR   rS   _BWS_RUN_TIMEOUTrT   rX   rY   rW   
returncoderV   rU   r   r   r   loadsJSONDecodeError
isinstancer   type__name__dictr   _is_valid_env_nameappend)r,   r   r   r   cmdr   procr:   errrawpayloadr   r   itemr   r   s                   r   r   r     sh    s8Xvz:v
FC
**//
C*CNN:s#  *D~~$$$
 ! {{/dkk/R668@@L$//*"S#YK8
 	
 ++


C=>>>L**S/ gt$-d7m.D.D-EF
 	
 !GH $%hhuo!#s#:eS+A!#&OO"3')CD  HY $$ "#3"44FG
	  D3C59:CD"  L;C5ABKLs<   -G H  H)G==H	HH I3IIr   )access_token_envr   override_existingr   auto_installr   r!   c                   t               }| s|S t        j                  j                  |d      j	                         }	|	sd| d|_        |S |s	d|_        |S t        |      }
|
|_        |
	d|_        |S 	 t        |	||
|||      \  }}||_        |j                  j                  |       |j                         D ]  \  }}||k(  r|j                  j!                  |       '|s;t        j                  j                  |      r|j                  j!                  |       d|t        j                  |<   |j"                  j!                  |        |S # t        $ r}t        |      |_        |cY d}~S d}~ww xY w)	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   &secrets.bitwarden.enabled is true but 3 is not set.  Run `hermes secrets bitwarden setup`.Msecrets.bitwarden.project_id is empty.  Run `hermes secrets bitwarden setup`.r)   Nhbws binary not available and auto-install is disabled.  Run `hermes secrets bitwarden setup` to install.r   r   r   r   r   r!   )r   r/   r   r   r   r   r;   binary_pathr   rY   rs   r   r   extenditemsskippedr   applied)enabledr  r   r  r   r  r   r!   resultr   r   r   r   r:   r   r   s                   r   apply_bitwarden_secretsr    s   0 ]F::>>"2B7==?L45E4F G> > 	 4 	 6FF~? 	 3%!/!
 FN
OO8$mmo #
U"" NN!!#& RZZ^^C%8NN!!#&

3c"# M)  3xs   6E 	E7E2,E72E7c                  @    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y
)BitwardenSourceu   Bitwarden Secrets Manager 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``).

    Bitwarden is a **bulk** source: it injects every secret in the
    configured BSM project, so explicit per-var bindings from mapped
    sources (e.g. the 1Password ``env:`` map) outrank it.
    	bitwardenzBitwarden Secrets Managerbulkr,   c                \    t        t        |t              xr |j                  dd            S )Nr  T)boolr   r   r   )selfcfgs     r   r  z!BitwardenSource.override_existing[  s'    
 JsD)Pcgg6I4.PQQr   c                z    d}t        |t              rt        |j                  d      xs |      }t	        |h      S )Nr   r  )r   r   rs   r   	frozenset)r  r  	token_envs      r   protected_env_varsz"BitwardenSource.protected_env_varsb  s7    &	c4 CGG$67D9EI)%%r   c           	     @    ddddddddddd	dd
dddddddddS )NzMaster switchF)descriptiondefaultz0Env var holding the machine-account access tokenr   zBSM project UUIDr   z!Disk+memory cache TTL; 0 disablesr   z&BSM values overwrite .env/shell valuesTz#Auto-download the pinned bws binaryz0Region / self-hosted endpoint (empty = US Cloud))r  r  r   r   r  r  r   r   )r  s    r   config_schemazBitwardenSource.config_schemah  s^    '65IQ-! +=LB"
  H"
  E
  R'
 	
r   c                   t        |t              r|ni }t               }t        |j	                  d      xs d      }t
        j                  j	                  |d      j                         }|s"d| d|_        t        j                  |_        |S t        |j	                  d      xs d      }|sd|_        t        j                  |_        |S t        |j	                  dd	            }t        |
      }||_        |d|_        t        j                  |_        |S 	 t!        |j	                  dd            }		 t'        ||||	t        |j	                  dd      xs d      j                         |      \  }
}|
|_        |j.                  j1                  |       |S # t"        t$        f$ r d}	Y ww xY w# t(        $ r5}t        |      |_        t+        t        |            |_        |cY d }~S d }~ww xY w)Nr  r   r   r	  r
  r   r  r  Tr)   r  r   r   g     r@r   r  )r   r   r   rs   r   r/   r   r   r   r   NOT_CONFIGURED
error_kindr  r;   r  BINARY_MISSINGfloat	TypeErrorr   r   rY   _classify_bws_errorr   r   r  )r  r  r!   r  r  r   r   r  r   ttlr   r   r:   s                r   fetchzBitwardenSource.fetch  s   T*csww'9:P>PQzz~~&6;AAC89I8J KB B L !* 8 8FM.4"5
8 L !* 8 8FMCGGND9:\:#>C L !* 8 8FM	 3S9:C	 7)%"%sww|R8>B?EEG#!GX !x(' :& 	C	  	s8FL 3CH =FM	s0   F 8?F2 F/.F/2	G0;*G+%G0+G0N)r  r   returnr  )r  r   )r/  r   )r  r   r!   r   r/  r   )r   
__module____qualname____doc__r   labelshapeschemer  r!  r%  r.  r   r   r   r  r  I  s2    
 D'EEFR&
47r   r  c                ,   | j                         dv rt        j                  S dv sdv rt        j                  S t	        fddD              rt        j
                  S t	        fddD              rt        j                  S t        j                  S )zABest-effort mapping of bws failure text onto the shared taxonomy.z	timed outzbinary not availablezfailed to invokec              3  &   K   | ]  }|v  
 y wNr   .0toklowereds     r   	<genexpr>z&_classify_bws_error.<locals>.<genexpr>  s      Ec3'> E   )unauthorizedzinvalid tokenzaccess token401403c              3  &   K   | ]  }|v  
 y wr8  r   r9  s     r   r=  z&_classify_bws_error.<locals>.<genexpr>  s      :c3'> :r>  )network
connectionresolvedownloaddns)rP   r   TIMEOUTr)  anyAUTH_FAILEDNETWORKINTERNAL)messager<  s    @r   r,  r,    s    mmoGg   (,>',I'''
 E &D E E$$$
 : &9 : :   r   c                V    t         j                          t        j                  |        y)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    s    r   _reset_cache_for_testsrP    s     LLNi r   )r   	_CacheKeyr/  rs   r8  )r!   Optional[Path]r/  r   )r/  r   )r*   r  r/  rR  )r/  rs   )r^   r  r/  r   )r   rs   r   r   r/  None)r   r   r   rs   r/  rs   )r   r   r/  rs   )r   zipfile.ZipFiler   rs   r/  rs   )r   rT  r   rs   r   r   r/  r   )r   rs   r/  rs   )r   rs   r   rs   r   rR  r   r*  r   r  r   rs   r!   rR  r/   Tuple[Dict[str, str], List[str]])r   )
r,   r   r   rs   r   rs   r   rs   r/  rU  )r  r  r  rs   r   rs   r  r  r   r*  r  r  r   rs   r!   rR  r/  r   )rM  rs   r/  r   )r!   rR  r/  rS  )Cr2  
__future__r   r   r   loggingr/   r>   r2   rw   rR   rh   r   urllib.errorr   urllib.requestrn   pathlibr   typingr   r   r   r   agent.secret_sources._cacher	   r   r
   r   r   r   agent.secret_sources.baser   r   	getLoggerr   r6   rQ   rf   rg   r   r   rs   rQ  r   __annotations___DISK_CACHE_BASENAMEr   r   r"   r(   r;   r-   r]   r4   rk   rl   rm   rp   rq   r   r   r   r  r  r,  rP  r   r   r   <module>ra     s  : #    	           . .  >			8	$  B,P  -\N$?     #sC- 	(*% * ( 3 #Y 
'% ,1 6B(V "' :zH "!$04	@B "" $@@ @ 	@
 @ @ @ @ &@H FHE	E E.1E?BE%E` /#" $RR R 	R
 R R R R R Rtpl pf*!r   