fix(cron): drop the launch profile's dotenv residue before overlaying a routed no_agent scope

The routed no_agent child env started from all of os.environ and only overwrote the
names present in the installed scope. A name defined only by the LAUNCH profile's .env
and absent from the routed profile therefore reached the routed child with the launch
value instead of unset -- the secret scrub only knows classified names, so a custom or
unclassified secret crossed the profile boundary (review finding on the first head).

strip_launch_profile_env (main, 284d220ba4) is the primitive the external-worker path
already uses for exactly this: it drops the launch profile's dotenv-owned keys and the
bridged TERMINAL_* settings, and is a no-op outside multiplex or when the target IS the
launch profile. Apply it to the base BEFORE the scope overlay (so a shared name keeps its
routed value) and BEFORE the sanitizer (so routed values still pass the scrub and
passthrough rules). Pinned by a child-process negative control: the launch-only name
arrives <unset>, the shared name arrives routed, and the parent process is unchanged.

(cherry picked from commit 69349b527b5144c1f1540a4c10035d5ec798c1db)
This commit is contained in:
John Paul Soliva
2026-09-12 23:06:04 +09:00
committed by kshitij
parent dbede34f6e
commit 023e4f997f
2 changed files with 44 additions and 4 deletions
+8 -4
View File
@@ -350,11 +350,15 @@ def _run_job_script(
"encoding": "utf-8",
"errors": "replace"}
# A routed profile's script (desktop multi-profile ticker, multiplex gateway) must see ITS
# profile's .env + vault values — the process env holds the launch profile's. Overlay the
# installed scope onto the base BEFORE sanitizing, so the same scrub / passthrough rules
# apply to those values as to any other; the parent process is never mutated.
# profile's .env + vault values — the process env holds the launch profile's. Drop the
# launch profile's dotenv-owned residue first (a name only the launch .env defines must
# come through UNSET, not with the launch value — the scrub only knows classified secrets),
# then overlay the installed scope, then sanitize, so routed values pass the same scrub /
# passthrough rules as any other. No-op outside multiplex or for the launch profile's own
# fires; the parent process is never mutated.
from agent.secret_scope import current_secret_scope
base = dict(os.environ)
from tools.environments.local import strip_launch_profile_env
base = strip_launch_profile_env(dict(os.environ))
scope = current_secret_scope()
if scope:
base.update(scope)
+36
View File
@@ -395,3 +395,39 @@ def test_a_routed_profile_script_receives_its_own_profile_env(hermes_env, monkey
assert ok, output
assert output.strip() == "routed|routed-only"
assert os.environ["CUSTOM_CRON_VALUE"] == "launch" # the parent process was not mutated
def test_a_routed_profile_script_never_receives_a_launch_profile_only_value(hermes_env, monkeypatch):
"""Negative control for the overlay above (#107695 review): a name the LAUNCH profile's .env
defines and the routed scope does not must reach the routed child UNSET — not with the launch
value. The secret scrub only knows classified names, so a custom or unclassified secret would
otherwise cross the profile boundary; the launch profile's dotenv residue is dropped first."""
import os
from agent import secret_scope
from cron.scheduler_script import _run_job_script
from hermes_constants import get_process_hermes_home, reset_hermes_home_override, set_hermes_home_override
launch = get_process_hermes_home()
(launch / ".env").write_text("LAUNCH_ONLY_VALUE=launch-only\nCUSTOM_CRON_VALUE=launch\n", encoding="utf-8")
monkeypatch.setenv("LAUNCH_ONLY_VALUE", "launch-only")
monkeypatch.setenv("CUSTOM_CRON_VALUE", "launch")
routed = launch / "profiles" / "ops"
(routed / "scripts").mkdir(parents=True, exist_ok=True)
# Under the routed home override the runner resolves scripts against THAT profile's scripts dir.
script = routed / "scripts" / "probe_launch_only.sh"
script.write_text('#!/bin/bash\necho "${CUSTOM_CRON_VALUE}|${LAUNCH_ONLY_VALUE:-<unset>}"\n')
home_token = set_hermes_home_override(str(routed))
context_token = secret_scope.set_multiplex_context(True)
scope_token = secret_scope.set_secret_scope({"CUSTOM_CRON_VALUE": "routed"})
try:
ok, output = _run_job_script("probe_launch_only.sh")
finally:
secret_scope.reset_secret_scope(scope_token)
secret_scope.reset_multiplex_context(context_token)
reset_hermes_home_override(home_token)
assert ok, output
assert output.strip() == "routed|<unset>"
assert os.environ["LAUNCH_ONLY_VALUE"] == "launch-only" # the parent process was not mutated