Repository navigation
fix(gateway): bind session context at agent turn entry (cross-session identity misbinding) - #271
Merged
Merged
Conversation
Reset and bind every _run_agent invocation from its own turn arguments before executor context copying, then restore nested ContextVar scopes with tokens. Add worker/subprocess regressions for direct, queued, steer, multiplex-profile, mismatch-warning, and A-to-B-to-A nesting paths. Verified 574 targeted tests and ruff.
Kyzcreig
enabled auto-merge
July 10, 2026 19:55
This was referenced Jul 10, 2026
Kyzcreig
added a commit
that referenced
this pull request
Jul 11, 2026
…tch net Follow-up to #271 (turn-entry binding). Tonight's live evidence (2026-07-10, runtime f61bcb6): an interactive turn ran its terminal subprocesses with a COMPLETE foreign session identity while the #271 entry warning stayed silent — so the corruption enters between the async-task bind and the subprocess spawn, or the entry warning's placement can't see it. RC3 diagnosis (spec 2026-07-10, §4): deterministically POISON a reused size-1 worker thread with a foreign binding and assert on the REAL subprocess-env surface (_make_run_env/_inject_session_context_env) from inside the worker: - gateway executor seam (_run_in_executor_with_context): ctx.run overrides poisoned thread — PROVEN - tool worker seam (propagate_context_to_thread): ctx.run overrides poisoned daemon worker — PROVEN Both wrapped seams are therefore RULED OUT as the live mechanism. The live leak path remains UNEXPLAINED (honest); prime remaining suspect is the shared LocalEnvironment snapshot / an unwrapped submit not yet found. Runtime net (the actionable half): _warn_on_tool_submit_session_mismatch at the tool-batch submit seam — compares the executing agent's _gateway_session_key against the bound HERMES_SESSION_KEY at the exact point tonight's leak was visible. Next occurrence logs 'Tool executor context mismatch' with both keys instead of silently misrouting. + test. 3 new tests (2 bidirectional residue gates + warning test); 26 pass across session-context + env-leak suites; 36 pass tool suites.
Kyzcreig
added a commit
that referenced
this pull request
Jul 11, 2026
…shot THE 2026-07-10 cross-session misbinding mechanism — finally pinned by a second live occurrence (a hermetic test's HERMES_HOME=/tmp/dw_e2e/home surfacing in a different session's interactive shell). Mechanism: the persistent env snapshot (export -p dump, re-sourced by every subsequent command in the environment) outlives a single turn, and the gateway's 'default' LocalEnvironment is SHARED across sessions: 1. Session B's turn runs a command; the end-of-command re-dump writes B's injected HERMES_SESSION_* (+ any HERMES_HOME export) into the snapshot. 2. Session A's next command: per-spawn injection puts A's identity into the Popen env — then the wrapped script SOURCES the snapshot, whose 'declare -x HERMES_SESSION_KEY=B' lines CLOBBER the injection. 3. A's command ends; re-dump persists B's identity again — self-perpetuating until someone exports over it. This explains every observation from the day's hunt: coherent complete foreign identity in terminal subprocess env; ContextVars correct (#271's and #281's warnings silent — they watch the ContextVar surface, which is fine; only the sourced-over child env is poisoned); fresh shells inheriting it immediately (they source the same snapshot); cross-HOME contamination from hermetic tests. Fix: pipe both snapshot dumps (init_session bootstrap + per-command re-dump) through a grep -vE filter excluding HERMES_SESSION_* and HERMES_HOME. These vars are per-spawn-injected (_inject_session_context_env / _inject_context_hermes_home) and the injection must be authoritative; everything else still persists (the snapshot's purpose is unchanged). Tests: tests/tools/test_env_snapshot_session_leak.py — 4 new: the exact incident sequence through a REAL LocalEnvironment (B exports identity -> snapshot clean -> A's command sees EMPTY), persistence-purpose preserved (non-session vars still roundtrip), bootstrap dump filtered (gateway process env can't seed), regex contract (both declare -x/export forms; lookalikes kept). Mutation-checked: filter neutered => incident + boot tests red. 3 existing test_base_environment assertions updated to the new dump shape. Known pre-existing red (NOT this change): test_concurrent_writes_never_ tear_the_snapshot fails 3/3 on clean fork/main baseline (hand-rolled shell repro, does not exercise this code path).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the live, thrice-reproduced cross-session identity misbinding: an interactive turn could execute with the COMPLETE coherent identity (HERMES_SESSION_CHAT_ID/THREAD_ID/KEY/ID/MESSAGE_ID) of a different live session, causing real misroutes (kanban notify-subscribe subscribed a foreign Discord thread's chat id).
Root cause
_run_agentnever enforced its own identity contract. The normal message handler binds session vars, but the in-band queued-follow-up path RECURSES into_run_agentdirectly — bypassing the handler — so the follow-up turn inherits the outer/sibling turn's coherent binding, which then propagates into the worker thread viacopy_context()and out to every tool subprocess.Fix (spec: Momus-reviewed, 2 passes)
_run_agentnow resets inherited state and bindssource/session_key/session_id/event_message_idfrom its OWN args before local or proxy execution — before any workercopy_context().restore_session_vars), never reset-to-UNSET — the A→B→A regression proves outer identity survives inner exit.copy_context().run; regression uses a single-worker executor across A→B→A.Tests
_run_in_executor_with_context→ subprocess env view), not MainThread ContextVars (which read _UNSET while the live bug was active — the fake-green trap).Cache safety
No prompt/messages/tool-schema/cache-key behavior changed — context binding only.