§
    ÷žyj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 )zÑMark 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/ragecks/.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 )z’Install 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/   øè è € Ð@Ð@ aˆt�Š˜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ÝŒj�nŠn˜TÑ"Ô"ˆØ�oˆsˆs¨7Ð2å×ÒÑÔ€EØÐØ�iŠi˜‰oŒoˆØˆ?ØˆJÝð 	ØˆNõ Œj�nŠn˜TÑ"Ô"ˆØ�oˆsˆs¨7Ð2åð 
Ý!ð˜$ð ð ð ñ
ô 
ð 	
õ Œ*�.Š.˜Ñ
Ô
€CØ�/ˆ3ˆ3 wÐ.r   Úvaluec                óÌ  — |                       ¦   «         } | s| S | d         }|dv r’d}|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  € ð  �KŠK‰MŒM€EØð ØˆØ�!ŒH€EØ�
ÐÐØˆØ•#�e‘*”*ŠnˆnØ�q”ˆBØ˜Š|ˆ|  d¢
 
Ø�Q‘�ØØ�UŠ{ˆ{Ø! ! a¡% & &œM×0Ò0Ñ2Ô2�	Ø×'Ò'¨Ñ,Ô,ð *Ø   1 q¡5 œ>Ð)Ø�Ø�‰FˆAð •#�e‘*”*Šnˆnð ˆÝŒ8�G˜U¨QÐ/Ñ/Ô/°Ô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ˆØ�yŠy‰{Œ{ˆØð 	�t—’ sÑ+Ô+ð 	ØØ�?Š?˜9Ñ%Ô%ð 	2Ø�˜I™œ˜˜Ô(×/Ò/Ñ1Ô1ˆDØ�dˆ?ˆ?ØØŸš sÑ+Ô+‰ˆˆQ�Ø�iŠi‰kŒkˆØð 	ØØ'Ð'Õ(=¸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Ý˜D 6™MÑ*Ô*€GðØBÐBÐBÐBÐBÐBØ3Ð3°DÑ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   