+
    lHàj¡2  ã                  ó�  a € 0 t $ R t^ RIHt ^ RIt^ RIt^ RIt^ RIt^ RIH	t	 ^ RI
HtHtHt ]P                  ! ]4      t]P"                  ! 4       tRsR]R&   R R ltR	 R
 ltR R ltR R ltR R ltR R ltR R ltR R lt]P:                  R R l4       t]P:                  R R l4       tR t ]PB                  R 4       t"R# )uŒ  Launch-profile policy for a process that hosts several profile homes (``hermes serve`` /
``hermes dashboard`` pooling, ``?profile=``, hosted rooms; the multiplexed gateway's own worker).

Two facts anchor this module:

* ``agent.secret_scope.get_secret`` fails closed ONLY while ``set_multiplex_active(True)`` holds.
  A ``serve`` backend that hosts a second profile home never flipped it, so every unscoped read
  for a secondary profile silently returned the LAUNCH profile's ``os.environ`` value. The flip
  happens here, at the moment the process first learns it hosts another profile home.
* Once multiplexing is active the launch profile is a profile too: its turns/RPCs must run under
  their own scope instead of ambient ``os.environ`` (a secondary context may have poisoned it,
  #107422). A scope rebuilt from ``<launch home>/.env`` + ``config.yaml`` alone would drop the
  launch process's legitimate env-only policy â€” ``TERMINAL_ENV=ssh`` or a provider key injected
  by systemd / ``op run`` has no file to rebuild it from. The process env is trusted exactly
  once: frozen at activation, before any secondary code has run, never re-read afterwards.
)ÚannotationsN)ÚPath)ÚDictÚIteratorÚOptionalzOptional[dict[str, str]]Ú	_snapshotc               ó   € V ^8„  d   QhRR/# ©é   Úreturnúdict[str, str]© )Úformats   "ÚF/home/ubuntu/.hermes/hermes-agent/tui_gateway/launch_profile_policy.pyÚ__annotate__r   !   s   € ÷ 
ñ 
˜Nñ 
ó    c                 ó¼   € \         ;_uu_ 4        \        f   \        \        P                  4      s\        \        4      uuRRR4       #   + '       g   i     R# ; i)ué   Freeze the process env as the launch profile's own; the first capture wins.

Called at activation, immediately before the first secondary home is registered as
served â€” the last moment ambient env is provably the launch profile's.
N)Ú_lockr   ÚdictÚosÚenvironr   r   r   Úcapture_launch_envr   !   s0   € ÷ 
ŽÜÒÜœRŸZ™ZÓ(ˆIÜ”I‹÷ 
��‹ús   �0A
Á
A	c               ó   € V ^8„  d   QhRR/# )r
   r   ÚNoner   )r   s   "r   r   r   .   s   € ÷ ñ ¨ñ r   c                 ó6   € ^ RI Hp  \        4        V ! R4       R# )z´This process now hosts a profile home other than its launch home: freeze the launch env
and make unscoped credential reads fail closed (``get_secret`` raises instead of borrowing).©Úset_multiplex_activeTN)Úagent.secret_scoper   r   r   s    r   Úactivate_multi_profile_hostingr   .   s   € õ 8ÜÔÙ˜Ör   c               ó   € V ^8„  d   QhRR/# )r
   r   Úsetr   )r   s   "r   r   r   6   s   € ÷ ñ  ñ r   c                 ó”  € ^ RI Hp  ^ RIHp V! RRRR7       UUu0 uF5  w  r#VR8X  g   V ! V4      '       g   K  \	        V4      P                  4       kK7  	  pppTP                  \	        \        P                  P                  R4      ;'       g    \        P                  ! 4       R,          4      P                  4       4       V# u uppi )uF  Resolved homes this host could be asked to serve: the launch home plus every profile dir
carrying a real servability marker.

``named_profile_has_identity`` accepts an EMPTY ``.env``, which is all a crashed
``hermes profile create`` leaves behind â€” counting it would flip a single-profile host
fail-closed at its next boot.
)Ú#named_profile_has_servable_identity)Úprofiles_to_serveT)Ú	multiplexÚinclude_standaloneÚinclude_parkedÚdefaultÚHERMES_HOMEz.hermes)Úhermes_constantsr"   Úhermes_cli.profilesr#   r   ÚresolveÚaddr   r   ÚgetÚhome)r"   r#   Únamer.   Úhomess        r   Ú_servable_profile_homesr1   6   s©   € õ EÝ5á4EÐPTÐimð  Cõ  5Dô Pñ  5D¡j dØ˜	Ô!Ñ%HÈ×%Nô "ŒT�$‹Z×ÑÖ!ñ  5D€Eñ Pà	‡I�IŒd”2—:‘:—>‘> -Ó0×KÐK´D·I²I³KÀ)Õ4KÓL×TÑTÓVÔWØ€LùóPs
   ›C¹Cc               ó   € V ^8„  d   QhRR/# )r
   r   Úboolr   )r   s   "r   r   r   G   s   € ÷ +ñ +°ñ +r   c                 óP  € ^ RI Hp  V ! 4       '       d   R#  \        4       p\        T4      ^8  d   R# \        P                  R\        T4      4       \        4        ^ RI	H
p T! \        T4      4       R#   \         d&    \        P                  RRR7       \        4         R# i ; i)u´  Activate at HOST startup when this machine has more than one servable profile home.

Activation used to be lazy and one-way â€” it fired the first time a *request* asked for a second
home. Everything the host had already done by then (idle-reaper ticks, cron start, adapter
connects, MCP discovery) ran under single-profile assumptions and is never re-scoped, and the
launch profile's env had already been mutated by then, so ``capture_launch_env`` froze a
polluted snapshot. One ``hermes serve`` / ``hermes gateway run`` per host means the process
knows at boot whether it can be asked for a second home: decide there, once. Call it as the LAST
boot step: the frozen snapshot is the only source for launch keys with no ``.env`` to rebuild
from, so every credential the boot still injects must already be in ``os.environ``.

``gateway.multiplex_profiles: false`` is deliberately NOT consulted: it is retired as a
topology opt-out, and a multi-home host that skipped activation because of a stale ``false``
would serve a second profile with the LAUNCH profile's credentials â€” the exact fail-open this
guard exists to prevent.

A genuinely single-profile host still never activates (byte-identical behaviour, ``os.environ``
precedence preserved). An unreadable profiles directory fails CLOSED: we cannot prove the host
is single-profile, and the lazy backstop only fires once a request has already been answered.
Returns True when this call activated hosting.
©Úis_multiplex_activeFz¬Could not enumerate this host's profile homes; activating multi-profile hosting fail-closed (unscoped credential reads will raise instead of borrowing the launch profile's)T)Úexc_infozFMulti-profile hosting activated at startup (%d servable profile homes))Úenable_profile_log_routing)r   r6   r1   Ú	ExceptionÚloggerÚwarningr   ÚlenÚinfoÚhermes_loggingr8   Úsorted)r6   r0   r8   s      r   Ú&activate_multi_profile_hosting_eagerlyr@   G   s•   € õ, 7Ù×ÒÙðÜ'Ó)ˆô ˆ5ƒz�A„~ÙÜ
‡K�KÐXÔZ]Ð^cÓZdÔeÜ"Ô$õ :Ùœv e›}Ô-Ùøô! ô Ü�‰ðà#'ð 	ô 	)ô 	'Ô(Úðús   —
A5 Á5,B%Â$B%c               ó   € V ^8„  d   QhRR/# r	   r   )r   s   "r   r   r   u   s   € ÷ Oñ O�^ñ Or   c                 ón   € ^ RI Hp  V ! 4       '       d   \        4       # \        \        P
                  4      # )zòThe launch profile's env: frozen once multiplexing is active; the LIVE process env before
(no secondary has run yet, so it is provably the launch profile's, and freezing it early would
miss values the launch process still bridges at startup).r5   )r   r6   r   r   r   r   r5   s    r   Ú_launch_envrC   u   s'   € õ 7Ù#6×#8Ò#8ÔÓÐN¼dÄ2Ç:Á:Ó>NÐNr   c               ó   € V ^8„  d   QhRR/# r	   r   )r   s   "r   r   r   }   s   € ÷ Xñ X˜^ñ Xr   c                 ó˜   € \        4       P                  4        U Uu/ uF   w  rV P                  R4      '       g   K  WbK"  	  upp # u upp i )zûThe frozen launch ``TERMINAL_*`` overlay for a launch-profile turn's terminal scope.

Production always captured at activation; a first capture here only happens when the
multiplexer flag was set by another owner (the messaging gateway) or a harness.
Ú	TERMINAL_)r   ÚitemsÚ
startswith)ÚkÚvs     r   Úlaunch_terminal_envrK   }   s;   € ô 0Ó1×7Ñ7Ô9ÔWÑ9‘T�Q¸Q¿\¹\È+×=VŒDˆAŠDÑ9ÒWÐWùÓWs
   œA¼Ac               ó    € V ^8„  d   QhRRRR/# )r
   Úlaunch_homeú
str | Pathr   r   r   )r   s   "r   r   r   †   s   € ÷ ñ  Zð °Nñ r   c                óØ   € ^ RI HpHp \        4       P	                  4        UUu/ uF  w  r4V! V4      '       d   K  W4bK  	  pppVP                  V! \        V 4      4      4       V# u uppi )uJ  The launch profile's secret mapping: its ``.env`` + external sources over the launch env
(systemd / ``op run`` injection survives the fail-closed flip; a secondary never sees it because
its scope is built from its own files only). Bound for EVERY launch-profile body, multiplexing or
not, so the body's credential source is decided once at entry: a request that entered while
single-profile keeps resolving from this mapping after a concurrent first secondary flips
``get_secret`` to fail closed (``_MULTIPLEX_ACTIVE`` is read on every ``get_secret``, the
scope decision was made at entry).

PRECEDENCE, and it is not the process's: ``<launch home>/.env`` (plus hydrated external sources)
WINS over the env. For a key present in both, an unscoped read returned the ambient
``os.environ`` value before activation and returns the ``.env`` value after â€” so activation is
not purely "stricter" for the launch tenant, it also changes which of the launch profile's own
two values it sees. Env-only keys (systemd ``Environment=``, ``op run``, Compose) are unaffected:
nothing in the files shadows them.
)Ú_is_global_envÚbuild_profile_secret_scope)r   rP   rQ   rC   rG   Úupdater   )rM   rP   rQ   rI   rJ   Úscopes   &     r   Úlaunch_secret_scoperT   †   sX   € ÷  NÜ)›m×1Ñ1Ô3ÔMÑ3‘d�a¹>È!×;LŒTˆQŠTÑ3€EÑMØ	‡L�LÑ+¬D°Ó,=Ó>Ô?Ø€Lùó Ns
   ¤A&»A&c               ó    € V ^8„  d   QhRRRR/# )r
   r.   rN   r   r   r   )r   s   "r   r   r   �   s   € ÷ 
2ñ 
2˜Zð 
2¨Nñ 
2r   c                óx   € ^ RI HpHp V! \        V 4      4      '       d   \	        V 4      # V! \        V 4      4      # )aè  THE secret mapping for a body owned by *home*: ``launch_secret_scope`` when *home* is this
process's own (its env-only keys from docker ``-e`` / systemd ``Environment=`` / ``op run`` have
no file to rebuild from), else the profile's own files (``build_profile_secret_scope``). Every
per-home binding point (routed gateway turns, cron fires, kanban ticks) builds through here, so
the launch profile keeps its env-only credentials when a single-profile gateway starts
multiplexing in place.)Ú_is_process_homerQ   )r   rW   rQ   r   rT   )r.   rW   rQ   s   &  r   Úowner_secret_scoperX   �   s1   € ÷ PÙœ˜T›
×#Ò#Ü" 4Ó(Ð(Ù%¤d¨4£jÓ1Ð1r   c               ó   € V ^8„  d   QhRR/# )r
   r   úIterator[None]r   )r   s   "r   r   r   «   s   € ÷ 'ñ '�^ñ 'r   c               #  ó†   a"  € ^ RI Hp Hp ^ RIHp V! 4       oV! V3R l4      p Rx € V ! V4       R#   T ! T4       i ; i5i)al  Mark a standalone launch-profile body (a primary-adapter message) at ENTRY. Nothing is bound
while single-profile, so its reads keep the process-env semantics they always had; if a second
profile switches the gateway to fail-closed mid-body, an unscoped read resolves from
``launch_secret_scope`` (frozen env + ``.env``) instead of raising ``UnscopedSecretError``.)Úreset_launch_body_scopeÚset_launch_body_scope©Úget_process_hermes_homec                 ó   <€ \        S 4      # )N)rT   )r.   s   €r   Ú<lambda>Úlaunch_body.<locals>.<lambda>³   s   ø€ Ô*=¸dÔ*Cr   N)r   r\   r]   r)   r_   )r\   r]   r_   Útokenr.   s       @r   Úlaunch_bodyrd   ª   s;   øé € ÷ RÝ8Ù"Ó$€DÙ!Ô"CÓD€Eð'Ûá Ö&øÑ Õ&üs   ƒ"A¦4 ª
A´
>¾Ac               ó    € V ^8„  d   QhRRRR/# )r
   rM   rN   r   rZ   r   )r   s   "r   r   r   »   s   € ÷ 3ñ 3¨jð 3¸^ñ 3r   c              #  óp  "  € ^ RI HpHp ^ RIHpHp ^ RIHpHp \        V 4      pR;p;rš V! \        V4      4      pV! \        V4      4      p	V! V\        4       R7      p
Rx € V
e	   V! V
4       V	e	   V! V	4       Ve   V! V4       R# R#   T
e	   T! T
4       T	e	   T! T	4       Te
   T! T4       i i ; i5i)aŠ  Bind the launch profile's own runtime scope for one body: HERMES_HOME override naming the
launch home, ``launch_secret_scope``, and its terminal policy over the frozen launch
``TERMINAL_*`` overlay. For hosts whose launch-profile bodies are not RPC sessions (the
standalone messaging gateway after a hosted room activated multiplexing, #112878).

The home override is bound even though the launch home IS the process home: under multiplexing
"override unset" is the fail-closed signal for an UNBOUND context (``serves_routed_profile``,
plugin runtime bindings such as OMH's ``pre_tool_call`` gate, per-home slots keyed on the
override), so a launch-profile turn without it was indistinguishable from no turn at all and
every plugin hook it fired saw an unscoped process (#118538). Routed turns already bind theirs
(``gateway/run.py::_profile_runtime_scope``); the launch profile is a tenant like any other.)Úreset_secret_scopeÚset_secret_scope)Úreset_hermes_home_overrideÚset_hermes_home_override)Úinstall_profile_terminal_scopeÚreset_terminal_scopeN)Úenv_overlay)r   rg   rh   r)   ri   rj   Útools.terminal_scoperk   rl   r   ÚstrrT   rK   )rM   rg   rh   ri   rj   rk   rl   r.   Ú
home_tokenÚsecret_tokenÚterminal_tokens   &          r   Úlaunch_profile_runtime_scopers   º   s¿   é € ÷ HßUßYä�Ó€DØ15Ð5€JÐ5�ð3Ù-¬c°$«iÓ8ˆ
Ù'Ô(;¸DÓ(AÓBˆÙ7¸ÔJ]ÓJ_Ô`ˆÛàÒ%Ù  Ô0ØÒ#Ù˜|Ô,ØÒ!Ù& zÖ2ñ "øð	 Ò%Ù  Ô0ØÒ#Ù˜|Ô,ØÒ!Ù& zÕ2ð "üs   ‚)B6¬8B Á$(B6Â'B3Â3B6c                 ó~   € ^ RI Hp  V ! 4       '       g   \        P                  ! 4       # ^ RIHp \        V! 4       4      # )uÜ  The launch profile's own runtime scope once this process multiplexes; ``nullcontext``
before.

The single seam for "no routed profile here": under the one-process-per-host ruling the launch
profile is a tenant like any other, so a body that used to run with NO scope at all (ambient
``os.environ`` + the process home) must bind the launch profile explicitly â€” otherwise a
secondary's context can have poisoned the ambient state, and a fail-closed ``get_secret`` raises
on a perfectly legitimate launch-profile read. Before activation the process env IS the launch
profile's, so binding nothing is still correct (and keeps single-profile hosts byte-identical â€”
callers assert the returned object is literally a ``nullcontext``).
r5   r^   )r   r6   Ú
contextlibÚnullcontextr)   r_   rs   )r6   r_   s     r   Ú#launch_profile_scope_if_multiplexedrw   Û   s1   € õ 7Ù× Ò Ü×%Ò%Ó'Ð'Ý8Ü'Ñ(?Ó(AÓBÐBr   c                óv   "  € \        4       ;_uu_ 4        R5x € RRR4       R#   + '       g   i     R# ; i5i)z¸``async with`` twin of :func:`launch_profile_scope_if_multiplexed` (no I/O of its own: the
launch scope is rebuilt from already-hydrated sources, so there is nothing to move off-loop).N)rw   r   r   r   Ú)async_launch_profile_scope_if_multiplexedry   î   s!   é € ô 
-×	.Õ	.Ü÷ 
/×	.×	.Ò	.üs   ‚9•%›
9¥6	°	9)#Ú__conditional_annotations__Ú__doc__Ú
__future__r   ru   Úloggingr   Ú	threadingÚpathlibr   Útypingr   r   r   Ú	getLoggerÚ__name__r:   ÚLockr   r   Ú__annotations__r   r   r1   r@   rC   rK   rT   rX   Úcontextmanagerrd   rs   rw   Úasynccontextmanagerry   )rz   s   @r   Ú<module>r‡      sÌ   øðôõ" #ã Û Û 	Û Ý ß +Ñ +à	×	Ò	˜8Ó	$€à�ŠÓ€Ø&*€	Ð#Ó *õ
õõõ"+õ\OõXõõ.
2ð ×Ñô'ó ð'ð ×Ñô3ó ð3ò@Cð& ×Ññó  òr   