fix(cron): strip launch external-source names too, and make the scope refresh replace
Two credential-isolation gaps found in review of the previous head. 1. strip_launch_profile_env() only knows dotenv- and terminal-config-owned names, but external secret sources (vault, 1Password, ...) also write their names into the shared os.environ and are tracked in secret_source_names(). A name the LAUNCH profile's source supplied therefore still reached a routed no_agent child. Drop every non-global source-owned name from the base; the routed scope overlay that follows puts back exactly the ones that profile's OWN sources supply, since build_profile_secret_scope folds get_secret_source_values(home) in. 2. refresh_installed_secret_scope() merged the rebuild with dict.update(), so a name a source had stopped supplying -- rotated, revoked, source removed -- kept its old value for the rest of the fire. Replace the mapping contents instead: the rebuild is the profile's current truth. Regressions: a routed child sees <unset> for a launch-source name while its own source value comes through, and a refresh whose rebuild omits a name drops it. Both fail if the corresponding change is reverted. (cherry picked from commit ecd51517c4828a75acb5f458ed99aea0bc3e5e9f)
This commit is contained in:
committed by
kshitij
parent
023e4f997f
commit
0943e77136
@@ -262,5 +262,10 @@ def refresh_installed_secret_scope(hermes_home: Path) -> bool:
|
||||
scope = _SECRET_SCOPE.get()
|
||||
if not isinstance(scope, dict):
|
||||
return False
|
||||
scope.update(build_profile_secret_scope(hermes_home))
|
||||
# REPLACE, don't merge: the rebuild is the profile's current truth, so a name a source has
|
||||
# stopped supplying (rotated, revoked, source removed) must disappear from the fire's scope
|
||||
# rather than survive as the stale value dict.update() would keep.
|
||||
rebuilt = build_profile_secret_scope(hermes_home)
|
||||
scope.clear()
|
||||
scope.update(rebuilt)
|
||||
return True
|
||||
|
||||
@@ -356,9 +356,18 @@ def _run_job_script(
|
||||
# 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
|
||||
from agent.secret_scope import _is_global_env, current_secret_scope
|
||||
from hermes_cli.env_loader import secret_source_names
|
||||
from tools.environments.local import strip_launch_profile_env
|
||||
base = strip_launch_profile_env(dict(os.environ))
|
||||
# strip_launch_profile_env only knows dotenv- and terminal-config-owned names. External
|
||||
# secret sources (vault, 1Password, ...) also write their names into the shared os.environ,
|
||||
# tracked in secret_source_names(), and a name the LAUNCH profile's source supplied is not
|
||||
# this profile's to see. Drop them all here; the overlay below puts back exactly the ones
|
||||
# the routed profile's own sources supply (build_profile_secret_scope folds them in).
|
||||
for name in secret_source_names():
|
||||
if not _is_global_env(name):
|
||||
base.pop(name, None)
|
||||
scope = current_secret_scope()
|
||||
if scope:
|
||||
base.update(scope)
|
||||
|
||||
@@ -432,3 +432,20 @@ class TestMultiplexContext:
|
||||
finally:
|
||||
ss.reset_secret_scope(scope_token)
|
||||
assert ss.refresh_installed_secret_scope(tmp_path) is False # nothing installed
|
||||
|
||||
|
||||
def test_refresh_drops_a_name_the_rebuild_no_longer_supplies(tmp_path, monkeypatch):
|
||||
"""refresh_installed_secret_scope must REPLACE the installed mapping, not merge into it: a
|
||||
rotated/revoked source value would otherwise survive for the rest of the fire (#107695 review)."""
|
||||
from agent import secret_scope
|
||||
|
||||
(tmp_path / ".env").write_text("KEPT=new\n", encoding="utf-8")
|
||||
token = secret_scope.set_secret_scope({"REVOKED_PLUGIN_TOKEN": "old", "KEPT": "stale"})
|
||||
try:
|
||||
assert secret_scope.refresh_installed_secret_scope(tmp_path) is True
|
||||
live = dict(secret_scope.current_secret_scope() or {})
|
||||
finally:
|
||||
secret_scope.reset_secret_scope(token)
|
||||
|
||||
assert live == {"KEPT": "new"}, live
|
||||
assert "REVOKED_PLUGIN_TOKEN" not in live
|
||||
|
||||
@@ -431,3 +431,39 @@ def test_a_routed_profile_script_never_receives_a_launch_profile_only_value(herm
|
||||
assert ok, output
|
||||
assert output.strip() == "routed|<unset>"
|
||||
assert os.environ["LAUNCH_ONLY_VALUE"] == "launch-only" # the parent process was not mutated
|
||||
|
||||
|
||||
def test_a_routed_profile_script_never_receives_a_launch_external_source_value(hermes_env, monkeypatch):
|
||||
"""External secret sources (vault, 1Password, ...) write their names into the shared
|
||||
``os.environ`` too, and ``strip_launch_profile_env`` only knows dotenv- and terminal-owned
|
||||
names. A name the LAUNCH profile's source supplied must still reach the routed child unset
|
||||
(#107695 review); a name the ROUTED profile's own source supplies must come through."""
|
||||
import os
|
||||
|
||||
from agent import secret_scope
|
||||
from cron.scheduler_script import _run_job_script
|
||||
from hermes_cli import env_loader
|
||||
from hermes_constants import get_process_hermes_home, reset_hermes_home_override, set_hermes_home_override
|
||||
|
||||
launch = get_process_hermes_home()
|
||||
routed = launch / "profiles" / "ops"
|
||||
(routed / "scripts").mkdir(parents=True, exist_ok=True)
|
||||
monkeypatch.setenv("LAUNCH_VAULT_ONLY", "launch-vault-value")
|
||||
monkeypatch.setitem(env_loader._SECRET_SOURCES, "LAUNCH_VAULT_ONLY", "vault")
|
||||
monkeypatch.setitem(env_loader._SECRET_SOURCES, "ROUTED_VAULT_ONLY", "vault")
|
||||
script = routed / "scripts" / "probe_vault.sh"
|
||||
script.write_text('#!/bin/bash\necho "${LAUNCH_VAULT_ONLY:-<unset>}|${ROUTED_VAULT_ONLY:-<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({"ROUTED_VAULT_ONLY": "routed-vault-value"})
|
||||
try:
|
||||
ok, output = _run_job_script("probe_vault.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() == "<unset>|routed-vault-value"
|
||||
assert os.environ["LAUNCH_VAULT_ONLY"] == "launch-vault-value" # parent untouched
|
||||
|
||||
Reference in New Issue
Block a user