Skip to content

feat(approval): block agents from killing their own gateway/host process#128

Merged
OmarB97 merged 1 commit into
mainfrom
fix/self-host-kill-guard
Jun 9, 2026
Merged

feat(approval): block agents from killing their own gateway/host process#128
OmarB97 merged 1 commit into
mainfrom
fix/self-host-kill-guard

Conversation

@OmarB97

@OmarB97 OmarB97 commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Why

Reproduced 2026-06-09 (mesh record hermes-dashboard-kill-orphans-hosted-session-20260609): an agent session killed its own desktop dashboard PID to clear stale bytecode. The host process died mid-turn, the session indicator went blank, stop failed, and prompting returned session-not-found. The recovery half already exists on main — graceful SIGTERM finalize in tui_gateway/entry.py, session.resume fallback to state.db, desktop websocket rebind (NousResearch#43004) — but nothing stopped an agent from pulling the rug out from under itself in the first place.

What changed

  • tools/approval.py: new function guard _check_self_host_kill running in check_all_command_guards right after the sudo-stdin guard, before any yolo/mode bypass. It extracts numeric kill targets plus the $$/$PPID shell self-tokens and blocks when they match os.getpid()/os.getppid() — i.e. the process hosting this very session (desktop dashboard with in-process tui_gateway, or the CLI). Block result is hardline-shaped (hardline: true) with a message pointing at supervisor restarts (hermes gateway restart, desktop Gateway menu).
  • Containerized backends (docker/singularity/modal/daytona) keep their existing early bypass — PIDs in a container namespace are not the host gateway's.
  • tests/tools/test_hardline_blocklist.py: 10 new cases.

How to review

  1. _check_self_host_kill + _self_host_kill_block_result in tools/approval.py — the regex scopes to kill at command position; pkill and negative-pgid forms are deliberately out of scope (rationale in the module comment; kill -1 is already hardline).
  2. The call site in check_all_command_guards — confirm it sits in the unconditional section (before yolo/mode=off checks).
  3. The new test block at the bottom of test_hardline_blocklist.py.

Evidence

  • test_yolo_cannot_bypass_self_host_kill and test_check_all_command_guards_blocks_self_host_kill pin the unconditional behavior; test_self_host_kill_allows_foreign_pid pins the non-overreach.

Verification

  • tests/tools/test_hardline_blocklist.py — 110 passed (100 pre-existing + 10 new).
  • tests/tools/test_approval.py test_cron_approval_mode.py test_execute_code_approval_cluster.py — 250 passed, 2 failed; both failures (TestHermesConfigWriteProtection perl/ruby in-place) reproduce identically on clean origin/main with this diff stashed — pre-existing, environment-dependent, untouched by this change.

Risks / gaps

  • pkill -f <pattern> matching the host's cmdline can still self-kill — out of scope here as name-matching needs cmdline introspection; covered by mesh record hermes-dashboard-kill-orphans-hosted-session-20260609 follow-up notes.
  • Process-group kills (kill -- -<pgid>) are not compared against our pgid — accepted scope, kill -1 (all processes) is already hardline-blocked.
  • Guard reads os.getppid() per invocation; if the host is re-parented mid-session (supervisor restart) the parent check follows reality — low risk.

Collaborators

  • @OmarB97 (operator)
  • Claude Fable 5 (Claude Code)

…host process

Reproduced 2026-06-09: a session cleared stale bytecode by killing its own
desktop dashboard PID. The host process died mid-turn, the session was
orphaned (blank indicator, stop failed, session-not-found on the next
prompt) and in-progress work was lost. Companion fixes already on main
(graceful SIGTERM finalize in tui_gateway/entry.py, resume fallback to
state.db, desktop ws rebind NousResearch#43004) make sessions recoverable after a
gateway death — this guard removes the foot-gun that caused it.

- tools/approval.py: _check_self_host_kill extracts kill numeric targets
  and $$/$PPID self-tokens, compares against os.getpid()/os.getppid(),
  and blocks unconditionally (hardline-style: yolo / approvals.mode=off /
  cron approve cannot bypass; containers still skip — their PID namespace
  is not the host's). Block message points at supervisor-driven restarts.
- Negative-pgid kill forms stay out of scope (kill -1 is already
  hardline); pkill name-matching is a separate concern.
- tests: 10 new cases in tests/tools/test_hardline_blocklist.py (own pid,
  parent pid, $$/$PPID, chained, foreign-pid allow, pgid/pkill allow,
  end-to-end hardline shape, yolo no-bypass, container bypass).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

🔎 Lint report: fix/self-host-kill-guard vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 10543 on HEAD, 10543 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 5537 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@OmarB97 OmarB97 merged commit ee54f6c into main Jun 9, 2026
17 of 23 checks passed
OmarB97 pushed a commit that referenced this pull request Jun 10, 2026
… fork consolidation; finish fork-feature ports

Per-cluster restoration with the test suite as the oracle, after comparing
the merged tree's failures against a pristine-upstream run in the same
environment (14 file-level deltas, now zero):

- gateway/run.py: upstream wholesale (fork's monolith had undone the mixin
  decomposition; both real fork deltas re-applied — voice_ack_callback
  **kwargs; the custom-providers context-length fix exists upstream).
- agent/conversation_loop.py + turn_context.py: upstream structure with the
  fork features regrafted at their new homes — sender_device attribution
  (#131), preflight token-usage emission + compression-complete status and
  live-estimate snapshots (#126).
- agent/chat_completion_helpers.py: upstream wholesale (brings the second
  partial-stream-stub routing site and the NousResearch#6600 cancellation fix).
- agent/tool_executor.py: usage= kwarg on tool start/complete callbacks now
  falls back to the bare 3-arg form for legacy receivers.
- tools/approval.py: upstream's resolved-HERMES_HOME rewrite + normalize
  steps restored alongside the fork's self-host kill guard (#128).
- hermes_cli/main.py: desktop install-identity stale-build cluster and the
  post-subcommand global-flag hoister ported from fork main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant