fix(kanban): strip worker Kanban env from terminal-spawned subprocesses - #81843
fix(kanban): strip worker Kanban env from terminal-spawned subprocesses#81843DavidMetcalfe wants to merge 5 commits into
Conversation
A dispatcher-owned Kanban worker legitimately carries HERMES_KANBAN_* in its own env, but a nested `hermes` CLI it launches through the terminal tool inherits that identity and is accepted as the parent run owner — it can complete/block the parent's card while the real worker is still running (NousResearch#81508). The merged delegate_task isolation (NousResearch#56647/NousResearch#69837) only covers delegated children; a nested full CLI was never scrubbed. Strip HERMES_KANBAN_* unconditionally at the terminal-tool subprocess boundary: - _make_run_env (foreground terminal commands) - _sanitize_subprocess_env (background/PTY spawns via process_registry.spawn_local, watchers, cua-driver) - _scrub_child_env (execute_code sandbox), closing the passthrough escape hatch for plain worker-spawned children The non-terminal hermes_subprocess_env surface is intentionally unchanged: the codex app server / copilot ACP runtime is the worker's own execution surface and must retain HERMES_KANBAN_TASK to write completion/block back to the board. The dispatcher's own worker spawn (kanban_db._default_spawn) builds env explicitly and is unaffected. Adds strip_kanban_env() (strip without the delegated-child lineage marker) alongside scrub_kanban_env(), and regression tests covering the foreground, background, and execute_code boundaries plus the codex runtime preservation constraint.
Review finding (Flash, 2026-08-07): the scrub helpers' bare `except Exception: pass` fallback returned the env unscrubbed if agent.delegation_context could not be imported — the same fail-open pattern the NousResearch#70898 review flagged. Strip via HERMES_KANBAN_* prefix sweep instead so the dispatcher identity never reaches a terminal child even on import failure.
…er on plain runtime spawns
Spawn an actual child through _make_run_env and assert the dispatcher identity never crosses the process boundary — the mocked-Popen tests cover the env dict, this covers the real fork (NousResearch#81508).
Downstream dependency — what does this need to land?This PR is the narrow fix for the nested-subprocess ownership track (#70809), and it currently gates nterprise-ai/claudius#870, a P0 in our repository. From the outside it looks ready: open, mergeable, required CI green. Is there anything blocking it that a contributor could help with — review, additional regression coverage, or a rebase? Happy to supply either. Production impact while it is open, from a fleet running Hermes Kanban daily. The inherited-environment path has reproduced repeatedly: a nested That is exactly the class this PR closes by stripping The separate retry/old-worker overlap track (#71175, PR #71189) remains open too, and together they have produced duplicate concurrent implementations of the same task — two live workers, two PRs for one issue, one of which had to be closed as superseded while preserving its branch as evidence. We have not built any downstream workaround for either, deliberately: the fencing belongs in the runtime, and a local shim would only mask it. Full attempt fencing ( |
|
@SharadKumar — I appreciate the summary and the offer to help. Current status, answering directly:
This PR deliberately leaves I can rebase if needed and add any extra coverage you identify. |
|
I found a gap between the documented fail-closed prefix contract and the successful-import path on this HEAD ( Deterministic probe through the real In other words, the import-failure fallback strips every Suggested fix: make |
|
I turned the reproduced normal-path gap into a focused draft extension against your exact branch: DavidMetcalfe#1. It makes the nested terminal/execute-code boundary prefix-wide, while leaving your Codex/ACP runtime exception unchanged. Regression matrix now includes |
|
@enzo-adami — good catch. Verified and applied. Confirmed: Applied as cherry-pick of your commit
Verification:
|
fix(kanban): strip worker Kanban env from terminal-spawned subprocesses
Summary
A dispatcher-owned Kanban worker legitimately carries
HERMES_KANBAN_*in its own env, but a nestedhermesCLI it launches through the terminal tool inherits that identity and is accepted as the parent run owner — it can callkanban_complete/kanban_block/kanban_heartbeatagainst the parent's card while the real worker is still running. The mergeddelegate_taskisolation (#56647/#69837) only covers delegated children; a nested full CLI spawned through the terminal was never scrubbed.Production incident (reported in #81508): a read-only nested reviewer (
hermes -z --model … -t terminallaunched from a worker) inherited the parent's task/run env, appended a task comment, and completed the parent card. The owning worker's laterkanban_block(reason="review-required: …")was rejected because the card was already terminal.Root cause
Several subprocess env builders only stripped
HERMES_KANBAN_*when the spawner was adelegate_taskchild (is_delegated_child_process_context()):tools/environments/local.py::_make_run_env— terminal foreground commandstools/environments/local.py::_sanitize_subprocess_env— background/PTY spawns viaprocess_registry.spawn_local, watchers, cua-drivertools/code_execution_tool.py::_scrub_child_env— execute_code sandboxA plain worker-spawned nested process is not a delegated child, so none of these scrubbed — the nested CLI inherited the full dispatcher identity, and the env-based ownership gates (
tools/kanban_tools.py::_worker_run_id,_enforce_worker_task_ownership,model_tools.pykanban toolset auto-injection,turn_finalizer.py,session_context.py) all accepted it as the parent worker.Fix
Strip
HERMES_KANBAN_*unconditionally at the terminal-tool subprocess boundary, without setting theHERMES_DELEGATED_CHILD_CONTEXTlineage marker (a plain nested spawn is not a delegate_task child):strip_kanban_env()inagent/delegation_context.py(strip, no marker), withscrub_kanban_env()refactored on top of it (strip + marker for delegated children)._make_run_env,_sanitize_subprocess_env, and_scrub_child_envnow scrub unconditionally, closing the execute_code passthrough escape hatch for plain worker-spawned children.agent.delegation_contextcannot be imported, aHERMES_KANBAN_*prefix sweep still strips the identity rather than returning the env unscrubbed.Intentionally unchanged
hermes_subprocess_envkeeps its delegated-child-only scrub: it feeds the codex app server / copilot ACP runtimes, which are the worker's own execution surface and must retainHERMES_KANBAN_TASKto write completion/block back to the board (agent/transports/codex_app_server.py:102,agent/transports/hermes_tools_mcp_server.py).kanban_db._default_spawn) builds its env explicitly and is unaffected.Validation
TestKanbanNestedSpawnScrubintests/tools/test_local_env_blocklist.py(foreground / background / execute_code boundaries, passthrough cannot re-grant, no false delegated marker);test_non_delegated_worker_keeps_kanban_env_for_runtimeintests/tools/test_hermes_subprocess_env.py(codex runtime constraint);_legacy_posix_scrubberoracle updated intests/tools/test_code_execution_windows_env.pyper that file's own convention for deliberate POSIX scrub changes.pytestfocused suites: 259 passed, 3 skipped (kanban tools, delegation isolation, cron kanban env isolation, local env, process registry, terminal tool, code execution).tests/tools+ kanban DB/CLI suites: 5732 passed; 45 failures are pre-existing environment-dependent failures identical on unchangedmain(daytona/docker, pulseaudio, wake-word models) — zero new failures._make_run_envand_sanitize_subprocess_envsees zeroHERMES_KANBAN_*vars and no falseHERMES_DELEGATED_CHILD_CONTEXT.Notes
This PR provides the environment-level isolation fix for the nested-CLI ownership leak. The complementary database-level fix — four-field writer CAS (
status + claim_lock + worker_pid + current_run_id) fencing terminal transitions to the full attempt identity — is planned in #79543 / #81324 and hardens this boundary further, including the sibling stale-worker race in #71175. The existing open PR #70898 takes a broader ContextVar-based ownership approach; this PR intentionally keeps a smaller footprint and does not collide with the #79543/#81324 plan's declared ownership ofkanban_db.py.Closes #70809. Closes #81508 (duplicate).