Skip to content

fix(kanban): enforce safe worker execution and authority - #94009

Closed
benperry6 wants to merge 24 commits into
NousResearch:mainfrom
benperry6:ben/kanban-safe-candidate-20260824
Closed

fix(kanban): enforce safe worker execution and authority#94009
benperry6 wants to merge 24 commits into
NousResearch:mainfrom
benperry6:ben/kanban-safe-candidate-20260824

Conversation

@benperry6

Copy link
Copy Markdown
Contributor

Summary

  • deny dangerous terminal/Tirith, execute_code, and plugin escalation for unattended Kanban workers by default while preserving safe operations and cron-owned policy
  • require successful material evidence from the current run across Hermes, Codex app-server, MCP, and cross-process completion
  • separate progress from liveness and revalidate expiry under the SQLite writer lock
  • prevent child chats from inheriting parent lifecycle ownership; authenticate native launches with a consumed one-shot nonce
  • persist exact Linux worker birth/process-group identity, retain fences through handoff, terminate by process group, and release successors only after full quiescence with full-state CAS

Integration

Explicit exclusions

Verification

All Python tests used the canonical scripts/run_tests.sh wrapper.

  • Linux authority: 36 passed
  • focused auth/evidence/nonce: 202 passed
  • focused progress/review/retry/writer collision: 206 passed
  • deterministic leading-dash nonce regression, retries disabled: 24 passed
  • broad Kanban/gateway/tools/approval sweep, retries disabled: 1071 passed, 1 OS skip
  • every modified Python test file, retries disabled: 964 passed, 1 OS skip
  • git diff --check origin/main...HEAD: passed

One broad run exposed a leading-dash nonce argparse flake; the source was fixed by binding the nonce as --kanban-worker-launch=<value>, then both the focused regression and the full broad sweep passed with retries disabled.

Not run

No live activation or live Kanban canary was run in this worktree.

benperry6 and others added 24 commits August 22, 2026 21:45
Co-Authored-By: Hermes Agent / OpenAI Codex / GPT-5.6 Sol <noreply@agents.invalid>
`hermes auth reset <provider>` printed "Reset status on N credentials" and left
the pool exactly as it was. Measured on a live install: an entry benched with
402 / Insufficient Balance kept `last_status: exhausted` and the same
`last_status_at`, read back in a fresh process immediately after the reset, and
the CLI went on refusing the provider with "No usable credentials found" long
after the account had been topped up.

Two causes, one per file.

write_credential_pool keeps a newer on-disk cooldown over the caller's snapshot
so one process cannot resurrect a key another has just rate-limited. That merge
compares `last_status_at`, and clearing sets it to None, which parses as epoch
0 — older than any real timestamp. So a deliberate operator reset was
indistinguishable from the stale snapshot the merge exists to reject, and the
cooldown was copied straight back over the cleared fields.

The failure profile made it hard to see: `_merge_disk_cooldown_state` returns
early for a cooldown that has already expired, so a reset appeared to work
whenever it did not matter and silently did nothing whenever it did.

reset_statuses also never cleared `failure_reason`. It lives in `extra` rather
than as a dataclass field, so `replace()` could not reach it, and an entry came
out of a reset with no status and no error code but still classified `billing` —
which `hermes auth list` renders as though it were current.

The fix threads the caller's intent through, mirroring the `removed_ids`
parameter that already exists one concern over for the same reason: an id in
`status_cleared_ids` says "I have seen the newer status and I am dropping it",
as opposed to "I have not seen it". Anything that does not declare the intent
keeps the old, protective behaviour.

Tests: three in tests/agent/test_credential_pool.py. The cooldown in the two
regression tests is deliberately recent, because an expired one passes with or
without the fix. Verified by reverting the source change with the tests kept —
both regression tests fail and the third keeps passing, so it is known to pin
the concurrency guard rather than the new flag. The guard test exists because
"skip the merge always" would have made these two pass while reintroducing the
lost update the merge was written to prevent.

204 passed, 2 skipped across tests/agent/test_credential_pool.py and
tests/hermes_cli/test_auth*.py.
Preserve the five carried fixes while adopting current upstream behavior.\n\nCo-Authored-By: GPT-5.6 SOL <noreply@openai.com>
…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
… 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.
Treat in-process cron jobs as independently owned approval contexts while keeping delegated children inside the parent worker policy. Cover the scheduler ContextVar thread hop in both policy directions.

Co-Authored-By: GPT-5.6 SOL <noreply@openai.com>
The new mixed-context test called clear_session_vars in its finally block.
That pins every session var to "" which is the "explicitly cleared" state,
and get_session_env treats that as authoritative and stops falling back to
os.environ for the rest of the process.

test_cron_session_unaffected_by_kanban_changes sets HERMES_CRON_SESSION as a
plain env var, so once the mixed-context test had run, _is_cron_approval_context
returned False there and the dangerous command auto-approved. Passed alone,
failed when the two files ran together.

reset_session_vars restores the _UNSET sentinel instead, which is what a test
teardown wants: it leaves the context looking like the vars were never bound,
so the env fallback works again.
benjaminperry6@yahoo.fr -> benperry6, who authored the cron-policy follow-up
on this PR. The attribution check fails on any commit email it cannot resolve
to a GitHub account, and that address is not public on his profile.
Retain the contributor's protocol-violation DB foundation while the current-run receipt implementation follows separately. Generated Paperclip evidence and stale file snapshots are excluded.\n\nCo-Authored-By: GPT-5.6 SOL <noreply@openai.com>
Persist one metadata-only tool receipt per current task run so both native Hermes and Codex app-server workers can prove successful material work before completion. Reject failed results, preserve the running task, and close the cross-process notification race with a bounded read.

Co-Authored-By: GPT-5.6 SOL <noreply@openai.com>
Track one deterministic progress lease independently from stream/activity
liveness. Claims, verified tool executions, and durable board transitions
renew progress; model output and heartbeat commentary do not.

An expired lease is detection-only in this commit: retain the exact task,
claim, PID, and run, emit a bounded no_progress_deferred receipt, and never
signal, requeue, or count failure without a separate settlement authority.
Propagate the timeout and passive receipt through dispatcher, CLI, gateway,
dashboard, Desktop/TUI notifications, diagnostics, config, and docs.
Nested hermes chat --source tool from a Kanban worker inherited
HERMES_KANBAN_TASK and could kanban_complete the parent card. Strip
lifecycle ownership from subprocess env and drop it at chat startup
unless the process is the dispatcher worker.
Port only the exact Linux birth/group identity, retained fences, process-group quiescence, full-state CAS, stale-attempt refusal, and dashboard writer guard required to prevent bypass. Exclude stale parser, config, main, TUI, broad dashboard, and generated snapshots.\n\nCo-Authored-By: GPT-5.6 SOL <noreply@openai.com>
Model the dispatcher lifecycle in tool tests by supplying synthetic Linux worker identity, releasing quiesced fences, and recomputing successors on the next simulated tick.\n\nCo-Authored-By: GPT-5.6 SOL <noreply@openai.com>
Keep the progress implementation from 95fe96f while supplying exact synthetic worker identity to lifecycle tests and explicitly clearing it for custom/remote spawn cases.\n\nCo-Authored-By: GPT-5.6 SOL <noreply@openai.com>
Route lifecycle fixtures through exact synthetic identity, retain complete authority tuples during reclaim, and assert fail-closed behavior for forged dashboard claims.\n\nCo-Authored-By: GPT-5.6 SOL <noreply@openai.com>
Pass the one-shot worker nonce with --kanban-worker-launch=<value> so urlsafe values beginning with '-' cannot be misparsed as another option.\n\nCo-Authored-By: GPT-5.6 SOL <noreply@openai.com>
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/cron Cron scheduler and job management comp/desktop Electron desktop app (apps/desktop/*) comp/gateway Gateway runner, session dispatch, delivery labels Aug 24, 2026
@alt-glitch alt-glitch added comp/plugins Plugin system and bundled plugins comp/tools Tool registry, model_tools, toolsets tool/mcp MCP client and OAuth tool/terminal Terminal execution and process management area/auth Authentication, OAuth, credential pools P3 Low — cosmetic, nice to have needs-decision Awaiting maintainer decision before any implementation sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 24, 2026
@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

Diff too large for automated batch review (95 changed files, +11063/-890) — recommending manual human review.

@benperry6

Copy link
Copy Markdown
Contributor Author

Closing this draft because it bundled several independent Kanban fixes together with unrelated carried commits, which made the change impractical to review.

The relevant work remains available on the existing issue-specific PRs:

No maintainer action is requested on this superseded draft. Future follow-ups will stay narrowly scoped to the corresponding source PR.

@benperry6 benperry6 closed this Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/cron Cron scheduler and job management comp/desktop Electron desktop app (apps/desktop/*) comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins comp/tools Tool registry, model_tools, toolsets needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/mcp MCP client and OAuth tool/terminal Terminal execution and process management type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants