fix(security): close kanban worker auto-approve gap for dangerous commands - #55946
fix(security): close kanban worker auto-approve gap for dangerous commands#55946webdevtodayjason wants to merge 3 commits into
Conversation
6fd63eb to
ef3ff3f
Compare
teknium1
left a comment
There was a problem hiding this comment.
Thanks for addressing the unattended Kanban-worker path. The fail-closed policy direction is useful, but the current implementation does not reach that policy for the actual worker process.
Problems
hermes_cli/kanban_db.py:8021-8048launcheshermes ... chat -q, whilecli.py:15834-15836unconditionally setsHERMES_INTERACTIVE=1. The new Kanban branch is nested under_run_approval_gate()'snot is_cli and not is_gatewaycondition (tools/approval.py:2092-2097), so the terminal/plugin path skips it. The new tests delete that marker and call guards directly, rather than exercising worker startup._default_spawn()still copies all parent environment state (hermes_cli/kanban_db.py:7931). Inherited gateway/ask/cron state can preempt the new policy;check_execute_code_guard()handles cron before the proposed Kanban branch (tools/approval.py:3018-3038). This matches the detached-callback issue documented in linked #63183.
Suggested changes
- Make Kanban worker identity/policy explicit and higher precedence than ambient CLI/gateway/cron state, and add a spawn-to-guard regression test seeded with those markers.
- Document
approvals.kanban_modebeside the existing approvals keys inwebsite/docs/user-guide/security.md:32-48.
Automated hermes-sweeper review.
| @@ -7727,6 +7727,12 @@ def _default_spawn( | |||
| if task.tenant: | |||
| env["HERMES_TENANT"] = task.tenant | |||
| env["HERMES_KANBAN_TASK"] = task.id | |||
| # Kanban workers run unattended (no user present to approve a dangerous | |||
| # command) -- flag the context so tools/approval.py applies the same | |||
| # deny-by-default policy cron jobs already get via HERMES_CRON_SESSION, | |||
There was a problem hiding this comment.
_default_spawn() still begins from dict(os.environ), so HERMES_EXEC_ASK, gateway/cron markers, and process-level YOLO remain in the child. Those markers can select an existing approval path before this new Kanban marker is consulted; please establish an explicit worker context and test seeded parent markers.
| @@ -2103,6 +2119,17 @@ def _run_approval_gate( | |||
| "description": description, | |||
There was a problem hiding this comment.
This branch is only reached after _run_approval_gate() has classified the caller as neither CLI nor gateway. A dispatcher worker runs hermes chat -q, and cli.py:15836 sets HERMES_INTERACTIVE=1 before processing that query, so the actual terminal/plugin worker path bypasses this branch. Make Kanban policy take precedence or ensure worker startup has an explicit noninteractive context.
There was a problem hiding this comment.
Good catch, that was exactly the hole. You're right that cli.main exports HERMES_INTERACTIVE=1 before the query runs, so the kanban branches were dead code for real workers, and _default_spawn copied the dispatcher's whole environment on top of that.
Fixed on the current head (df8cc2df5, "enforce kanban worker approval policy on the real worker path"). Three coordinated changes: HERMES_KANBAN_SESSION is now decided before any ambient context inference in all three gates with both arms returning, the spawn env scrubs the seven inherited markers alongside the existing HERMES_TUI pop, and worker startup no longer self-marks interactive when the kanban marker is set.
tests/tools/test_kanban_worker_real_path.py drives the real _default_spawn env through the real gate entry points: 12 of 14 cases fail on the old code and all pass now, existing approval suites unchanged. An independent security review also reproduced the bypass on current main (a real-spawn probe inherited yolo/gateway state and auto-approved rm -rf) and confirmed the reviewed head scrubs that state and denies the dangerous command, execute_code, and plugin escalation while still honoring an explicit kanban approve mode. Branch is rebased clean on current main now if you want another look.
|
Both problems are fixed in 7aaf2d43f. You were right that the policy was dead code on the real worker path. What changed: HERMES_KANBAN_SESSION is now decided before the ambient is_cli/is_gateway/is_ask inference and before the cron marker in all three gates (_run_approval_gate, check_all_command_guards, check_execute_code_guard), with both arms returning so ambient state can never reroute a worker. _default_spawn scrubs the seven inherited context markers (HERMES_INTERACTIVE, HERMES_GATEWAY_SESSION, HERMES_EXEC_ASK, HERMES_CRON_SESSION, HERMES_SESSION_PLATFORM, HERMES_SESSION_KEY, HERMES_YOLO_MODE) alongside the existing HERMES_TUI pop, and worker startup no longer self-marks HERMES_INTERACTIVE when the kanban marker is set, so a stdin=DEVNULL child stops advertising a prompt surface. Worker-local --yolo stays an explicit operator opt-in per the existing test; INHERITED parent yolo is neutralized by the spawn scrub, which lines up with #63183. Testing: new tests/tools/test_kanban_worker_real_path.py (14 tests) builds the worker env with the real _default_spawn and drives the real gate entry points instead of deleting markers and calling guards directly. On the previous code 12 of 14 fail, for exactly your P1 and P2; all 14 pass with the fix. A human-surface spy asserts a worker is never routed to a prompt, smart approval, or a pending queue. Existing suites pass unchanged: test_kanban_approval_mode 17/17, test_execute_code_approval_cluster 24/24, test_approval 303/303. Full targeted sweep: 1498 passed, 0 failed. Three pre-existing test bugs surfaced during that sweep (verified pre-existing at the prior HEAD in a clean worktree) are fixed in the same commit and called out in the message. |
|
not enough evidence I couldn't establish a coherent review tree for this security-boundary change. The deterministic replay of Review setup: I reviewed a run-owned local rebase or patch replay against current GitHub Signed: GPT-5.6-sol-xhigh in Codex |
7aaf2d4 to
77036d9
Compare
|
Rebased onto current main (fea838c). The single conflict was comment-only in |
77036d9 to
df8cc2d
Compare
|
looks mergeable The PR closes the verified kanban-worker approval-context gap on the real spawn path. It gives workers an explicit unattended marker, removes inherited interactive, gateway, cron, session-cache, and yolo markers, prevents quiet worker startup from reasserting interactivity, and gives the terminal, plugin-escalation, and execute_code gates kanban-specific deny-by-default handling with an explicit profile-scoped approve override. A current-main snapshot reproduced inherited yolo/gateway state and auto-approval of a dangerous local command; the same real-spawn probe against the reviewed head scrubbed that state, denied the dangerous command, denied execute_code and plugin escalation, allowed a safe command, and honored explicit kanban approve mode. No source-backed residual bypass was found in the reviewed execution paths. Security evidence:
Uncertainty: CodeRabbit completed and raised two minor suggestions, but source adjudication found no production bypass: one is redundant on the real spawn path and one concerns only test-state isolation. The focused probe intercepted Popen and validated the exact constructed child environment and real gate functions; it did not launch a model-backed worker subprocess end to end. Signed: GPT-5.6-sol-xhigh in Codex |
df8cc2d to
37f4df0
Compare
|
looks mergeable The PR closes the verified kanban-worker approval-context gap on the real spawn path. It gives workers an explicit unattended marker, removes inherited interactive, gateway, cron, session-cache, exec-ask, and yolo markers, prevents quiet worker startup from reasserting interactivity, and gives the terminal, plugin-escalation, and execute_code gates kanban-specific deny-by-default handling with an explicit profile-scoped approve override. A current-main reproduction again showed inherited worker context approving a dangerous local command; the reviewed replay scrubbed that state, denied dangerous commands, execute_code, and plugin escalation without touching a human-approval surface, allowed a safe command, and retained the intended explicit approve mode. No source-backed residual bypass was found in the reviewed execution paths. Security evidence:
Not checked:
Signed: GPT-5.6-sol-xhigh in Codex |
…mands Kanban-dispatched worker subprocesses fall through every approval-context check in tools/approval.py and silently auto-approve any non-hardline dangerous command, with only a logger.warning() left behind. Same bug class as NousResearch#30882 (P0, fixed in NousResearch#34497) -- an execution surface the approval system doesn't recognize defaults to trusted-by-omission instead of trusted-by-explicit-config. Unlike NousResearch#30882 this is a plain omission, not a regression: kanban workers are separate subprocesses (not threads sharing contextvars), and the approval-context question was simply never wired in when kanban dispatch was built. hermes_cli/kanban_db.py's worker-spawn env construction sets 15+ env vars (HERMES_KANBAN_TASK, HERMES_KANBAN_WORKSPACE, etc.) but never any of the four flags tools/approval.py checks for a non-interactive context (HERMES_CRON_SESSION, HERMES_GATEWAY_SESSION, HERMES_SESSION_PLATFORM, HERMES_INTERACTIVE), so all three approval-gate functions (check_dangerous_command, check_all_command_guards, check_execute_code_guard) fall through to bare auto-approve. Mirrors the proven cron_mode pattern exactly: - HERMES_KANBAN_SESSION env flag set at worker spawn time - approvals.kanban_mode config (default deny, matching cron_mode) - _get_kanban_approval_mode() mirrors _get_cron_approval_mode() verbatim - A parallel deny-by-default branch added to all three call sites, alongside the existing cron branch -- not replacing it approvals.kanban_mode: approve remains available for users who've deliberately decided to trust their kanban workers, same as cron_mode. Verified against the real, unmodified functions (not mocked): captured the actual env _default_spawn() builds for a real task in a scratch kanban DB (subprocess.Popen intercepted, not launched), confirmed all four approval flags were absent, then fed that exact env into a fresh process calling the real check_dangerous_command() -- confirmed auto-approved pre-fix, blocked post-fix, and confirmed kanban_mode: approve restores opt-in pass-through. kanban_mode lives under approvals (not the kanban: config block) -- that block has a known, separately-filed duplicate-key bug (NousResearch#55779) that silently drops the first of two "kanban" keys in the same DEFAULT_CONFIG dict literal. Fixes NousResearch#55945
…er path The kanban policy branches were dead code for real workers: cli.main exports HERMES_INTERACTIVE=1, so a spawned `hermes ... chat -q` worker took the interactive path before ever reaching them, and _default_spawn copied the dispatcher's full environment, letting inherited gateway/ask/cron markers reroute worker approvals into prompts and pending queues no one watches (NousResearch#63183). Three coordinated changes: HERMES_KANBAN_SESSION is decided before all ambient context inference in the three gates, with both arms returning; the spawn env scrubs the seven ambient markers alongside the existing HERMES_TUI pop; worker startup no longer self-marks interactive when the kanban marker is set. tests/tools/test_kanban_worker_real_path.py builds the worker env with the real _default_spawn and drives the real gate entry points: 12 of 14 cases fail on the previous code, all pass now. Existing approval suites pass unchanged. Also fixes three pre-existing test bugs surfaced by the full sweep, each verified pre-existing at ef3ff3f15: a /tmp symlink hardcode, a missing darwin zombie probe in a _pid_alive test mirror, and a timing flake under parallel load. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnMCvi2vXqfs996AjVeF2F
37f4df0 to
51e8c22
Compare
… env-isolation gate The completeness test added upstream force-classifies every dispatcher kanban env var as identity (scrubbed from delegated children) or behaviour-only (inherited). The unattended-session marker must be inherited: a delegate_task child of a kanban worker is still unattended, so scrubbing the marker would reopen the dangerous-command auto-approve gap one fork deeper.
|
suggesting changes The PR closes the direct kanban-worker auto-approval gap, but the new raw HERMES_KANBAN_SESSION checks do not honor the existing non_dispatcher_owned_context used by in-process cron jobs. A kanban worker can therefore run a cron job under kanban_mode: approve even when cron_mode: deny, allowing dangerous terminal commands and execute_code.
Security evidence:
Not checked:
Signed: GPT-5.6-luna-max in Codex |
What
Kanban-dispatched worker subprocesses — the primary execution vehicle for long-horizon, unattended multi-agent work in Hermes — fall through every approval-context check in
tools/approval.pyand silently auto-approve any non-hardline dangerous command, with only alogger.warning()left behind.Why
Same bug class as #30882 (P0, fixed in #34497): an execution surface the approval system doesn't recognize defaults to trusted-by-omission instead of trusted-by-explicit-config. That fix's own description states the philosophy plainly: "The approval gate is a documented heuristic in SECURITY.md, not a security boundary; this restores its intended behavior — it doesn't add one."
Unlike #30882 (a ContextVar-propagation regression in threads), this is a plain omission, not a regression — kanban workers are separate
subprocessprocesses (not threads sharing contextvars), and the approval-context question was simply never wired in when kanban dispatch was built.hermes_cli/kanban_db.py's worker-spawn env construction (_default_spawn) sets 15+ environment variables (HERMES_KANBAN_TASK,HERMES_KANBAN_WORKSPACE, etc.) but never any of the four flagstools/approval.pychecks to recognize a non-interactive context:HERMES_CRON_SESSION,HERMES_GATEWAY_SESSION,HERMES_SESSION_PLATFORM,HERMES_INTERACTIVE. All three approval-gate functions (check_dangerous_command,check_all_command_guards,check_execute_code_guard) therefore fall through to the bare non-interactive auto-approve branch.Change
Mirrors the proven
cron_modepattern exactly — not a new mechanism:HERMES_KANBAN_SESSIONenv flag set at worker spawn time (hermes_cli/kanban_db.py)approvals.kanban_modeconfig, defaultdeny(matchingcron_mode's default)_get_kanban_approval_mode()mirrors_get_cron_approval_mode()verbatimtools/approval.py, alongside the existing cron branch — not replacing itapprovals.kanban_mode: approveremains available for users who've deliberately decided to trust their kanban workers, same ascron_modekanban_modelives underapprovals(not thekanban:config block) — that block has a known, separately-filed duplicate-key bug (#55779) that silently drops the first of two"kanban"keys in the sameDEFAULT_CONFIGdict literal.How to test
Verified against the real, unmodified functions (not mocked) before writing any fix code:
test_kanban_approval_mode.pypass; all pre-existing cron/CLI/gateway tests across the 4 touched test files stay byte-stable when run per-file (matchingscripts/run_tests.sh's per-file isolation).scripts/check-windows-footguns.pyclean on the diff (pure env-var/config logic).Platforms tested
macOS (logic-only change, no OS-specific code paths — pure env-var/config string matching).
Note
While testing I found an unrelated, pre-existing test-isolation issue (
test_cron_approval_mode.py+test_approval.pyleak shared module state when run together in onepytestprocess — confirmed on unmodifiedmaintoo, invisible to CI becausescripts/run_tests.shisolates each test file into its own subprocess). Filed separately, not part of this diff.Fixes #55945