fix(cron): scope the handoff multiplex context to the worker env build; gate the no_agent overlay

_launch_external_cron_worker wrapped fifty lines of dispatch, payload
write and scope hydration in the routed-fire multiplex context, though
only build_subprocess_env / strip_launch_profile_env read it. Compute
`multiplex_active` once (process flag OR routed fire), serialize it into
the payload, and set the context for exactly the env build inside the
existing secret-scope try/finally. Drop restore_managed_env on this path:
the worker re-runs load_hermes_dotenv -> _apply_managed_env at import
and strip_launch_profile_env already leaves managed keys in place.

_run_job_script now overlays the installed scope and re-applies managed
keys only under multiplex. In a single-profile process the scope is
os.environ, so the overlay could only re-sanitize values the child
already inherits; the "no-op outside multiplex" comment is now literal.
This commit is contained in:
kshitijk4poor
2026-09-14 23:43:27 +05:30
committed by kshitij
parent f367ebeb5d
commit 0fd59b447a
2 changed files with 63 additions and 60 deletions

View File

@@ -3187,72 +3187,74 @@ def _launch_external_cron_worker(job: dict) -> bool:
)
from cron.scheduler_provider import routed_profile_fire
from hermes_cli.env_loader import hydrate_profile_secret_sources
from tools.environments.local import build_subprocess_env, restore_managed_env, strip_launch_profile_env
from tools.environments.local import build_subprocess_env, strip_launch_profile_env
from tools.process_registry import (
restart_safe_gateway_child_argv,
systemd_user_bus_env,
)
context_token = (
set_multiplex_context(True) if routed_profile_fire() and not is_multiplex_active() else None
)
# A fire routed to another profile is multiplexed at this handoff even when the process flag is
# off (the desktop ticker is not a multiplexer): the payload says so, and the worker env is built
# under that context so the scrub and the launch-residue strip both apply. The context is set
# for exactly the env build; nothing else here reads it.
multiplex_active = is_multiplex_active() or routed_profile_fire()
try:
try:
require_restart_safe_scope = bool(
(load_config_readonly().get("cron") or {}).get("require_restart_safe_scope", False)
)
except Exception:
require_restart_safe_scope = False
multiplex_active = is_multiplex_active()
dispatch = restart_safe_gateway_child_argv(
command,
unit_suffix=f"cron-{job_id}-exec-{execution_id}",
require_restart_safe_scope=require_restart_safe_scope,
require_restart_safe_scope = bool(
(load_config_readonly().get("cron") or {}).get("require_restart_safe_scope", False)
)
if dispatch.mode == "in_process":
return False
except Exception:
require_restart_safe_scope = False
dispatch = restart_safe_gateway_child_argv(
command,
unit_suffix=f"cron-{job_id}-exec-{execution_id}",
require_restart_safe_scope=require_restart_safe_scope,
)
if dispatch.mode == "in_process":
return False
if mark_execution_handoff_pending(execution_id) is None:
raise RuntimeError(
"cron execution claim changed before external worker handoff"
if mark_execution_handoff_pending(execution_id) is None:
raise RuntimeError(
"cron execution claim changed before external worker handoff"
)
_ensure_cron_dir(handoff_dir)
try:
handoff_dir.chmod(0o700)
except OSError:
pass
fd = os.open(payload_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
try:
with os.fdopen(fd, "w", encoding="utf-8") as payload_file:
json.dump(
{
"job": job,
"profile_home": str(_get_hermes_home().resolve()),
"multiplex_active": multiplex_active,
},
payload_file,
)
payload_file.flush()
os.fsync(payload_file.fileno())
except BaseException:
payload_path.unlink(missing_ok=True)
raise
_ensure_cron_dir(handoff_dir)
try:
handoff_dir.chmod(0o700)
except OSError:
pass
fd = os.open(payload_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
try:
with os.fdopen(fd, "w", encoding="utf-8") as payload_file:
json.dump(
{
"job": job,
"profile_home": str(_get_hermes_home().resolve()),
"multiplex_active": multiplex_active,
},
payload_file,
)
payload_file.flush()
os.fsync(payload_file.fileno())
except BaseException:
payload_path.unlink(missing_ok=True)
raise
profile_home = _get_hermes_home().resolve()
hydrate_profile_secret_sources(profile_home)
secret_token = set_secret_scope(build_profile_secret_scope(profile_home))
try:
worker_env = restore_managed_env(strip_launch_profile_env(build_subprocess_env(
scrub_secrets=multiplex_active,
inherit_profile_home=True,
extra={"HERMES_HOME": str(profile_home)},
)))
finally:
reset_secret_scope(secret_token)
profile_home = _get_hermes_home().resolve()
hydrate_profile_secret_sources(profile_home)
secret_token = set_secret_scope(build_profile_secret_scope(profile_home))
context_token = set_multiplex_context(True) if multiplex_active and not is_multiplex_active() else None
try:
# No restore_managed_env here: the worker re-runs load_hermes_dotenv -> _apply_managed_env at
# import, and strip_launch_profile_env leaves managed keys in place.
worker_env = strip_launch_profile_env(build_subprocess_env(
scrub_secrets=multiplex_active,
inherit_profile_home=True,
extra={"HERMES_HOME": str(profile_home)},
))
finally:
if context_token is not None:
reset_multiplex_context(context_token)
reset_secret_scope(secret_token)
worker_env = systemd_user_bus_env(worker_env)
try:
process = subprocess.Popen(

View File

@@ -356,15 +356,16 @@ 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 current_secret_scope, is_multiplex_active
from tools.environments.local import restore_managed_env, strip_launch_profile_env
base = strip_launch_profile_env(dict(os.environ))
scope = current_secret_scope()
if scope:
base.update(scope)
# Administrator-managed values keep their precedence over the routed profile's own .env, exactly
# as they do in the launch process (``_apply_managed_env`` applies them last, with override).
restore_managed_env(base)
if is_multiplex_active():
# Single-profile: the scope IS os.environ, so overlaying it would only re-sanitize
# values the child already inherits byte-identical.
base.update(current_secret_scope() or {})
# Administrator-managed values keep their precedence over the routed profile's own .env,
# exactly as they do in the launch process (``_apply_managed_env`` applies them last).
restore_managed_env(base)
env = build_subprocess_env(base=base)
env.update(env_overlay)
# Subprocess cwd only (default: scripts-dir parent). NEVER os.chdir() the process.