From 0fd59b447a024adfcc9fa9c298d55bcd84f7b3f4 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Mon, 14 Sep 2026 23:43:27 +0530 Subject: [PATCH] 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. --- cron/scheduler.py | 108 ++++++++++++++++++++------------------- cron/scheduler_script.py | 15 +++--- 2 files changed, 63 insertions(+), 60 deletions(-) diff --git a/cron/scheduler.py b/cron/scheduler.py index 85f5a053db..30d412a8ab 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -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( diff --git a/cron/scheduler_script.py b/cron/scheduler_script.py index cb839a5958..1c1743e3e9 100644 --- a/cron/scheduler_script.py +++ b/cron/scheduler_script.py @@ -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.