fix(cron): isolate gateway approvals from environment pollution - #37969
fix(cron): isolate gateway approvals from environment pollution#37969coygeek wants to merge 5 commits into
Conversation
|
Observed this in a real Telegram gateway session as well, not just in tests. While handling a normal user message from Telegram,
The live tool environment for that turn contained both gateway/session markers and the leaked cron marker:
Config had This matches the PR description: A fix that moves the cron marker to scoped per-job context (or otherwise ensures gateway per-turn context wins over leaked process env) should address the observed failure. |
egilewski
left a comment
There was a problem hiding this comment.
Recommendation: request changes.
I reviewed this in security mode against current GitHub main 1e7316ced2261576bc4054aa915d3642ebd2b133, PR base ada04573a9669b92556788f2882feb3237753d03, and PR head bdb5611d0651493190af28f19344cfa4465c331a.
Validation:
git merge-tree --write-tree upstream/main refs/remotes/upstream/pr/37969: passed, wrote tree59fbc1628b886169efd10afd66d6d3d6f56e5529.git diff --check upstream/main...refs/remotes/upstream/pr/37969: passed.- Patch replay of the PR diff onto current main, then
python -B -m pytest -q tests/tools/test_cron_approval_mode.py -p no:cacheprovider: passed,26 passed. - Synthetic approval probe on the replayed patch with
HERMES_CRON_SESSION=1,set_session_vars(platform="telegram", session_key="ctx-session"), and noHERMES_GATEWAY_SESSION:check_dangerous_command("rm -rf /tmp/stuff", "local")andcheck_all_command_guards(...)both returned cron-modeBLOCKED, not gateway approval. - The same probe with legacy
HERMES_GATEWAY_SESSION=1returnedapproval_required/pending_approval, so the patch fixes only that env-flag path.
Finding:
The fix only gives precedence to the legacy process-global HERMES_GATEWAY_SESSION flag in tools/approval.py, but the messaging gateway binds live session identity through set_session_vars(...) contextvars in gateway/run.py. A stale process-global HERMES_CRON_SESSION=1 still short-circuits before _get_session_platform() is consulted, so a live Telegram/Discord/etc. gateway turn that relies on contextvars remains governed by approvals.cron_mode instead of user approval. The new tests cover only the legacy env flag, not this current gateway path.
Signed: GPT-5.5-xhigh in Codex
|
Addressed the review in I reproduced the broader contextvar/session path that
Validation:
|
egilewski
left a comment
There was a problem hiding this comment.
Recommendation: request changes
I reviewed this against current GitHub main d1383a6b1450c6c139720b1b01f8b99cc130453f, PR base ada04573a9669b92556788f2882feb3237753d03, and PR head 897363df847a14875676e5f23d87d1d2c5e73b27.
Validation:
git merge-tree --write-tree d1383a6b1450c6c139720b1b01f8b99cc130453f refs/remotes/pr/37969: passed.HOME=/tmp/hermes-review-pr/runs/37969-20260610T221530Z/home bash scripts/run_tests.sh tests/tools/test_cron_approval_mode.py tests/gateway/test_session_env.py tests/cron/test_scheduler.py: passed, 178 tests.- Direct approval-routing probe with
HERMES_GATEWAY_SESSION=1plus context-localcron_session="1": failed the cron invariant;check_all_command_guards("rm -rf /tmp/stuff", "local")returnedstatus=pending_approvalwith no cron-mode block.
Finding:
tools/approval.py still lets process-global gateway state override an actual context-local cron job. _is_gateway_approval_context() checks HERMES_GATEWAY_SESSION before _is_cron_session(), so a cron job running in a process that has the legacy gateway env marker set is treated as a live gateway approval context. That contradicts the function's cron contract and can route cron-dangerous commands into pending gateway approval instead of enforcing approvals.cron_mode.
CodeRabbit reported this, and I reproduced it with the direct probe above. It also pointed out that the temporary os.environ["HERMES_CRON_SESSION"] mirror is still capture/restored by each parallel job independently, so the env mirror can transiently disappear or leak when multiple cron jobs overlap. The ContextVar path is the right primary mechanism, but the legacy env mirror should not reintroduce the process-global race this PR is trying to remove.
Signed: GPT-5.5-xhigh in Codex
|
Addressed the latest requested-changes review in What changed:
Validation:
|
egilewski
left a comment
There was a problem hiding this comment.
Recommendation: request changes
I reviewed this against current GitHub main d62979a6f34f64f2ed840f159aac66e24d7cad78, PR base ada04573a9669b92556788f2882feb3237753d03, and PR head 725f2561cf03eba3da37bf8763ebacf1d5b1cf81.
Validation:
git merge-tree --write-tree upstream/main upstream/pr/37969: passed, produced575c3d95ab09725cfcb203acb5cb612ef4e7b001.git diff --check upstream/main...upstream/pr/37969: passed./home/mac/hermes-agent/.venv/bin/python -B -m pytest -o addopts='' -p no:cacheprovider tests/gateway/test_session_env.py tests/tools/test_cron_approval_mode.py tests/tools/test_execute_code_approval_cluster.py -q: failed 2 tests.- Direct probe after
set_session_vars(...); clear_session_vars(...)showed a later env-onlyHERMES_CRON_SESSION=1context returns{'approved': True, 'message': None}fromcheck_execute_code_guard(...)even withapprovals.cron_modepatched todeny.
Finding:
Adding HERMES_CRON_SESSION to the session ContextVar map makes clear_session_vars() set that ContextVar to an explicit empty string. Because get_session_env("HERMES_CRON_SESSION") does not fall back to os.environ after an explicit ContextVar value exists, any later legacy/env-only cron context in the same Python context is treated as non-cron. That breaks the existing execute-code cron-deny contract: test_guard_cron_deny_blocks now approves the code path, and test_execute_code_entry_blocks_before_spawn_when_guard_denies can execute successfully instead of returning the cron BLOCKED error.
Please preserve the stale-env suppression for live gateway turns without globally disabling env-only cron detection after session context cleanup.
Signed: GPT-5.5-xhigh in Codex
teknium1
left a comment
There was a problem hiding this comment.
Thanks for tracing the gateway/cron contamination to a real process-global marker. Current main still sets HERMES_CRON_SESSION globally in cron/scheduler.py:2685-2688, and approval routing checks that marker before the gateway ContextVar path in tools/approval.py:194-198, so the fix direction is valid.
Problems
gateway/session_context.py:164adds_CRON_SESSIONtoclear_session_vars(). That cleanup writes"";get_session_env()explicitly treats a set empty ContextVar as authoritative and does not fall back toos.environ(gateway/session_context.py:304-315). After any cleared session context, a later legacy env-onlyHERMES_CRON_SESSION=1is no longer recognized as cron. This breaks the compatibility contract exercised by the existing env-only cron-deny tests.
Suggested changes
- Separate cron scope cleanup from ordinary gateway-session cleanup, then add a regression for clear-session → env-only cron → cron-deny, including
check_execute_code_guard(). - Apply the corrected change against current
cron/scheduler.py:2685-2719; the target moved since this PR's base.
Automated hermes-sweeper review.
Give explicit gateway sessions precedence over the process-wide cron marker when classifying approval context. This keeps cron-origin jobs on cron approval policy while preventing stale scheduler state from disabling live gateway approval prompts.
Bind cron approval state to the active session context so leaked scheduler env does not hijack gateway approvals. Restore the legacy env marker after each job and cover the gateway, execute_code, and scheduler cleanup regressions. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Make context-local cron state authoritative over stale gateway env markers, and stop run_job from writing HERMES_CRON_SESSION into process-global env for each cron job. Propagate only ContextVars across delegate subagent thread boundaries so delegated cron work keeps cron approval policy without overriding the subagent approval callback.
725f256 to
1fd2a7c
Compare
|
Rebased onto current The cron marker now has separate scope semantics from ordinary gateway-session cleanup: I also closed the adjacent cron-policy bypass identified on the competing implementation: inherited Verification:
The PR body now contains the exact commands and the isolated full-scheduler-file caveat. |
|
CI slice 4 exposed one compatibility edge in the new scoped cleanup: several legacy/mock gateway paths call Fixed in Verification:
The updated push has started a fresh CI run; the previous slice-4 failure is superseded by this commit. |
|
suggesting changes CI is currently failing on this PR head in Please fix or rerun the failing check, then push a new head or ask for re-review. Signed: GPT-5.6-terra-low in Codex |
|
CI slice 8 exposed two stale test seams in Fixed in Verification:
The second red check, |
|
looks mergeable Security evidence:
Signed: GPT-5.6-sol-xhigh in Codex |
|
Merged via #77022 using @hinablue's implementation from PR #43370, which was the most thorough of the competing approaches. Your PR identified the correct bug and proposed a valid fix; the merged version uses a ContextVar-based approach instead of env var save/restore for stronger session isolation. Thank you for reporting and contributing! |
Summary
The in-process cron scheduler must not place approval policy in process-global state. A persistent
HERMES_CRON_SESSION=1causes later interactive gateway turns to useapprovals.cron_mode; deny mode blocks a live user's command, while approve mode can bypass the expected gateway approval prompt.This PR now:
ContextVarand restores its exact prior state with its tokenos.environfallback intact for standalone and compatibility entrypointsHERMES_EXEC_ASKflagCloses #37968
Root cause
cron/scheduler.pysetHERMES_CRON_SESSIONinos.environ, even though the default gateway hosts the cron ticker and interactive sessions in the same process. Approval readers intools/approval.pytherefore could not distinguish the job that set the marker from unrelated live turns.Verification
Rebased onto current
mainat477c08b44766ace8b890faa72bf82ecbcf2b3ba8.scripts/run_tests.sh tests/gateway/test_session_env.py tests/tools/test_cron_approval_mode.py tests/tools/test_delegate.py -q— 214 passedtest_session_env,test_incomplete_gateway_turns,test_stacked_skill_platform_disabled,test_telegram_topic_mode) — 69 passedscripts/run_tests.sh tests/cron/test_scheduler.py -q -k run_job_keeps_cron_session_env_unchanged— 1 passed.venv/bin/ruff checkon all eight changed files — passed.venv/bin/python scripts/check-windows-footguns.pyon all eight changed files — passedgit diff --check origin/main...HEAD— passedThe full
tests/cron/test_scheduler.pyfile also reached 213 passing tests, but ten unrelated tick tests currently fail while opening the executions SQLite path in the isolated test environment; the changed scheduler regression passes independently as shown above.Risk
This changes approval-context routing. The marker remains visible inside each cron job and its delegated workers, but is no longer written into process-global environment state. Cleanup uses token reset rather than an empty-string sentinel so later env-only cron detection continues to work.