
    g5j!                       U d Z ddlm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 daded	<   d)dZd*dZ edd          Zded<    G d de          Zd+dZd,dZd-dZ eh d          ZdZd.dZd/d0d"Zd1d&Zd2d(ZdS )3u  Profile-scoped credential resolution for multi-profile gateway multiplexing.

The multiplexing gateway serves many profiles from one process. Each profile
has its own ``.env`` with its own provider keys and platform tokens, so we
**cannot** union them into the process-global ``os.environ`` (that would leak
profile A's keys to profile B's turns, and to every subprocess spawned with
``env=dict(os.environ)``).

This module provides a fail-closed, context-local secret scope:

- ``set_secret_scope(mapping)`` installs the active profile's secrets for the
  current task (a contextvar, so it propagates into the agent's worker thread
  via ``copy_context()`` exactly like the HERMES_HOME override).
- ``get_secret(name)`` reads from that scope. When multiplexing is **active**
  and no scope is set, it RAISES rather than silently falling back to
  ``os.environ`` — an un-migrated or newly-added call site fails loud at that
  exact line instead of leaking another profile's value. When multiplexing is
  **off** (the default), it transparently reads ``os.environ`` so the
  single-profile gateway and every non-gateway caller behave exactly as before.

Design rationale lives in ``docs/design/multiplexing-gateway.md`` (Workstream A).
    )annotationsN)
ContextVarToken)Path)DictMappingOptionalFbool_MULTIPLEX_ACTIVEactivereturnNonec                $    t          |           adS )zMark whether the process is running as a profile multiplexer.

    Called once at gateway startup. When True, ``get_secret`` fails closed on
    an unscoped read instead of falling back to ``os.environ``.
    N)r
   r   )r   s    8/home/rurouni/.hermes/hermes-agent/agent/secret_scope.pyset_multiplex_activer   '   s     V    c                     t           S )z?Return whether the process is running as a profile multiplexer.)r    r   r   is_multiplex_activer   1   s    r   _SECRET_SCOPE)defaultz'ContextVar[Optional[Mapping[str, str]]]c                      e Zd ZdZdS )UnscopedSecretErrora  Raised when a secret is read in multiplex mode with no scope installed.

    This is the fail-closed signal: it means a credential read reached
    ``get_secret`` without a profile scope active, which in a multiplexer would
    otherwise leak whichever profile's value happened to be in ``os.environ``.
    The fix is to wrap the call path in ``set_secret_scope(...)`` (the per-turn
    / per-adapter profile scope), not to widen the allowlist.
    N)__name__
__module____qualname____doc__r   r   r   r   r   <   s           r   r   secretsOptional[Mapping[str, str]]r   c                6    t                               |           S )zInstall the active profile's secret mapping for the current context.

    Returns a token for ``reset_secret_scope``. Pass ``None`` to clear.
    )r   set)r   s    r   set_secret_scoper"   G   s    
 W%%%r   tokenc                :    t                               |            dS )z"Restore the previous secret scope.N)r   reset)r#   s    r   reset_secret_scoper&   O   s    r   c                 4    t                                           S )zEReturn the active secret mapping, or None when no scope is installed.)r   getr   r   r   current_secret_scoper)   T   s    r   >   TZPWDHOMELANGPATHUSERSHELLLC_ALLTMPDIR
PYTHONPATHHERMES_HOMEVIRTUAL_ENVSSL_CERT_FILEHERMES_PROFILE_HERMES_GATEWAYHERMES_KANBAN_DBHERMES_MAX_TOKENSHERMES_API_TIMEOUTHERMES_KANBAN_BOARDHERMES_MAX_ITERATIONSHERMES_REDACT_SECRETSHERMES_GATEWAY_LOCK_DIRHERMES_NOUS_TIMEOUT_SECONDSHERMES_KANBAN_WORKSPACES_ROOT)HERMES_KANBAN_HERMES_TELEGRAM_	TERMINAL_namestrc                Z      t           v rdS t           fdt          D                       S )zGReturn True for genuinely process-global (non-profile-secret) env vars.Tc              3  B   K   | ]}                     |          V  d S N)
startswith).0prE   s     r   	<genexpr>z!_is_global_env.<locals>.<genexpr>x   s/      @@atq!!@@@@@@r   )_GLOBAL_ENV_EXACTany_GLOBAL_ENV_PREFIXES)rE   s   `r   _is_global_envrQ   t   s8       t@@@@+?@@@@@@r   r   Optional[str]c                T   t          |           r%t          j                            |           }||n|S t                                          }||                    |           }||n|S t
          rt          d| d          t          j                            |           }||n|S )uZ  Resolve a credential by env-var name, honoring the active profile scope.

    Resolution order:

    1. Genuinely-global vars (``_is_global_env``) always read ``os.environ`` —
       they are deployment settings, not profile secrets.
    2. When a secret scope is installed (multiplexed turn), read from it; an
       absent key returns ``default``. The scope is authoritative — we do NOT
       fall through to ``os.environ``, because in a multiplexer ``os.environ``
       may hold another profile's value.
    3. No scope installed:
       - multiplex INACTIVE (default deployment): read ``os.environ`` —
         identical to the legacy ``os.getenv`` behavior every caller had before.
       - multiplex ACTIVE: FAIL CLOSED. Raise ``UnscopedSecretError`` so the
         missing scope is caught loudly instead of leaking a cross-profile value.
    Nzget_secret(a1  ) called with no profile secret scope active while multiplexing is on. This credential read must run inside a set_secret_scope(...) block (the per-turn / per-adapter profile scope). Reading os.environ here would risk leaking another profile's value. See docs/design/multiplexing-gateway.md (Workstream A).)rQ   osenvironr(   r   r   r   )rE   r   valscopes       r   
get_secretrX   {   s    " d 3jnnT""oss72Eiioooss72 
!$   
 
 	
 *..

C/33w.r   env_pathr   Dict[str, str]c                   i }	 |                      d          }n# t          t          t          f$ r |cY S w xY w|                                D ]}|                                }|r|                    d          r.|                    d          r)|t          d          d                                         }d|vrq|	                    d          \  }}}|                                }|s|                                }t          |          dk    r&|d         |d	         k    r|d         d
v r
|dd	         }|||<   |S )ur  Parse a ``.env`` file into a plain dict WITHOUT touching ``os.environ``.

    Used to load a profile's secrets into an isolated mapping for
    ``set_secret_scope``. Mirrors python-dotenv's basic parsing (KEY=VALUE,
    ``export`` prefix, ``#`` comments, optional matching quotes) but never
    mutates the process environment — that isolation is the whole point.
    zutf-8)encoding#zexport N=   r   )'"   )
	read_textFileNotFoundErrorOSErrorUnicodeDecodeError
splitlinesstriprJ   lenlstrip	partition)rY   r   textrawlinekey_values           r   load_env_filers      s`    !G!!7!33w(:;        yy{{ 	ts++ 	??9%% 	2I(//11Dd??s++Qiikk 	u::??uQx5944qZ9O9O!B$KENs    77hermes_homec                @    t          t          |           dz            S )u)  Build a profile's secret mapping from its ``<home>/.env``.

    Returns a fresh dict (safe to install via ``set_secret_scope``). Genuinely
    global vars are intentionally NOT copied in — ``get_secret`` reads those
    from ``os.environ`` directly, so the scope holds only profile secrets.
    z.env)rs   r   )rt   s    r   build_profile_secret_scoperv      s     k**V3444r   )r   r
   r   r   )r   r
   )r   r   r   r   )r#   r   r   r   )r   r   )rE   rF   r   r
   rI   )rE   rF   r   rR   r   rR   )rY   r   r   rZ   )rt   r   r   rZ   )r   
__future__r   rT   contextvarsr   r   pathlibr   typingr   r   r	   r   __annotations__r   r   r   RuntimeErrorr   r"   r&   r)   	frozensetrN   rP   rQ   rX   rs   rv   r   r   r   <module>r~      s    , # " " " " " 				 ) ) ) ) ) ) ) )       * * * * * * * * * *       % % % %    :DT: : :    
    ,   & & & &   
    I       A A A A%/ %/ %/ %/ %/P   D5 5 5 5 5 5r   