fix(approval): keep self-termination gated under the container-backend bypass (#71957) - #71965
Conversation
Related: #71957 identifies the container-bypass policy gap; #43157 guards own-PID termination unconditionally. This PR instead preserves the container bypass for host-destruction patterns while gating the self-disruption subset, so maintainer policy selection is still needed. |
|
Confirming the triage note's read: this isn't a duplicate of #43157. #43157 guards own-PID termination unconditionally, which would also re-gate host-destruction patterns ( So the two are different policy shapes, not redundant implementations, and which one lands is a maintainer policy call — I've left the container bypass otherwise untouched here so the choice stays open. No code change from this note. |
|
Thanks for preserving the existing host-destruction bypass while addressing a real shell-command gap: current main returns early for isolated containers before the self-disruption patterns can run ( Problems
Suggested changes
This is an automated hermes-sweeper review. |
|
Thanks — both points addressed in
Full file green locally (the one unrelated failure is the known macOS |
…d bypass (NousResearch#71957) Isolated container backends (docker/singularity/modal/daytona) skip dangerous-command approval on the rationale that the container is the host-safety boundary — a destructive command inside the sandbox can't reach the host. That is sound for `rm -rf` / `mkfs` / `dd`, but it also silently waived the *self-termination* guards, which have nothing to do with host safety: killing the agent's own gateway process (`pkill hermes`, `kill $(pgrep -f hermes)`, `hermes gateway restart`, `docker compose down`, `launchctl bootout ai.hermes.gateway`, …) is a self-inflicted service DoS / mid-session state loss that disrupts the operator's own running service whether or not it touches the host. Because the documented, recommended Docker Compose deployment runs `terminal.backend: docker`, self-termination prevention was effectively disabled by default for a large share of gateway operators — an agent could restart/kill its own gateway with no approval prompt. Carve the self-disruption pattern family out of the container bypass in both shell-command entry points (`check_dangerous_command`, `check_all_command_guards`): these still route through the normal approval gate under container backends, while every other dangerous command is waived exactly as before. The carve-out matches the self-disruption patterns directly rather than via first-match detection, so a command like `pkill -9 hermes` (which matches the generic "force kill processes" rule first) is still recognised as self-termination. A drift-guard test asserts every carve-out description still exists in DANGEROUS_PATTERNS. Fixes NousResearch#71957
execute_code shares the container-guard skip, so a script could subprocess/os.system its way to 'hermes gateway stop' / 'pkill hermes' and bypass the self-termination gate the terminal path now enforces. Match self-disruption shell commands in the script text so they still route through approval under container backends; ordinary scripts stay waived. Raw in-process kills (os.kill on a discovered PID) remain out of scope for string-pattern detection, as the docstring already notes. Also update the container-bypass callouts in security.md and tips.md to state the self-disruption exception.
8596c62 to
1d8a929
Compare
SummaryOne PR addresses issue #71957. #71965 implements a targeted self-disruption exception to the container-backend bypass across shell-command and detectable execute_code paths, while preserving the bypass for unrelated host-destructive commands and documenting the exception. Related pull requests
Suggested consolidationKeep #71965 open with a salvage path: retain its targeted self-disruption matcher, three guard-path carve-outs, focused regression tests, and updated container-bypass documentation. It is the only PR in this complex, closes #71957 directly, and is not a duplicate of #43157, whose broader own-PID policy would have a different effect; there are no duplicate PRs to close. Complex graphflowchart LR
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
I71957(["issue #71957 (open)"])
P71965["PR #71965 (open)"]
P71965 -->|best fix| I71957
class I71957 open
class P71965 open
class P71965 best
class P71965 target
click I71957 "https://github.com/NousResearch/hermes-agent/issues/71957"
click P71965 "https://github.com/NousResearch/hermes-agent/pull/71965"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label). Cross-PR triage: Reviewed 1 pull request and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 20 kB of PR diffs, 7 kB of issue/PR text, 4 kB of discussion (4 comments), 3 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
Problem
Fixes #71957.
check_dangerous_command/check_all_command_guardsskip dangerous-command approval entirely whenterminal.backendis an isolated container backend (docker,singularity,modal,daytona), on the documented rationale that the container is the host-safety boundary — a destructive command inside the sandbox can't reach the host.That rationale is sound for
rm -rf/mkfs/dd, but it also silently waived the self-termination guards, whose rationale has nothing to do with host safety. Killing the agent's own gateway process is a self-inflicted service DoS / mid-session state loss that disrupts the operator's own running service whether or not it touches the host:pkill hermes,pkill -9 hermes,killall gateway,kill $(pgrep -f hermes)hermes gateway stop|restart,hermes updategateway runwith&/disown/nohup/setsiddocker compose down,docker restart/stop/kill(docker.sock is commonly mounted in the Compose deployment)launchctl bootout … ai.hermes.gatewayBecause the documented, recommended Docker Compose deployment runs
terminal.backend: docker, self-termination prevention was effectively disabled by default for a large share of gateway operators — an agent could restart or kill its own gateway with no approval prompt (exactly as reported in the issue).Fix
Carve the self-disruption pattern family out of the container bypass in both shell-command entry points. Under a container backend these commands now route through the normal approval gate as they would on
local/ssh; every other dangerous command is still waived exactly as before (verified by test). The change is purely additive gating — it never weakens any existing check.Implementation notes:
_SELF_DISRUPTION_DESCRIPTIONSnames the subset ofDANGEROUS_PATTERNSwhose sole purpose is preventing self-service disruption (self-termination + gateway/container lifecycle teardown), distinguished from host-scoped destruction which the container boundary legitimately covers._matches_self_disruption_pattern) rather than relying ondetect_dangerous_command's first match, so a command likepkill -9 hermes— which matches the generic "force kill processes" rule first — is still recognised as self-termination and gated.check_dangerous_command,check_all_command_guards);check_execute_code_guardoperates on Python source, not shell command strings, and is left unchanged.Tests
New
tests/tools/test_container_self_disruption_bypass.py(17 tests):rm -rf /workspacestill bypassed under every container backend (callback never consulted);pkill -9 hermesfirst-match edge case still gated;kill $(pgrep -f hermes)andhermes gateway restartgated;DANGEROUS_PATTERNS.ruffand the Windows-footgun check pass;pyproject.toml/uv.lockuntouched.