
    TdjÉ             	      2   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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	Z#d
Z$dZ%dZ&dZ'dZ(dZ)dZ*dZ+dZ,dZ-de.d<   dddddddddZ/de.d <   d!d"d#d$d%d&d#d$d'd(d)d$d*Z0d+e.d,<   d-Z1de.d.<   d/Z2de.d0<   d1Z3de.d2<   d3Z4de.d4<    e5e
d5e
j6                  Z7i Z8d6e.d7<   e G d8 d9                      Z9e G d: d;                      Z:dd>Z;dd?Z<dd@Z=ddBZ>ddCZ?dDdEddIZ@dDdJddLZAddPZBddSZCddVZDddXZEdd]ZFdd_ZGdDdJddaZHddddZIddeZJdDdJddfZKddhZL	 dddkZMddlZNddpZOddqZPe,dddddrdd}ZQddZRddZSddZTddZUddddZVddddZWdDdddZXddZYddZZdZ[da\dge.d<   ddZ]ddZ^ddZ_ddZ`dddddDddddZaddZbddddZcddDddddZdddZeddZfddZgddZhddZiddddZjddZkg dZldS )u
  iron-proxy (`ironsh/iron-proxy`) integration for credential-injecting egress control.

Why
---

Remote terminal sandboxes (Docker, Modal, SSH) currently see real upstream
API credentials.  A prompt-injected agent inside one of these sandboxes can
``cat ~/.config/openrouter/auth.json`` or ``printenv | grep -i key`` and
exfiltrate them.

iron-proxy is a TLS-intercepting egress firewall (Apache-2.0, Go binary, by
ironsh).  It sits between the sandbox and the internet, enforces a default-deny
allowlist on outbound hosts, and *swaps proxy tokens for real credentials*
on the way out.  The sandbox only ever holds opaque proxy tokens — leaking
them is useless, since they only work behind the configured trusted proxy
boundary (the CA private key and proxy endpoint integrity are part of that
boundary: if traffic can be redirected to attacker-controlled proxy
infrastructure, the guarantee no longer holds).

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

* The ``iron-proxy`` binary is auto-installed into ``<hermes_home>/bin/iron-proxy``
  on first use.  Hermes pins one upstream version (``_IRON_PROXY_VERSION``)
  and downloads the matching tar.gz from the official GitHub Releases page,
  verifying the SHA-256 against the release's ``checksums.txt``.

* A long-lived CA at ``<hermes_home>/proxy/ca.{crt,key}`` is generated on
  first ``hermes egress setup``.  Sandboxes trust this CA so iron-proxy can
  terminate TLS and rewrite headers.

* The proxy config lives at ``<hermes_home>/proxy/proxy.yaml``.  It enumerates
  the per-provider allowlists and the ``secrets`` transform that does the
  Authorization-header swap.

* Token mappings (proxy token -> real credential lookup) live alongside the
  config.  The real credential is **never** written to the config — iron-proxy
  reads it from its own environment via ``{type: env, var: NAME}``.  When
  Bitwarden Secrets Manager is configured, the real value is pulled there
  at proxy startup instead.

* The proxy runs as a managed subprocess (``hermes egress start``), pidfile
  at ``<hermes_home>/proxy/iron-proxy.pid``.  Daemon output (including
  per-request records on v0.39) goes to ``<hermes_home>/proxy/iron-proxy.log``;
  ``audit.log`` is pre-created but reserved for a future pin that supports
  ``log.audit_path``.

* Failures (binary missing, port collision, bad config) emit a one-line
  warning and do *not* block agent startup.  The Docker backend refuses to
  start a sandbox with the proxy enabled-but-down, with a clear error.

This module is intentionally subprocess-driven rather than depending on any
iron-proxy Python bindings — a single cross-platform binary is easier to
lazy-install than a wheels-with-extension dependency, and we keep maintenance
to a "bump the pinned version" loop.
    )annotationsN)	dataclassfield)Path)DictListOptionalTuplez0.39.0z8https://github.com/ironsh/iron-proxy/releases/download/vzchecksums.txtzchecksums.txt.asczpublic-key.ascx         HERMES_IRON_PROXY_MGMT_KEY      i#  )openrouter.ai*.openrouter.aiapi.openai.comapi.anthropic.com!generativelanguage.googleapis.comapi.x.aiapi.mistral.aiapi.groq.comapi.together.xyzapi.deepseek.cominference.nousresearch.comTuple[str, ...]_DEFAULT_ALLOWED_HOSTS)r   r   )r   )r   )r   )r   )r   )r   )r   )OPENROUTER_API_KEYOPENAI_API_KEYGROQ_API_KEYTOGETHER_API_KEYDEEPSEEK_API_KEYMISTRAL_API_KEYXAI_API_KEYNOUS_API_KEYzDict[str, Tuple[str, ...]]_BEARER_PROVIDERS)r   )z	x-api-keyAuthorization )hostsmatch_headersaliases)z*.openai.azure.comz*.cognitiveservices.azure.comz*.services.ai.azure.com)zapi-keyr'   )r   )zx-goog-api-key)GOOGLE_API_KEY)ANTHROPIC_API_KEYAZURE_OPENAI_API_KEYGEMINI_API_KEYz%Dict[str, Dict[str, Tuple[str, ...]]]_HEADER_AUTH_PROVIDERS)AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYGOOGLE_APPLICATION_CREDENTIALS_NON_BEARER_PROVIDERS)z127.0.0.0/8z::1/128z169.254.0.0/16z	fe80::/10z
10.0.0.0/8z172.16.0.0/12z192.168.0.0/16zfc00::/7z::ffff:0:0/96z100.64.0.0/10z198.18.0.0/15_DEFAULT_UPSTREAM_DENY_CIDRS)PATHHOMETMPDIRTZLANGLC_ALLLC_CTYPENO_COLORSSL_CERT_DIRSSL_CERT_FILE
SYSTEMROOTUSERPROFILE_PROXY_SUBPROCESS_ENV_ALLOWLIST)HTTPS_PROXYhttps_proxy
HTTP_PROXY
http_proxy	ALL_PROXY	all_proxyNO_PROXYno_proxy_PROXY_SUBPROCESS_ENV_STRIPSIGKILLDict[str, str]_VERSION_CACHEc                      e Zd ZU dZdZded<   dZded<   dZded	<   dZded
<   dZ	ded<   dZ
ded<   dZded<   eZded<    ee          Zded<   edd            Zedd            ZdS )ProxyStatusz8Snapshot of the iron-proxy installation + runtime state.FboolenabledNOptional[Path]binary_pathOptional[str]binary_versionconfig_pathca_cert_pathOptional[int]pid	listeninginttunnel_port)default_factory	List[str]warningsreturnc                F    | j         d uo| j                                         S N)rT   existsselfs    D/home/thesage/.hermes/hermes-agent/agent/proxy_sources/iron_proxy.py	installedzProxyStatus.installed9  s$    t+I0@0G0G0I0II    c                    | j         d uo:| j                                         o!| j        d uo| j                                        S rc   )rW   rd   rX   re   s    rg   
configuredzProxyStatus.configured=  sT     D( + ''))+!-+ !((**		
ri   ra   rQ   )__name__
__module____qualname____doc__rR   __annotations__rT   rV   rW   rX   rZ   r[   _DEFAULT_TUNNEL_PORTr]   r   listr`   propertyrh   rk   r(   ri   rg   rP   rP   +  s        BBG"&K&&&&$(N(((("&K&&&&#'L''''CI+K++++%555H5555J J J XJ 
 
 
 X
 
 
ri   rP   c                  N    e Zd ZU dZded<   ded<   ded<   dZded<   d	Zded
<   dS )TokenMappinguL  Map a sandbox-visible proxy token to a real upstream credential lookup.

    ``real_env_name`` is the env-var name iron-proxy reads at egress time.
    When Bitwarden is configured as the credential source for the proxy,
    iron-proxy's *own* environment is populated from bws on startup — the
    sandbox still sees only ``proxy_token``.

    ``match_headers`` names the request headers iron-proxy scans for the
    proxy token (default: ``Authorization`` for bearer providers; e.g.
    ``("x-api-key", "Authorization")`` for Anthropic native).

    ``alias_env_names`` are additional env-var names the SANDBOX receives
    the same proxy token under (e.g. ``GOOGLE_API_KEY`` for
    ``GEMINI_API_KEY``).  They do not appear in the iron-proxy config —
    only one secrets rule is emitted per mapping, keyed on
    ``real_env_name``.
    strproxy_tokenreal_env_namer   upstream_hostsr'   r*   r(   alias_env_namesN)rm   rn   ro   rp   rq   r*   r|   r(   ri   rg   rv   rv   G  sd          $ ####%7M7777')O))))))ri   rv   ra   r   c                 (    ddl m}   |             dz  S )Nr   get_hermes_homebinhermes_constantsr   r~   s    rg   _hermes_bin_dirr   g  s'    000000?u$$ri   c                 (    ddl m}   |             dz  S )u   Return the proxy state dir without creating it.

    Read-only callers (status probes, pidfile reads, version queries) use
    this — there's no reason to materialize ``~/.hermes/proxy/`` just to
    check whether a pidfile exists.
    r   r~   proxyr   r~   s    rg   _proxy_state_dir_ror   m  s)     100000?w&&ri   c                     t                      } |                     dd           	 |                     d           n# t          $ r Y nw xY w| S )u  Return the proxy state dir, creating it with 0o700 if absent.

    Writable callers (CA gen, config write, mappings write, start_proxy)
    use this.  We force 0o700 — the dir holds the CA signing key, audit
    log, and pidfile, so traversal by other local users is undesirable.
    The chmod is unconditional so a pre-existing dir with a slack umask
    gets tightened on first access.
    Tparentsexist_ok  )r   mkdirchmodOSError)ds    rg   _proxy_state_dirr   y  sd     	AGGD4G(((	    		
 Hs   = 
A
	A
rw   c                 8    t          j                    dk    rdndS )NWindowsziron-proxy.exe
iron-proxy)platformsystemr(   ri   rg   _platform_binary_namer     s     '00I==<Ori   c                 H   t          j                    } t          j                                                    }| dk    r|dv rdnd}dt           d| dS | dk    r|dv rdnd}dt           d	| dS | d
k    rt          dt           d          t          d|  d|           )u   Map (uname, arch) → upstream release asset filename.

    iron-proxy ships ``iron-proxy_<version>_<os>_<arch>.tar.gz``.
    Windows builds aren't published upstream as of v0.39.0; we raise a
    clear error for callers on Windows.
    Linux)arm64aarch64r   amd64ziron-proxy__linux_z.tar.gzDarwin_darwin_r   z8iron-proxy does not ship native Windows binaries as of vz5. Run the proxy on a Linux/macOS host, or inside WSL.z2Unsupported platform for iron-proxy auto-install:  )r   r   machinelower_IRON_PROXY_VERSIONRuntimeError)r   r   archs      rg   _platform_asset_namer     s     _F  &&((G!%999wwwF0FFFFFF!%999wwwG0GG$GGGG#  
 
 	
 OVOOgOO  ri   Finstall_if_missingr   rQ   rS   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 )ui  Return a path to a usable ``iron-proxy`` binary, or None.

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

    When ``install_if_missing`` is True and neither resolves, calls
    :func:`install_iron_proxy` to download and verify the pinned version.
    r   z"iron-proxy auto-install failed: %sN)r   r   rd   osaccessX_OKshutilwhichr   install_iron_proxy	Exceptionloggerwarning)r   managedr   excs       rg   find_iron_proxyr     s     "7"9"99G~~ BIgrw77 \,''F F|| 	%''' 	 	 	NN?EEE44444	 4s   ;B	 	
B9B44B9)forcer   c           	     @   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          |	|          }
t          |          }|
                                |                                k    rt#          d| d|
 d	|           t%          j        |d
          5 }t)          |t                                }	 |                    ||d           n&# t,          $ r |                    ||           Y nw xY w||j        z  }ddd           n# 1 swxY w Y   t          j        t3          |          d          \  }}t5          j        |           t9          j        ||           t5          j        |t>          j         t>          j!        z  t>          j"        z  t>          j#        z  t>          j$        z  t>          j%        z  t>          j&        z             t5          j'        ||           ddd           n# 1 swxY w Y   tP          )                    t3          |          d           t                              dtT          |           |S )aU  Download, verify, and install the pinned ``iron-proxy`` 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 proxy install`` surface lets them
    propagate so the wizard can show a clear error.
    Tr   /zhermes-iron-proxy-prefixzDownloading %szChecksum mismatch for z: expected z, got zr:gzdata)filterNz.iron-proxy_)dirr   zInstalled iron-proxy %s at %s)+r   r   r   rd   r   _IRON_PROXY_RELEASE_BASE_IRON_PROXY_CHECKSUM_NAMEtempfileTemporaryDirectoryr   r   info_http_download_verify_checksums_signature_expected_sha256_sha256_filer   r   tarfileopen_pick_tar_memberextract	TypeErrornamemkstemprw   r   closer   copy2r   statS_IRUSRS_IWUSRS_IXUSRS_IRGRPS_IXGRPS_IROTHS_IXOTHreplacerN   popr   )r   bin_dirtarget
asset_name	asset_urlchecksum_urltmpdirtmparchive_pathchecksum_pathexpectedactualtfmember	extractedfdstageds                    rg   r   r     s    GMM$M...,...F}} u %''J+::j::I.LL1JLLL		$,@	A	A	A 5#V6llZ'77$i000y,///|]333 	$C777#M:>>l++>>v||~~--5 5 5$5 5,25 5  
 \,// 	*2%b*?*A*ABBF(

63v
6666 ( ( ( 

63'''''	(
 fk)I	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	*$ %#g,,~NNN
F
Y'''
L4<'$,6l!\*l!\*	
 	
 	
 	
66"""k5# 5# 5# 5# 5# 5# 5# 5# 5# 5# 5# 5# 5# 5# 5#v s6{{D)))
KK/1DfMMMMs\   C K
/G"F&%G"& G	G"G		G"K
"G&	&K
)G&	*CK

KKurldestNonec                   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timeoutwbzFailed to download : )urllibrequestRequesturlopen_DOWNLOAD_TIMEOUTr   r   copyfileobjerrorURLErrorr   )r   r   reqrespfr   s         rg   r   r   %  sd   
.
 
 |^.L
 
M
MCH^##C1B#CC 	,t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r   r   c                   t          j        d          }|st                              d           dS t           dt
           }t           dt           }| t
          z  }| t          z  }	 t          ||           t          ||           n3# t          $ r&}t                              d|           Y d}~dS d}~ww xY w| dz  }|	                    dd	
           |dt          |          ddg}	t          j        g |	dt          |          d	d          }
|
j        dk    r>t                              d|
j                            dd          dd                    dS t          j        g |	dt          |          t          |          d	d          }|j        dk    r3t          d|j                            dd          dd                    t                              d           d	S )u  Best-effort GPG verification of ``checksums.txt`` (maxpetrusenko P1).

    Downloads the detached signature (``checksums.txt.asc``) and the release
    signing key (``public-key.asc``), imports the key into an ephemeral
    keyring, and verifies the signature over ``checksum_path``.

    Returns True when the signature is verified. Returns False (with a warning)
    when verification is unavailable — ``gpg`` not installed, or the signature /
    public-key assets are missing from the release. Raises RuntimeError ONLY
    when verification actively FAILS (a present-but-bad signature), which is a
    tamper signal we must not ignore.

    Rationale for graceful degradation on "unavailable": the SHA-256 check
    against ``checksums.txt`` remains in force regardless, and many install
    hosts (CI, minimal containers) won't have gpg. We harden when we can and
    never make gpg a hard dependency for a working install.
    gpguu   gpg not found on PATH — skipping iron-proxy release-signature verification (SHA-256 checksum check still enforced).Fr   u{   iron-proxy release signature assets unavailable (%s) — skipping GPG verification (SHA-256 checksum check still enforced).Ngnupgr   T)moder   z	--homedirz--batchz--no-ttyz--import<   )capture_outputr   r   ub   Could not import iron-proxy signing key — skipping GPG verification (SHA-256 still enforced): %sutf-8r      z--verifyu~   iron-proxy checksums.txt failed GPG signature verification — refusing to install (possible release-channel tampering). gpg: i,  z0Verified iron-proxy checksums.txt GPG signature.)r   r   r   r   r   _IRON_PROXY_CHECKSUM_SIG_NAME_IRON_PROXY_PUBKEY_NAMEr   r   r   rw   
subprocessrun
returncodestderrdecoder   )r   r   r   sig_url
pubkey_urlsig_pathpubkey_pathr   
gnupg_homebase_cmdimpverifys               rg   r   r   /  s[   $ ,u

C D	
 	
 	
 u)KK,IKKG,HH/FHHJ22H//Kw)))z;////   HIL	
 	
 	
 uuuuu wJ%$///[#j//9jIH
.1(1J1K 0 01R  C ~8Jgy11$3$7	
 	
 	

 u^B(BJBHBs=/A/ABR  F AEM(()<<TcTBE E
 
 	

 KKBCCC4s   * B 
B;B66B;checksum_filer   c                0   |                      dd          }|                                D ]Q}|                                                                }t	          |          dk    r|d         |k    r
|d         c S Rt          d| d| j                   )	z?Parse the standard ``sha256sum`` output: ``<hex>  <filename>``.r  r   )encodingerrorsr   r   zNo checksum entry for z in )	read_text
splitlinesstripsplitlenr   r   )r  r   textlinepartss        rg   r   r   x  s     ""GI"FFD!!  

""$$u::??uRyJ668OOO
EEE1CEE  ri   pathc                    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)r   s   rg   <lambda>z_sha256_file.<locals>.<lambda>  s    !&&-- ri   ri   )hashlibsha256r   iterupdate	hexdigest)r!  hchunkr   s      @rg   r   r     s    A	dD		 Q////55 	 	EHHUOOOO	               ;;==s   ,AA"%A"r   tarfile.TarFilebinary_nametarfile.TarInfoc           	        g }|                                  D ]}|                                s|j                            d          sdt	          |j                  j        v rMt	          |j                  j        |k    r|                    |           |s:t          d| dd |                                  dd         D              d          |                    d	 
           |d         S )a  Find the binary inside the upstream tar.

    iron-proxy's archive is typically flat (binary at root) but we tolerate
    a top-level directory.  Members must be regular files with a leaf name
    matching ``binary_name``, no absolute paths, and no ``..`` traversal.
    r   z..zCould not find z% inside downloaded archive (members: c                    g | ]	}|j         
S r(   )r   .0ms     rg   
<listcomp>z$_pick_tar_member.<locals>.<listcomp>  s    >>>Q!&>>>ri   Nr   z...)c                *    t          | j                  S rc   )r  r   )r5  s    rg   r&  z"_pick_tar_member.<locals>.<lambda>  s    #af++ ri   )keyr   )	
getmembersisfiler   
startswithr   r   appendr   sort)r   r/  
candidatesr   s       rg   r   r     s    )+J--// & &}} 	;!!#&& 	$$v{2C2C2I*I*I![00f%%% 
Ek E E>>"--//"1"*=>>>E E E
 
 	
 OO--O...a=ri   binaryc           	        t          |           }t                              |          }||S 	 i }t          j        }t
          D ]}||v r||         ||<   t          j        t          |           dgddddt          |          }n# t          t          j
        f$ r Y dS w xY w|j        p|j        pd                                }|r
|t          |<   |S )a  Return ``iron-proxy --version`` output, stripped.  Empty on failure.

    Cached by binary path: ``get_status`` is called per Docker container
    create, but the version string is constant for a given binary.  A
    single subprocess invocation is plenty.
    Nz	--versionTr  r   )r  r  r  r  r   env )rw   rN   getr   environrB   r  r  _RUN_TIMEOUTr   TimeoutExpiredstdoutr
  r  )r?  r8  cachedminimal_envparentr   resouts           rg   iron_proxy_versionrM    s    f++C$$F ')3 	1 	1Dv~~$*4LD! n[[+&	 
 
 
 Z./   rr:))r
0
0
2
2C
  "!sJs   AB B B Tuple[Path, Path]c                   t                      }|dz  }|dz  }|                                r|                                r| s||fS t          j        d          t	          d          t          j        d          5 }t          |          }|dz  }|dz  }t          j	        ddd	t          |          d
gddd           t          j	        ddddddt          |          dddddddddd	t          |          gddd           |                                }|                                }	|                    |j        dz             }
t          j        t          j        z  t          j        z  }t%          t          d          r|t          j        z  }	 |
                                 n# t*          $ r Y nw xY wt          j        t          |
          |d          }	 t          j        |d          5 }|                    |           ddd           n# 1 swxY w Y   n5# t2          $ r( 	 t          j        |           n# t6          $ r Y nw xY w w xY wt          j        |
|           |                    |	           t          j        |t>          j         t>          j!        z  t>          j"        z  t>          j#        z             ddd           n# 1 swxY w Y   tH          %                    d|           ||fS ) u*  Generate (or return existing) iron-proxy CA cert + key.

    Uses the host's ``openssl`` binary.  We don't try to bind to a Python
    crypto library — openssl is universally available on the platforms we
    support, and it sidesteps cryptography-package licensing/distribution
    surface.
    ca.crtzca.keyopensslNzpopenssl not found on PATH. Install OpenSSL (apt: `openssl`, brew: `openssl`) to generate the iron-proxy CA cert.zhermes-proxy-ca-r   genrsaz-out4096Tr  )checkr  r   r   z-x509z-newz-nodesz-keyz-sha256z-days3650z-subjz/CN=hermes iron-proxy CAz-addextz!basicConstraints=critical,CA:TRUEzkeyUsage=critical,keyCertSignz.staged
O_NOFOLLOW  r   zGenerated iron-proxy CA at %s)&r   rd   r   r   r   r   r   r   r  r  rw   
read_byteswith_suffixsuffixr   O_WRONLYO_CREATO_TRUNChasattrrV  unlinkFileNotFoundErrorr   fdopenwriter   r   r   r   write_bytesr   r   r   r   r   r   r   r   )r   stateca_crtca_keyr   r   tmp_keytmp_crt	key_bytes	crt_bytes
key_staged
open_flagsr   r   s                 rg   ensure_ca_certrm    s    EXFXF}} 6==?? 5 v~|I&C
 
 	
 
	$,>	?	?	? ?T66ll..&#g,,?		
 	
 	
 	
 	5'68G7F3>:G 	
 	
 	
 	
& &&((	&&((	 ''	(ABB
[2:-
:
 2|$$ 	("-'J	  	 	 	D	WS__j%88	2t$$ #	"""# # # # # # # # # # # # # # # 	 	 	   	 	
:v&&& 	9%%%
4t|CdlRSSS?T ?T ?T ?T ?T ?T ?T ?T ?T ?T ?T ?T ?T ?T ?TB KK/8886>s   ?DKF,+K,
F96K8F99&K H#5HH#H	H#H	H#"K#
I.II
IIIIA3KKKhermes-proxyr   c                    |  dt          j        t          j        d                                                    dd          S )u  Mint a fresh opaque token to hand to the sandbox.

    The token has no internal structure beyond a recognizable prefix —
    iron-proxy matches on exact equality.  We use a 128-bit random suffix
    (32 hex chars from a SHA-256 of 32 bytes of os.urandom).  At that
    entropy the birthday-bound collision probability is below 2^-64 for
    up to 2^32 tokens, which is plenty for a proxy-scoped namespace.
    -    N)r'  r(  r   urandomr+  r   s    rg   mint_proxy_tokenrs  <  s?     HHw~bjnn55??AA#2#FHHHri   c                 $    t                      dz  S )Nmanagement.tokenr   r(   ri   rg   _management_token_pathrw  I  s     222ri   c           	        t                      }| sR|                                r>	 |                    d                                          }|r|S n# t          $ r Y nw xY wt          d          }t          j        t          |          t          j	        t          j
        z  t          j        z  t          t          dd          z  d          }	 t          j        |d           n# t          t          f$ r Y nw xY w	 t          j        ||                    d                     t          j        |           n# t          j        |           w xY w|S )a[  Return the management-API bearer key, minting it on first call.

    Stored at ``<hermes_home>/proxy/management.token`` with 0600 perms.
    The daemon receives it via the ``HERMES_IRON_PROXY_MGMT_KEY`` env var
    (named in the generated config's ``management.api_key_env``);
    ``hermes egress reload`` reads the same file to authenticate.
    r  r  zhermes-mgmtr   rV  r   rW  )rw  rd   r  r  r   rs  r   r   rw   r[  r\  r]  getattrfchmodAttributeErrorrb  encoder   )r   pexistingtokenr   s        rg   ensure_management_tokenr  M  sU    	  A QXXZZ 	{{G{44::<<H    	 	 	D	M222E	A
bj 2:-L!0L0LL
 
B

	"e^$   
U\\'**+++
Ls/   +A 
A A C( (C<;C< (D= =ErU   c                     t                      dz  } 	 |                     d                                          }n# t          $ r Y d S w xY w|pd S )Nru  r  ry  )r   r  r  r   )r~  r  s     rg   _read_management_tokenr  o  se     22AW--3355   tt=Ds   (< 
A
	A
rW   Optional[Tuple[str, int]]c                   | pt                      dz  }|                                sdS 	 ddl}n# t          $ r Y dS w xY w	 |                    |                    d                    }n# t          |j        f$ r Y dS w xY w|pi                     d          pi                     d          pd}t          |t                    rd	|vrdS |                    d	          \  }}}	 t          |          }n# t          $ r Y dS w xY w|pd
|fS )zBReturn ``(host, port)`` of the management listener, if configured.
proxy.yamlNr   r  ry  
managementlistenrB  :	127.0.0.1r   rd   yamlImportError	safe_loadr  r   	YAMLErrorrC  
isinstancerw   
rpartitionr\   
ValueError)	rW   cfgr  r   r  host_port_sports	            rg   #_read_management_listen_from_configr  x  s]   
 
?-//,>C::<< t   tt~~cmmWm==>>T^$   ttzr|,,277AAGRFfc"" c&7&7t'',,OD!V6{{   ttK&&s0   0 
>>)A, ,B B'C7 7
DDc            
        t                      } | rt          |           st          d          t                      }|t          d          t	                      }|st          d          ddl}ddl}|\  }}|j                            d| d| dd	d
d| id          }	 |j        	                    |t                    5 }|j        dk    r	 ddd           dS t          d|j                   # 1 swxY w Y   dS # |j        j        $ r}d}		 |                                                    dd          dd         }	n# t           $ r Y nw xY w|j        dk    rt          d|	           ||j        dk    rt          d          |t          d|j         d|	           |d}~w|j        j        t           f$ r}t          d| d| d| d          |d}~ww xY w) u  Hot-reload the running daemon's ruleset via the management API.

    POSTs to ``/v1/reload`` on the loopback management listener; the daemon
    re-reads proxy.yaml and atomically swaps the transform pipeline —
    validation failures leave the running config untouched (HTTP 422).

    Returns True on a successful reload.  Raises ``RuntimeError`` with an
    actionable message when the daemon isn't running, the config predates
    management-API support (no ``management`` block → restart required),
    or the reload is rejected.
    uL   iron-proxy is not running — nothing to reload.  Run `hermes egress start`.NzThe generated proxy.yaml has no management listener (written before reload support).  Re-run `hermes egress setup` and use `hermes egress restart` this one time.u[   management.token is missing — re-run `hermes egress setup`, then `hermes egress restart`.r   zhttp://r  z
/v1/reloadPOSTr'   zBearer ri   )methodr   r   r   r  Tz*management API returned unexpected status rB  r  r   r  i  i  zZiron-proxy rejected the new config (validation failed; the running ruleset is unchanged): i  u   management API rejected our key (401).  The running daemon was started with a different management.token — run `hermes egress restart`.zmanagement reload failed (HTTP z): z&could not reach the management API at z (zV).  If the daemon was started before reload support, run `hermes egress restart` once.)	_read_pid
_pid_aliver   r  r  urllib.errorurllib.requestr   r   r   _MGMT_RELOAD_TIMEOUTstatusr   	HTTPErrorr%  r  r   coder   )
rZ   mgmtr  r   r  r  r   r   r   bodys
             rg   reload_proxyr    s    ++C 
joo 
)
 
 	
 /00D|5
 
 	

 #$$E 
,
 
 	

 JD$
.
 
 )$))))) "3E"3"34	 !  C"^##C1E#FF 	${c!!	 	 	 	 	 	 	 	 JT[JJ  	 	 	 	 	 	 	 	 	 	 <!   	88::$$WY$??EDD 	 	 	D	8s??=6:= =   8s??/  	
 AchAA4AA
 
	 L!7+    ,T , ,D , ,C , , ,
 
 		ss   %!D C7D  C77C;;D >C;?D G$F,1EF,
EF,EAF,,G$GG$r]   r\   r_   c                    t          j                    dk    r8t                      }|r|dk    r| d|  gS t                              d           d|  gS )u  Build the single host:port bind the proxy should listen on.

    iron-proxy v0.39 supports exactly ONE ``proxy.http_listen`` bind per
    daemon process, so this returns a one-element list and the choice of
    host matters:

    * **Linux:** bind the docker bridge gateway (``172.17.0.1`` by
      default).  Sandboxes reach the proxy via
      ``host.docker.internal:host-gateway``, which Docker resolves to
      exactly this bridge gateway IP on Linux — a loopback-only bind is
      unreachable from inside containers there.  The bridge IP is still
      host-local (it's an address on the host's ``docker0`` interface),
      so host-side tooling and the status probe can reach it too.  When
      no docker bridge is detected (docker not installed / not started),
      fall back to loopback — there are no sandboxes to serve in that
      state, and the operator gets a warning.
    * **macOS / Windows Docker Desktop:** ``host.docker.internal``
      resolves via VPNkit to the host, so a loopback bind is reachable
      from containers and is the least-exposed choice.

    We never bind ``0.0.0.0`` — that would expose the proxy (and, with a
    leaked sandbox token, the user's API quota) to anyone on the local
    network.  The bridge-gateway bind is reachable by other containers
    on the default bridge network, which is unavoidable given v0.39's
    single-bind limit; requests still require a minted proxy token and
    an allowlisted upstream.
    r   r  r  u   No docker bridge (docker0) detected — binding iron-proxy to loopback only.  Docker sandboxes will NOT be able to reach the proxy until it is restarted with docker running.
127.0.0.1:)r   r   _detect_docker_bridge_ipr   r   )r]   	bridge_ips     rg   _default_http_listenr    sz    : G##,..	 	2k11 00;0011C	
 	
 	

 '&&''ri   c                    d} 	 t          j        g dddddd          }|j        dk    r|j                                        D ]q}|                                }t          |          D ]G\  }}|d	k    r<|d
z   t          |          k     r&||d
z                                d          d         }  nH|  nrn# t          t           j	        f$ r Y dS w xY w| sdS 	 t          j        |           }n# t          j        t          f$ r Y dS w xY w|j        s#|j        s|j        s|j        s|j        s|j        rt(                              d|            dS t-          |          S )u  Return the docker0 bridge IPv4, if present, else None.

    Best-effort: we try ``ip -4 addr show docker0`` first.  Anything that
    fails, doesn't parse as a strict IPv4, or parses as an address we
    must NOT bind to (unspecified, loopback, multicast, reserved, public)
    returns None — callers handle that as "no bridge bind".

    SECURITY: a hostile ``ip`` shim earlier on the operator's PATH used
    to be able to inject ``0.0.0.0`` here and re-open INADDR_ANY binding
    that the rest of the bind-policy work explicitly closed.  We
    validate via :mod:`ipaddress` and reject anything that isn't
    plausibly a docker bridge IP (private + non-special).
    N)ipz-4-oaddrshowdocker0Tr  r   r   r  r  r  r  r   r   inet   r   zORefusing suspicious docker bridge IP %s reported by `ip`; skipping bridge bind.)r  r  r	  rG  r  r  	enumerater  r   rF  	ipaddressIPv4AddressAddressValueErrorr  is_unspecifiedis_loopbackis_multicastis_reservedis_link_local	is_globalr   r   rw   )	candidaterK  r  r   itokr  s          rg   r  r    s     $In999dWYXY
 
 
 >Q
--//  

'..  FAsf}}QU););$)!a%L$6$6s$;$;A$>	(E )Z./   tt  t$Y//'4   tt 	  	
  > 	$%.	
 	
 	
 tt99s$   B2B7 7CCC. .DD)r]   	audit_logallowed_hostsupstream_deny_cidrshttp_listenmappingsList[TokenMapping]ca_certrf  r  r  Optional[List[str]]r  r  r   c           
        t          |pt                    }| D ]%}	|	j        D ]}
|
|vr|                    |
           &g }| D ]S}	t          |	j        pd          }|                    d|	j        d|	j        |ddddd |	j        D             d           T|t          t                    }nt          |          }|rt          |          nt          |          }|r|d
         nd| }|	                    dd          d
         pd}| d|dz    }ddi}|}ddd||ddd
d|dddid|t          z    t          dt          |          t          |          ddddd|iddd|idg|dS ) uR  Build the iron-proxy YAML config (as a dict) for a given mapping set.

    The dict is YAML-serializable via ``yaml.safe_dump``.  iron-proxy reads
    real secrets from its OWN environment via ``source: {type: env, var: ...}``;
    the sandbox never sees them.

    Bind policy: the sandbox-facing listeners (``tunnel_listen`` on
    ``tunnel_port``, plain-HTTP ``http_listen`` on ``tunnel_port + 1``)
    bind the docker bridge gateway on Linux (``172.17.0.1`` or whatever
    ``docker0`` resolves to — that's what ``host.docker.internal``
    resolves to inside containers there) and loopback on macOS / Windows
    Docker Desktop.  We do NOT bind ``0.0.0.0`` — a LAN peer with a
    leaked sandbox token could otherwise spend the operator's API quota
    against any allowlisted upstream.

    SSRF policy: ``upstream_deny_cidrs`` defaults to a conservative deny
    list covering loopback, link-local (incl. AWS/GCP/Azure IMDS at
    169.254.169.254), and RFC1918.  Pass an explicit ``[]`` to opt out of
    the deny list entirely (only sensible in hermetic tests).

    Schema mirrors the official iron-proxy schema as of v0.39.0.  Notable
    points:

    * The ``dns`` section is required by the binary even when we only use the
      CONNECT tunnel.  We point it at loopback so it doesn't conflict with
      anything else and disable the listener.
    * The ``proxy.tunnel_listen`` is what sandboxes hit via ``HTTPS_PROXY``.
      ``http_listen`` / ``https_listen`` are present (loopback only) so the
      proxy boots; sandboxes never route directly to them.
    * ``allowlist`` transform takes ``domains:`` and ``cidrs:``, not ``hosts:``.
    * ``secrets`` transform takes ``secrets:`` (plural), each with a
      ``source``, a ``replace.proxy_value`` (the sandbox-visible token), and
      a list of ``rules`` saying which hosts the swap should fire on.
    r{   rA  )typevarTF)proxy_valuer*   match_query
match_bodyrequirec                    g | ]}d |iS )r  r(   )r4  r,  s     rg   r6  z&build_proxy_config.<locals>.<listcomp>  s    <<<avqk<<<ri   )sourcer   rulesNr   r  r  r  r  levelr   z127.0.0.1:0)r  proxy_ipi   120s)tunnel_listenr  https_listenmax_request_body_bytesmax_response_body_bytes upstream_response_header_timeoutr  r  )r  api_key_envi     )r  rf  cert_cache_sizeleaf_cert_expiry_hours	allowlistdomains)r   configsecrets)dnsr   metricsr  tls
transformslog)rs   r   rz   r<  r*   ry   rx   r5   r  rsplit_MGMT_PORT_OFFSET_MGMT_API_KEY_ENVrw   )r  r  rf  r]   r  r  r  r  r)   r5  r,  secrets_rulesr*   
deny_cidrslistensprimary_listen	bind_hostplain_http_listen	log_blockr  s                       rg   build_proxy_configr  O  sm   \ MC-CDDE    ! 	  	 A~~Q	  M    Q_B0BCC$Q_== } "/  $#  3 6 =<1+;<<<;
 
 	 	 	 	H "677

-..
4 $/Ud;4H4U4UG#*JWQZZ0J[0J0JN%%c1--a0?KI$88{Q88'I 	A $#
 
 ,, *&6'(06 $.)
 
D m
 E;1B#BDD,
 

 7||&kk#&)	
 
 $$e, 
 "$m4 	
 SJ J Jri   
audit_pathc                   	 t           j        t           j        z  t           j        z  }t	          t           d          r|t           j        z  }t          j        t          |           |d          }	 t          j        |d           t          j	        |           dS # t          j	        |           w xY w# t          $ r}t          d|  d| d          |d}~ww xY w)uF  Create the audit log file with private permissions (0o600).

    Called from the wizard right before ``start_proxy``.  On the pinned
    v0.39 the daemon never writes this file (no ``log.audit_path``
    config field), so the pre-create is purely forward-compat: when the
    pin moves to a version that supports a dedicated audit stream, the
    file already exists with tight permissions and the daemon inherits
    them instead of creating it under the default umask.

    Raises :class:`RuntimeError` on any OSError (planted symlink,
    immutable parent dir, full disk) so the caller can decide how to
    surface it.  The wizard treats this as a WARNING on v0.39 — the
    file is non-load-bearing until the version bump — but the qualified
    message keeps operators from wiring monitoring to a path that can't
    exist.
    rV  rW  z2Refusing to start: could not pre-create audit log z with restrictive permissions (z;).  Move or chmod any existing file at that path and retry.N)r   r[  r\  O_APPENDr^  rV  r   rw   r{  r   r   r   )r  rl  r   r   s       rg   ensure_audit_logr  )  s    $[2:-;
2|$$ 	("-'JWS__j%88	 Ib%   HRLLLLLBHRLLLL   GG G:=G G G
 
 		s0   A-B4 0B B4 B11B4 4
C>CCr  c                   	 ddl }n"# t          $ r}t          d          |d}~ww xY wt                      }|dz  }|dz  }t	          |dd          5 }|                    | |d	d	
           ddd           n# 1 swxY w Y   t          j        |t          j	        t          j
        z             t          j        ||           |S )zSerialize the config dict to ``<hermes_home>/proxy/proxy.yaml``.

    Uses ``yaml.safe_dump`` so we never emit Python tags.
    r   NzGPyYAML is required to write the iron-proxy config but is not installed.r  z.proxy.yaml.tmpwr  ry  F)default_flow_style	sort_keys)r  r  r   r   r   	safe_dumpr   r   r   r   r   r   )r  r  r   rd  rL  tmp_pathr   s          rg   write_proxy_configr  O  s4      
 
 	 E
,
C((H	hg	.	.	. M!vqUeLLLM M M M M M M M M M M M M M M HXt|dl2333JxJs"    
&!&A99A= A=c                X   t                      }|dz  }dd | D             d}|dz  }t          |dd          5 }t          j        ||d	
           ddd           n# 1 swxY w Y   t	          j        |t          j        t          j        z             t	          j	        ||           |S )u  Persist the sandbox-visible proxy tokens to ``mappings.json``.

    The Docker backend reads this file to inject the right tokens as env
    vars when starting a sandbox.  The file is NOT read by iron-proxy
    itself — the mapping is already baked into ``proxy.yaml``.
    mappings.jsonr  c           	         g | ]J}|j         |j        t          |j                  t          |j                  t          |j                  d KS ))rx   env_namerz   r*   r|   )rx   ry   rs   rz   r*   r|   r3  s     rg   r6  z"write_mappings.<locals>.<listcomp>x  sd     	
 	
 	
   !}O"&q'7"8"8!%ao!6!6#'(9#:#: 	
 	
 	
ri   )versiontokensz.mappings.json.tmpr  r  ry  r   )indentN)
r   r   jsondumpr   r   r   r   r   r   )r  rd  rL  payloadr  r   s         rg   write_mappingsr
  l  s    E
/
!C	
 	
 	
 	
 	
 G ++H	hg	.	.	. (!	'1Q''''( ( ( ( ( ( ( ( ( ( ( ( ( ( (
 HXt|dl2333JxJs   AA"%A"c                    t                      } | dz  }|                                sg S 	 t          j        |                    d                    }n@# t
          t          j        f$ r'}t                              d|           g cY d}~S d}~ww xY wg }|	                    dg           D ]}	 |
                    t          |d         |d         t          |	                    d	          pd
          t          |	                    d          pd          t          |	                    d          pd
                               # t          t          f$ r Y w xY w|S )z;Read mappings.json, if it exists.  Empty list on any error.r  r  ry  z+Failed to read iron-proxy mappings.json: %sNr  rx   r  rz   r(   r*   r{   r|   rx   ry   rz   r*   r|   )r   rd   r  loadsr  r   JSONDecodeErrorr   r   rC  r<  rv   tupleKeyErrorr   )rd  r   r	  r   rL  items         rg   load_mappingsr    s    EA88:: 	*Q[['[::;;T)*   DcJJJ						 !CHb))  	JJ| /":.$TXX.>%?%?%E2FF $DHH_$=$=$SASTT %dhh/@&A&A&GR H H	 	 	 	 	 	 	 )$ 	 	 	H	Js0   (A B*BBB/BE		EE)available_env_namesr  c                B   | t          |           n(d t          j                                        D             g }t                                          D ]c\  }}|vr
|                    t          t          |                                	                    dd                    ||                     dt                                          D ]\  }}t          |                    d          pd          }|vrt          fd	|D                       sI|                    t          t          |                                	                    dd                    |t          |d
                   t          |d                   |                     |S )a1  Mint a TokenMapping for every known provider whose env var is set.

    Pass ``available_env_names`` to override the lookup source (used by the
    Bitwarden adapter so we mint mappings for keys that *will* be in the
    proxy's environment even if they aren't in the host process env right
    now).
    Nc                    h | ]	\  }}||
S r(   r(   r4  kvs      rg   	<setcomp>z-discover_provider_mappings.<locals>.<setcomp>  !    777tq!Q7777ri   _api_keyrB  r   )rx   ry   rz   r+   r(   c              3      K   | ]}|v V  	d S rc   r(   )r4  anamess     rg   	<genexpr>z-discover_provider_mappings.<locals>.<genexpr>  s'      ,I,IAQ%Z,I,I,I,I,I,Iri   r)   r*   r  )setr   rD  itemsr&   r<  rv   rs  r   r   r0   r  rC  any)r  r  r  r)   specr+   r  s         @rg   discover_provider_mappingsr$    s    &'((77rz//11777#%H,2244  %5  (0@0@0H0HUW0X0XYYY" 
 
 
 	 	 	 	
 16688  $++1r22 5  ,I,I,I,I,I,I,I)I)I (0@0@0H0HUW0X0XYYY" g//_ 566#
 
 
 	 	 	 	 Ori   c                    | t          |           n(d t          j                                        D             fdt          D             S )u3  Return env-var names for providers we recognize but can't proxy.

    AWS Bedrock (SigV4) and GCP Vertex (SDK-minted OAuth) can't be swapped
    by a static header replacement.  When any of these are configured, the
    sandbox is holding real credentials that the proxy can't strip — the
    isolation guarantee is incomplete for those providers.

    The wizard and ``hermes egress status`` use this to print a warning.
    (Anthropic / Azure OpenAI / Gemini used to be here; they're now
    first-class swapped providers via ``_HEADER_AUTH_PROVIDERS``.)
    Nc                    h | ]	\  }}||
S r(   r(   r  s      rg   r  z/discover_uncovered_providers.<locals>.<setcomp>  r  ri   c                    g | ]}|v |	S r(   r(   )r4  nr  s     rg   r6  z0discover_uncovered_providers.<locals>.<listcomp>  s    ;;;!U

A


ri   )r   r   rD  r!  r4   )r  r  s    @rg   discover_uncovered_providersr)    sW      &'((77rz//11777;;;;,;;;;ri   )rotater  
discoveredr*  c           
        d | D             }g }|D ]v}|                     |j                  }|C|sA|                    t          |j        |j        |j        |j        |j                             a|                    |           w|S )u  Combine an existing mapping set with freshly discovered providers.

    By default this PRESERVES tokens for providers already in ``existing`` —
    re-running ``hermes egress setup`` should not invalidate the tokens
    baked into containers that are already running.  Only newly added
    providers get freshly minted tokens.

    When ``rotate=True``, every token in the result is freshly minted
    regardless of overlap.  The wizard exposes this via ``--rotate-tokens``
    for the rare case where the operator wants to roll all tokens
    deliberately (e.g. after a suspected token leak).

    Providers that are in ``existing`` but no longer in ``discovered``
    (operator removed the env var since last setup) are dropped.
    c                    i | ]
}|j         |S r(   )ry   r3  s     rg   
<dictcomp>z"merge_mappings.<locals>.<dictcomp>	  s    444aq444ri   Nr  )rC  ry   r<  rv   rx   rz   r*   r|   )r  r+  r*  by_namerL  r   priors          rg   merge_mappingsr1    s    , 548444G C  AO,,V JJ|!-#1 /o ! 1       JJqMMMMJri   c                 $    t                      dz  S )Niron-proxy.pidrv  r(   ri   rg   _pidfiler4  "  s     000ri   rY   c                    t                      dz  } |                                 sd S 	 t          |                     d                                                    }n# t
          t          f$ r Y d S w xY w|dk    r|nd S )Nr3  r  ry  r   )r   rd   r\   r  r  r   r  )pfrZ   s     rg   r  r  &  s     
		!1	1B99;; t",,,00668899Z    tt''33t#s   5A A43A4HERMES_IRON_PROXY_NONCE_proxy_noncerZ   c                (   	 t          d|  d                              d          }n# t          $ r Y dS w xY w|                    d          }|dk     rdS ||dz   d                                         }t          |          d	k    rdS |d	         S )
zReturn /proc/<pid>/stat[21] (starttime) on Linux, else None.

    Comparing starttime is the standard cheap way to detect PID recycling
    without relying on cmdline scanning.  When None, callers fall back to
    the cmdline + nonce check.
    /proc/z/statr  ry  N)r   r     )r   r  r   rfindr  r  )rZ   r  rparenfieldss       rg   _pid_proc_starttimer@  <  s    'S'''((22G2DD   tt
 ZZ__Fzzt&1*++$$&&F 6{{bt":s   '* 
88c                 $    t                      dz  S )a7  Path to the on-disk sibling of the pidfile that stores the nonce.

    Written by ``_write_pidfile_safely`` after ``start_proxy`` plants
    the nonce in the iron-proxy child env, read by ``_pid_alive`` in a
    later CLI invocation (``stop`` / ``status``) so cross-process
    PID-recycling defense holds.
    ziron-proxy.nonce)r   r(   ri   rg   _persisted_nonce_pathrB  U  s       #555ri   c                    t                      } 	 t          j        }t          t          d          r|t          j        z  }t          j        t          |           |          }n# t          $ r Y dS w xY w	 	 t          j        |          }t          t          d          r3|j	        t          j
                    k    r	 t          j        |           dS n# t          $ r Y nw xY wt          j        |d                              dd                                          }|pd	 t          j        |           S # t          j        |           w xY w)u   Read the on-disk nonce written next to the pidfile.

    Returns None when the file is missing, unreadable, or empty —
    callers fall back to argv0 basename matching in that case.
    rV  Ngetuid   r  ignorer  )rB  r   O_RDONLYr^  rV  r   rw   r   fstatst_uidrD  r   r|  r%  r  r  )r~  flagsr   str   s        rg   _read_persisted_noncerL  `  sE    	A

 2|$$ 	#R]"EWSVVU##   tt	"Br8$$ bikk)A)A 	  	 	 	D	wr3&&wx&@@FFHH|t
s>   AA# #
A10A16AC D8 
C D8 C  AD8 8Ec                   | dk    rdS 	 ddl }|                    |           sdS n^# t          $ rQ t          j                    dk    rn6	 t          j        | d           n# t          t          t          f$ r Y Y dS w xY wY nw xY wg }t          r|                    t                     t                      }|r||vr|                    |           |ra	 t          d|  d                                          }|D ]'}t           d|                                 }||v r dS (n# t          $ r Y nw xY w	 t          d|  d	          }|                                r~|                                                    d
          }|rS|d                             dd          }	t
          j                            |	          }
|
                    d          rdS dS n# t          $ r Y nw xY w	 t/          j        ddt3          |           ddgddddd          }|j        dk    rM|j        pd                                }t
          j                            |                              d          S n# t          t.          j        f$ r Y nw xY wdS )a#  Return True iff ``pid`` is alive AND is an iron-proxy process.

    Defends against PID reuse via three signals (in priority order):
    1. ``/proc/<pid>/environ`` contains our nonce  (most reliable, Linux)
    2. ``/proc/<pid>/cmdline`` basename matches the managed binary
    3. ``ps -p <pid>`` command line contains the binary path

    The legacy ``"iron-proxy" in cmdline`` match was loose enough to match
    ``tail iron-proxy.log`` or an editor with that file open.  We tighten
    on argv[0] basename plus an in-process nonce instead.
    r   FNr   r:  z/environ=Tz/cmdline    r  rF  r  r   psz-pr  zcomm=r   r   r  rB  )psutil
pid_existsr  r   r   r   killProcessLookupErrorPermissionErrorr   r8  r<  rL  r   rX  _HERMES_IRON_PROXY_NONCE_ENVr}  rd   r  r  r!  basenamer;  r  r  rw   r	  rG  r  rF  )rZ   rQ  nonce_candidateson_disk	env_bytesnonceneedlecmdline_pathr  argv0
argv0_baserK  comms                rg   r  r    s9    axxu 	  %% 	5	 
 
 
?	)) Q&A   uuu
, #% .---#%%G )7"222((( 	3c33344??AAI)    8BB5BBIIKKY&&44 '   	 	 	D	
2S22233   	!,,..44W==F  q	(((BBW--e44
((66  45	    	n4S41dWYXY
 
 
 >QJ$"++--D7##D))44\BBB  Z./   
 4sr   & "B	ABA;6B:A;;B BAD# !D# #
D0/D04B!G 
G'&G'+BI0 0J	J	T)r?  rW   	extra_envr   refresh_secrets_from_bitwardenbitwarden_configra  Optional[Dict[str, str]]rb  rc  Optional[Dict]c           
     &   t                      }|rt          |          rt                      S | pt          |          }|t	          d          |pt                      dz  }|                                st	          d| d          t          |||          }	t          |          t                      |	t          <   t          j        t          j        d                                                    at           |	t"          <   t                      d	z  }
t          j        t          j        z  t          j        z  }t+          t          d
          r|t          j        z  }	 t          j        t1          |
          |d          }n)# t2          $ r}t	          d|
 d| d          |d}~ww xY w	 t          j        |d           n# t2          $ r Y nw xY w	 t          j        |          }t+          t          d          rK|j        t          j                    k    r/t          j        |           t	          d|
 d|j         d          n# t>          $ r Y nw xY w	 tA          |	tB          j"        |tB          j#                  }tI          j%                    dk    rd|d<   tC          j&        t1          |          dt1          |          gfi |n9# t2          $ r,}t          j        |           t	          d|           |d}~ww xY w	 	 t          j        |           n:# t2          $ r Y n.w xY w# 	 t          j        |           w # t2          $ r Y w w xY wxY wtO                      	 tQ          j)                   n # t          $ r tU          d            w xY wtW                      }||\  }}n	dtX          }}d}fd}d}d}tI          j%                    dk    o%t[          j.                    t[          j/                    u }|r>ta          j0        t`          j1        |          }ta          j0        t`          j2        |          }	 tg          j3                    th          z   }	 5                                Qtm          |
d          }	 7                                 n# tp          $ r Y nw xY wt	          d j9         d!|           tu          ||          rd}n-tg          j3                    |k    rntg          j;        d"           |r>ta          j0        t`          j1        |           ta          j0        t`          j2        |           nF# |r?ta          j0        t`          j1        |           ta          j0        t`          j2        |           w w xY w5                                Qtm          |
d          }	 7                                 n# tp          $ r Y nw xY wt	          d j9         d!|           |shtm          |
d          }tU          d           	 7                                 n# tp          $ r Y nw xY wt	          d#| d$| d%th           d&|           tx          =                    d'j)        |           t                      S )(uL  Spawn iron-proxy as a managed background subprocess.

    Idempotent — if the proxy is already running with the expected PID,
    just returns the live status.

    ``refresh_secrets_from_bitwarden=True`` re-fetches upstream secrets
    via ``bws secret list`` at startup and injects them into the child
    env.  This delivers the rotation promise that distinguishes
    ``credential_source: bitwarden`` from ``credential_source: env``.
    Without this flag (or with ``bitwarden_config=None``) the proxy still
    starts but uses whatever the host process env happens to contain.
    r   Nu@   iron-proxy binary not available — run `hermes egress install`.r  ziron-proxy config not found at z". Run `hermes egress setup` first.ra  refresh_from_bitwardenrc     ziron-proxy.logrV  rW  z!Refusing to write iron-proxy log r   '.  Remove that path manually and retry.rD  ziron-proxy log  has unexpected owner uid=z; refusing to write.)rA  stdinrG  r
  r   Tstart_new_sessionz-configzfailed to spawn iron-proxy: r   grace_secondsr  Fc                    t          d           	                                  n# t          $ r Y nw xY wt                      )Nr   rn  )_kill_and_waitr_  r`  KeyboardInterrupt)_signum_framepidfileprocs     rg   _interrupt_handlerz'start_proxy.<locals>._interrupt_handler  s\     	t1----	NN  	 	 	D	!!!s   ) 
66   linesz$iron-proxy exited immediately (code z). Last log lines:
皙?ziron-proxy did not bind r  z within z)s.  Process was killed.  Last log lines:
z#Started iron-proxy pid=%s config=%s)>r  r  
get_statusr   r   r   rd   _build_proxy_subprocess_envr  r  r  r'  r(  r   rr  r+  r8  rV  r[  r\  r  r^  rV  r   rw   r   r{  rH  rI  rD  r   r|  dictr  DEVNULLSTDOUTr   r   Popenr4  _write_pidfile_safelyrZ   rq  _read_http_listen_from_configrr   	threadingcurrent_threadmain_threadsignalSIGINTSIGTERMtime_STARTUP_GRACE_SECONDSpoll	_tail_logr_  r`  r	  _port_listeningsleepr   r   )r?  rW   ra  r   rb  rc  r  bin_pathr  rA  log_pathlog_open_flagslog_fdr   rK  popen_kwargs	listen_hp
probe_hostr]   r[   rw  prev_sigintprev_sigterminstall_handlersdeadlinetailru  rv  s                             @@rg   start_proxyr    s   0 {{H Jx(( ||O<NOOOHN
 
 	
 
<*,,|;C::<< 
/c / / /
 
 	
 &=)  C +3//;!8!:!: >"*R..11;;==L(4C$%!!$44H [2:-;Nr<   ("-'X>>   3 3 3C 3 3 3
 
 	
	&%       	Xf2x   	RY")++%=%=HV7( 7 7y7 7 7       "$$	
 
 
 ?	))04L,-]]Is3xx0
 

 
  J J J
?#??@@cIJ	 		HV 	 	 	D		HV 	 	 	D	 jjGgtx0000   t1----" .//I"+
KK"-/CK
I" " " " " " KLY& 	B$&&)*?*A*AA   ImFM3EFF}V^5GHH89;;!77	yy{{& 444NN$$$$(   D"/4? / /(,/ /   z;77  	y{{h&&JsOOO!	$  	8M&-555M&.,777  	8M&-555M&.,7777	8
 yy{{,,,	NN  	 	 	D	'4? ' ' $' '
 
 	
  
,,,t1----	NN  	 	 	D	'z ' 'K ' '%' ' $' '
 
 	
 KK5txEEE<<s  '#F 
F1F,,F15G 
GGA4I 
II"A2K L5 
L'LLL5 L% %
L21L25M7MM
MMMM.N N!AU R$ #U $
R1.U 0R11AU AV;W 
WWX4 4
Y Yru  c                J   t           j        t           j        z  t           j        z  }t	          t           d          r|t           j        z  }	 t          j        t          |           |d          }n# t          $ r t                      }|r%t          |          rt          d|  d| d          	 |                                  n# t          $ r Y nw xY wt          j        t          |           |d          }Y n(t          $ r}t          d|  d| d          |d	}~ww xY w	 	 t          j        |          }t	          t           d
          r6|j        t          j                    k    rt          d|  d|j                   n# t$          $ r Y nw xY wt          j        |t          |                              d                     t          j        |           n# t          j        |           w xY wt,          r$|                     d          }d}	 t           j        t           j        z  t           j        z  }	t	          t           d          r|	t           j        z  }	t          j        t          |          |	d          }t          j        |t,                              d                     n# t          $ r Y nw xY w|dk    r(	 t          j        |           d	S # t          $ r Y d	S w xY wd	S # |dk    r&	 t          j        |           w # t          $ r Y w w xY ww xY wd	S )u  Write ``pid`` to ``pidfile`` with O_EXCL + O_NOFOLLOW + ownership check.

    O_EXCL means "another start is in progress" if the file already
    exists with a live owner — we cleanly fail rather than racing.  When
    the existing pidfile points at a dead pid (stale crash), we
    explicitly unlink it before retrying once.

    Side effect: also persists the in-process nonce to disk so
    cross-CLI-invocation ``_pid_alive`` checks (start in one process,
    stop in another) can still defeat PID recycling.
    rV  rW  z<Another iron-proxy start appears to be in progress (pidfile z -> pid z4).  Run `hermes egress stop` if that proxy is stuck.zRefusing to write pidfile r   rj  NrD  zpidfile rk  r  z.noncer  r   )r   r[  r\  O_EXCLr^  rV  r   rw   FileExistsErrorr  r  r   r_  r`  r   rH  rI  rD  r|  rb  r}  r   r8  rY  r]  )
ru  rZ   rl  r   existing_pidr   rK  	noncefilenfdnopens
             rg   r  r    s    rz)BI5Jr<   $bm#
WS\\:u55 6 6 6 !{{ 	J|44 	D#D D-9D D D  	NN  	 	 	D	WS\\:u55   3 3 3C 3 3 3
 
 		"Br8$$ bikk)A)A"MwMM")MM    	 	 	D	
SXX__W--...
  ''11		K"*,rz9Er<(( '&'#i..%77CHS,--g667777 	 	 	 D	
 axxHSMMMMM   DD xsaxxHSMMMM   D  s   #A0 0?D0CD
CDC&D:	DDD$AF G 
FG F8G G4BJ2 1K2 2
J?<K2 >J??K2 	K 
K-,K-2L :LL 
LL LL rn  rv  'subprocess.Popen'ro  c               V   	 |                                   n# t          $ r Y dS w xY w	 |                     |           dS # t          j        $ rX 	 |                                  n# t          $ r Y nw xY w	 |                     |           Y dS # t          j        $ r Y Y dS w xY ww xY w)u<   Best-effort SIGTERM → wait → SIGKILL for a child we own.Nr   )	terminater   waitr  rF  rS  )rv  ro  s     rg   rq  rq  &  s       
		-	((((($   	IIKKKK 	 	 	D		IImI,,,,,,( 	 	 	DDD	sY    
%%A B(A&%B(&
A30B(2A33B(7BB$B(#B$$B(rg  rh  c                   i }t           j        }t          D ]}||v r||         ||<   i }t                      }t	                      D ]?}|                    |j                   |j        rt          |j                  ||j        <   @|D ]M}||v r||         ||<   |	                    |d          D ]$}	|	                    |	          r||	         ||<    n%N|r|r	 ddl
m}
 |	                    dd          }|	                    |d                                          }|	                    dd          }|r|r|
                    ||dd	          \  }}t          |t          |          z
            }|D ]}||v r||         ||<   |rE|pi 	                    d
          st          d| d          t                               d|           |r(t                               dt%          |                     n@|pi 	                    d
          st          d          t                               d           nZ# t&          f$ rL}|pi 	                    d
          st          d          |t                               d|           Y d}~nd}~ww xY w| r|                    |            t*          D ]}|                    |d           |                    dd           |S )u  Construct the minimal env for the iron-proxy subprocess.

    Allowlists infrastructure vars (PATH, HOME, locale) plus the env vars
    named in ``load_mappings()`` (the real upstream secrets the proxy
    needs to do the swap).  Everything else is stripped — see
    ``_PROXY_SUBPROCESS_ENV_STRIP`` for proxy chain protection.

    When ``refresh_from_bitwarden=True`` AND ``bitwarden_config`` is
    populated, fetches upstream secrets via the BSM SDK at startup and
    merges them in.  This is what delivers the rotation guarantee
    promised by ``credential_source: bitwarden`` — without it, rotating
    a key in the Bitwarden web app doesn't reach the proxy.
    r(   r   )	bitwardenaccess_token_envBWS_ACCESS_TOKENrB  
project_idF)access_tokenr  cache_ttl_seconds	use_cacheallow_env_fallbackz-Bitwarden refresh did not return secrets for z.  Either add the secrets to your BWS project, switch to credential_source: env via `hermes egress setup --no-bitwarden`, or set `proxy.allow_env_fallback: true` in config.yaml to opt into the legacy host-env fallback.uw   Bitwarden refresh did not return secrets for %s — falling back to host env for those names (allow_env_fallback=true).z[Bitwarden refresh produced %d warning(s); run `hermes secrets bitwarden status` for detail.zcredential_source=bitwarden but the access-token env or project_id is empty.  Either set both, switch to credential_source: env, or set `proxy.allow_env_fallback: true` to opt into the legacy fallback behaviour.u   credential_source=bitwarden but access-token env or project_id is empty — proxy will fall back to parent env (allow_env_fallback=true).a  Bitwarden refresh module unavailable at proxy start (credential_source=bitwarden with proxy.allow_env_fallback: false).  Either fix the import, switch to credential_source: env, or set `proxy.allow_env_fallback: true` to opt into the legacy fallback behaviour.zmBitwarden refresh module unavailable at proxy start, falling back to parent env (allow_env_fallback=true): %sNr=   1)r   rD  rB   r   r  addry   r|   r  rC  agent.secret_sourcesr  r  fetch_bitwarden_secretssortedr   r   r   r  r  r*  rK   r   
setdefault)ra  rh  rc  rA  rJ  r   alias_sourcesneededr5  aliasbwaccess_token_namer  r  r  r`   missingr(  r   s                      rg   r}  r}  9  s   ( CZF/ % %6>>tCI 13MUUF__ F F

1?### 	F-213D-E-EM!/*  6>>tCII&**444  ::e$$  &uCIE  a"2 a`	<<<<<< 0 4 4"$6! ! "::&7<<BBDDL)--lB??J D
 D$&$>$>!-)&'#	 %? % %! !#g,,!677 , ,AG||!(A  -2778LMM *I&I I I   NN5  	    NNLH   ).B334HII &9   1  
 ~ 	 	 	 %*//0DEE "1   NNK       !	2  

9 ,  dNN:s###Js   EH= =JAJJc                    dd} t                      }|rt          |          s |              dadS t          |          }	 t	          j        |t          j                   n# t          $ r  |              daY dS w xY wt          j	                    dz   }t          j	                    |k     r;t          |          snt          j
        d           t          j	                    |k     ;t          |          }|duo	|duo||k    pt          |           }|rt                              d|           n,	 t	          j        |t                     n# t          $ r Y nw xY w |              dat                              d	|           d
S )z=Stop the managed iron-proxy.  Returns True if it was running.ra   r   c                     t                                          d           	 t                                                       dS # t          $ r Y dS t          $ r Y dS w xY w)z1Best-effort cleanup of pidfile + persisted nonce.T)
missing_okN)r4  r_  rB  r`  r   r(   ri   rg   _cleanup_state_filesz(stop_proxy.<locals>._cleanup_state_files  s{    

T***	!##**,,,,,  	 	 	DD 	 	 	DD	s    A 
A 	A A NFg      @r{  z?iron-proxy pid=%s appears recycled before SIGKILL; not killing.zStopped iron-proxy pid=%sTra   r   )r  r  r8  r@  r   rS  r  r  rT  r  r  r   r   _KILL_SIGNALr   )r  rZ   starttime_beforer  starttime_afterrecycleds         rg   
stop_proxyr    s   
    ++C joo u
 +3//
V^$$$$   uu y{{S H
)++
 
 # 	
3 )++
 
  .c22D( 4t+4 O3! C 	 	
  		NN #   
\****%    L
KK+S1114s$   A" "A<;A<3E 
EEc                    t                      } t                      }||\  }| _        nd}t          | _        t	          d          }|r|| _        t          |          | _        t                      }|dz  }|dz  }|	                                r|| _
        |	                                r|| _        t                      }|r0t          |          r!|| _        t          || j                  | _        | S )u3  Snapshot the current proxy state — does NOT start anything.

    Crucially, this is called per Docker-container-create when egress
    enforcement is on.  It must not have side-effects (no mkdir, no
    binary version subprocess that takes 30s on a hung binary).  The
    state dir is read-only here.
    Nr  Fr   r  rP  )rP   r  r]   rr   r   rT   rM  rV   r   rd   rW   rX   r  r  rZ   r  r[   )r  r  r  r?  rd  r  carZ   s           rg   r|  r|  	  s     ]]F-//I)2&
F&& 
1666F ;# !36 : :!!E
,
C		B
zz|| ! 	yy{{ ! 
++C
 Kz# K
 +:v7IJJMri   c                 6    t                      } | d S | d         S )Nr  )r  )r  s    rg   _read_tunnel_port_from_configr  K	  s!    *,,F~t!9ri   c                 F   t                      dz  } |                                 sdS 	 ddl}n# t          $ r Y dS w xY w	 |                    |                     d                    }n# t          |j        f$ r Y dS w xY w|pi                     d          pi }|                    d          p|                    d          pd	}t          |t                    rd
|vrdS |                    d
          \  }}}	 t          |          }n# t          $ r Y dS w xY w|pd|fS )u  Return ``(host, port)`` of the configured sandbox-facing listener.

    Reads ``proxy.tunnel_listen`` — the CONNECT/MITM listener sandboxes
    hit via ``HTTPS_PROXY`` — falling back to ``proxy.http_listen`` for
    configs written before the tunnel/http listener-role split.

    The bind host matters for liveness probes: on Linux the daemon binds
    the docker bridge gateway (e.g. ``172.17.0.1``), where a loopback
    connect would report "not listening" for a perfectly healthy daemon.
    r  Nr   r  ry  r   r  r  rB  r  r  r  )	r  r  r   proxy_blockr  r  r  r  r  s	            rg   r  r  R	  sn    

,
.C::<< t   tt~~cmmWm==>>T^$   tt:2""7++1rK
 ___--U1O1OUSUFfc"" c&7&7t'',,OD!V6{{   ttK&&s0   . 
<< )A* *A?>A?<D 
DDr  r  c                    ddl }	 |                    | |fd          5  	 ddd           dS # 1 swxY w Y   dS # t          $ r Y dS w xY w)uD   Cheap TCP connect probe — True iff something accepts on host:port.r   Ng      ?r   TF)socketcreate_connectionr   )r  r  r  s      rg   r  r  y	  s     MMM%%tTlC%@@ 	 		 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	   uus'   ; .; 2; 2; 
A	A	rx  ry  rz  c               ,   |                                  sdS 	 |                                 dd          }d                    |                    dd                                          | d                    S # t
          $ r}d| dcY d }~S d }~ww xY w)	Nz(no log file)i 
r  r   r  z(could not read log: r;  )rd   rX  joinr  r  r   )r!  rz  r   r   s       rg   r  r  	  s    ;;== .  (yyWY??JJLLeVWWUVVV . . .-s---------.s   A A9 9
BBBBc                 <    t                                            dadS )u  Clear module-level caches so tests get a fresh start.

    This module owns two mutable globals that need reset between tests:
    - ``_VERSION_CACHE`` — subprocess output cache keyed by binary path.
    - ``_proxy_nonce`` — the strong-proof token written by ``start_proxy``
      and read by ``_pid_alive`` to defeat PID recycling.

    Today the repo's tests run each file in its own subprocess (per
    AGENTS.md) so leakage is bounded, but any in-process caller
    (notebooks, ad-hoc scripts, ``pytest -p no:xdist``) would otherwise
    see whichever values were probed first regardless of subsequent
    ``install_iron_proxy(force=True)`` or ``start_proxy`` calls.
    N)rN   clearr8  r(   ri   rg   _reset_for_testsr  	  s      LLLri   )rP   rv   r  r$  r)  r  rm  r  r   r|  r   rM  r  r1  rs  r  r  r  r
  r  )ra   r   )ra   rw   )r   rQ   ra   rS   )r   rQ   ra   r   )r   rw   r   r   ra   r   )r   r   r   r   ra   rQ   )r  r   r   rw   ra   rw   )r!  r   ra   rw   )r   r.  r/  rw   ra   r0  )r?  r   ra   rw   )r   rQ   ra   rN  )rn  )r   rw   ra   rw   )r   rQ   ra   rw   )ra   rU   rc   )rW   rS   ra   r  rl   )r]   r\   ra   r_   )r  r  r  r   rf  r   r]   r\   r  rS   r  r  r  r  r  r  ra   r   )r  r   ra   r   )r  r   ra   r   )r  r  ra   r   )ra   r  )r  r  ra   r  )r  r  ra   r_   )r  r  r+  r  r*  rQ   ra   r  )ra   rY   )rZ   r\   ra   rU   )rZ   r\   ra   rQ   )r?  rS   rW   rS   ra  rd  r   rQ   rb  rQ   rc  re  ra   rP   )ru  r   rZ   r\   ra   r   )rv  r  ro  r\   ra   r   )ra  rd  rh  rQ   rc  re  ra   rM   )ra   rP   )ra   r  )r  rw   r  r\   ra   rQ   )r!  r   rz  r\   ra   rw   r  )mrp   
__future__r   r'  r  r  loggingr   r   r   r  r   r  r   r   r  r  r  r   r  dataclassesr   r   pathlibr   typingr   r   r	   r
   	getLoggerrm   r   r   r   r   r  r  r   rE  r  r  r  r  rr   r   rq   r&   r0   r4   r5   rB   rK   rz  r  r  rN   rP   rv   r   r   r   r   r   r   r   r   r   r   r   r   rM  rm  rs  rw  r  r  r  r  r  r  r  r  r  r
  r  r$  r)  r1  r4  r  rV  r8  r@  rB  rL  r  r  r  rq  r}  r  r|  r  r  r  r  r  __all__r(   ri   rg   <module>r     sc  7 7 7r # " " " " "        				                        ( ( ( ( ( ( ( (       . . . . . . . . . . . .		8	$	$   U?RTT  , 
 !4 *     1    
  +     " ?)%--* 3	1 	1  	 	 	 	< (7 

 6  8,& +A A     T*     1     44     &0      wvy&.99 "$ # # # # 
 
 
 
 
 
 
 
6 * * * * * * * *>% % % %	' 	' 	' 	'   *P P P P   D 38      8 ). R R R R R RjH H H HF F F FR
 
 
 
      2, , , ,h %* Z Z Z Z Z ZD
I 
I 
I 
I 
I3 3 3 3 .3      D    #'' ' ' ' '6M M M M`&( &( &( &(R@ @ @ @P , $)-/3'+W W W W W Wt# # # #L   :   D   @ 04, , , , , ,b 04< < < < < <8 	' ' ' ' ' '^1 1 1 1$ $ $ $$  9 " " " " "   26 6 6 6   B[ [ [ [@ ""&*.#+0'+q q q q q qhP P P Pf FG      * +/#('+	` ` ` ` ` `F@ @ @ @F) ) ) )X   $' $' $' $'N	 	 	 	 +- . . . . . .   *  ri   