
    epj2                       U d Z ddlm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 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d0dZd1d2d"Zd3d$Zd4d(Zd5d*ZdS )6u  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/thesage/.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   2   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"   H   s    
 W%%%r   tokenc                :    t                               |            dS )z"Restore the previous secret scope.N)r   reset)r#   s    r   reset_secret_scoper&   P   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)   U   s    r   >   TZPWDHOMELANGPATHUSERSHELLLC_ALLTMPDIR
PYTHONPATHHERMES_HOMEVIRTUAL_ENVSSL_CERT_FILEHERMES_PROFILEAPI_SERVER_HOSTAPI_SERVER_PORT_HERMES_GATEWAYHERMES_KANBAN_DBHERMES_MAX_TOKENSAPI_SERVER_ENABLEDHERMES_API_TIMEOUTHERMES_KANBAN_BOARDHERMES_MAX_ITERATIONSHERMES_REDACT_SECRETSAPI_SERVER_CORS_ORIGINS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prI   s     r   	<genexpr>z!_is_global_env.<locals>.<genexpr>   s/      @@atq!!@@@@@@r   )_GLOBAL_ENV_EXACTany_GLOBAL_ENV_PREFIXES)rI   s   `r   _is_global_envrU   }   s8       t@@@@+?@@@@@@r   r   Optional[str]c                   t          |           r%t          j                            |           }||n|S t                                          }|G|                    |           }||S t
          r|S t          j                            |           }||n|S t
          rt          d| d          t          j                            |           }||n|S )u=  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. Under
       multiplexing the scope is authoritative — an absent key returns
       ``default`` and we do NOT fall through to ``os.environ``, because in a
       multiplexer ``os.environ`` may hold another profile's value. When
       multiplexing is OFF, a scope miss falls through to ``os.environ``:
       single-profile deployments legitimately provide credentials via the
       process environment (systemd ``Environment=``, secret-manager wrappers
       like ``pass-cli run`` / ``op run``, plain shell exports) rather than
       ``<home>/.env``, and the scope — installed unconditionally around e.g.
       every cron job — must stay a ``.env`` overlay, not a blindfold.
    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).)rU   osenvironr(   r   r   r   )rI   r   valscopes       r   
get_secretr\      s    . d 3jnnT""oss72Eiioo?J 	N jnnT""oss72 
!$   
 
 	
 *..

C/33w.r   valuec                   |                                  } | s| S | d         }|dv rd}|t          |           k     r{| |         }|dk    r|dk    r|dz  }-||k    rC| |dz   d                                         }|                    d          r| d|dz            S | S |dz  }|t          |           k     {| S t	          j        d	| d
          d                                          S )a-  Strip a dotenv-style inline comment from a raw ``.env`` value.

    Mirrors python-dotenv (1.2.2) semantics, verified empirically:

    - Quoted values: scan for the matching close quote
      (backslash-escape-aware for double quotes, since ``save_env_value``
      writes ``\"``/``\\`` escapes). Everything through the close quote is
      kept; a trailing ``# ...`` remainder after it is discarded, so
      ``KEY="has # inside" # trailing`` yields ``has # inside``. Non-comment
      trailing junk leaves the value untouched (lenient, unlike dotenv's
      hard parse error).
    - Unquoted values: truncate only at a ``#`` PRECEDED BY WHITESPACE, so
      ``KEY=foo#bar`` keeps ``foo#bar`` while ``KEY=value # comment`` keeps
      ``value``. A value that *starts* with ``#`` (``KEY=#leading``) is kept.
    r   )'"   r`   \   N#z\s+#)maxsplit)striplenlstriprN   resplit)r]   quoteich	remainders        r   _strip_inline_commentro      s     KKMME !HE
#e**nnqB||d

QU{{!!a%&&M0022	'',, * 1q5>)FA #e**nn 8GUQ///288:::r   env_pathr   Dict[str, str]c                &   i }	 |                      d          }n# t          t          t          f$ r |cY S w xY wddlm} |                                D ]}|                                }|r|                    d          r.|                    d          r)|t          d          d         
                                }d|vrq|                    d          \  }}}|                                }|s |t          |                    ||<   |S )	u  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``. Parses the small KEY=VALUE subset Hermes writes
    itself (``export`` prefix, ``#`` comments — full-line and
    dotenv-compatible inline, matching quotes with the
    writer's ``\"``/``\\`` escapes reversed — the same semantics as
    ``hermes_cli.config._parse_env_value``) but never mutates the process
    environment — that isolation is the whole point.

    Encoding is ``utf-8-sig`` so a leading UTF-8 BOM (Windows Notepad /
    PowerShell ``Set-Content -Encoding UTF8``) does not prefix the first
    key as ``\ufeffNAME`` and make ``get_secret('NAME')`` miss under scope.
    z	utf-8-sig)encodingr   )_parse_env_valuerd   zexport N=)	read_textFileNotFoundErrorOSErrorUnicodeDecodeErrorhermes_cli.configrt   
splitlinesrf   rN   rg   rh   	partitionro   )	rp   r   textrt   rawlinekey_r]   s	            r   load_env_filer      s?    !G!!;!77w(:;    322222   F Fyy{{ 	ts++ 	??9%% 	2I(//11Dd??s++Qiikk 	''(=e(D(DEENs    77hermes_homec                    t          |           }t          |dz            }	 ddlm}  ||          }n# t          $ r i }Y nw xY w|                                D ]\  }}t          |          r|||<   |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.envr   )get_secret_source_values)r   r   hermes_cli.env_loaderr   	ExceptionitemsrU   )r   homer   r   external_secretsr   r]   s          r   build_profile_secret_scoper     s     DD6M**GBBBBBB33D99    ',,..  
U# 	Ns   5 AA)r   r
   r   r   )r   r
   )r   r   r   r   )r#   r   r   r   )r   r   )rI   rJ   r   r
   rM   )rI   rJ   r   rV   r   rV   )r]   rJ   r   rJ   )rp   r   r   rq   )r   r   r   rq   ) r   
__future__r   rX   ri   contextvarsr   r   pathlibr   typingr   r   r	   r   __annotations__r   r   r   RuntimeErrorr   r"   r&   r)   	frozensetrR   rT   rU   r\   ro   r   r   r   r   r   <module>r      s    , # " " " " " 				 				 ) ) ) ) ) ) ) )       * * * * * * * * * *       % % % %    :DT: : :    
    ,   & & & &   
    I      ( A A A A6/ 6/ 6/ 6/ 6/r"; "; "; ";J+ + + +\     r   