fix: close gateway-lifecycle guard gaps in cron guard and execute_code - #68289
fix: close gateway-lifecycle guard gaps in cron guard and execute_code#68289arcimun wants to merge 1 commit into
Conversation
Two real incidents (2026-07-13 and 2026-07-19) exposed gaps where the
gateway could be killed from inside itself:
1. cron/lifecycle_guard.py:
- Python argv-list form (subprocess.run([...])) was not matched
because the regex used \s+ which doesn't match commas/quotes
- launchctl bootout/bootstrap/disable/remove were missing — bootout
unloads the job so KeepAlive can't revive it (26h outage)
- pkill regex used \bhermes\b which fails on hermes_cli.main
- gateway run/start --replace was not blocked (SIGTERMs live gateway)
2. tools/code_execution_tool.py:
- execute_code had NO gateway-lifecycle guard at all — os.system()
and subprocess.run() inside Python scripts sailed through
- Several bundled skills document bootout recipes, giving the
model a standing invitation to run them
teknium1
left a comment
There was a problem hiding this comment.
Thanks for extending a real gateway self-termination defense. Current main still has the reported lifecycle and execute_code gaps.
Problems
cron/lifecycle_guard.py:73only makes the Hermes and launchctl branches argv-list aware. The next systemctl branch (:75) still requires whitespace, sosubprocess.run(["systemctl", "restart", "hermes-gateway"])remains unmatched.cron/lifecycle_guard.py:95accepts anygateway run --replacesubstring. That conflicts with the existing command-shaped guard contract atcron/lifecycle_guard.py:45-47and can reject safe prose.- The PR adds no regression tests. Existing coverage in
tests/hermes_cli/test_gateway_restart_loop.py:28-89,313-381does not exerciseexecute_codeor the new variants.
Suggested changes
- Make systemctl and --replace argv-list aware, preserving a concrete Hermes/module command anchor for --replace.
- Add direct
execute_codegateway-marker tests plus positive and safe-prose pattern cases.
Automated hermes-sweeper review.
| # through this gap, booted the job out, and the Mac gateway stayed dead for | ||
| # 26h until a manual restart. `disable`/`remove` are blocked for the same | ||
| # reason (they leave the label un-startable). | ||
| r"|(?:launchctl[\"',\s]+(?:kickstart|unload|load|stop|restart|bootout|bootstrap|disable|remove)\b[^\n]*\bhermes[.\-]?gateway)" |
There was a problem hiding this comment.
This makes the Hermes and launchctl branches argv-list aware, but the systemctl branch immediately below still begins with systemctl\s+. subprocess.run(["systemctl", "restart", "hermes-gateway"]) therefore remains unblocked; apply the same delimiter handling there and add a regression case.
| # victim logged `parent_pid=1` (its own launchd parent), which reads like | ||
| # launchd killed it and hides the actual caller. Covers both the CLI shape | ||
| # and the module shape (`python -m hermes_cli.main gateway run --replace`). | ||
| r"|(?:gateway\s+(?:run|start)\b[^\n]*--replace)" |
There was a problem hiding this comment.
This matches arbitrary prose containing gateway run --replace, although the existing guard deliberately requires command-shaped identifiers to avoid false positives. Require a Hermes CLI or hermes_cli.main invocation prefix, and add a safe-prose regression case.
|
Current-main follow-up: I reproduced a remaining execute_code path that the source-text check at this head does not cover. A Python child can assemble |
|
Nice work closing the source-text gaps. I found one remaining bypass while reproducing this locally: the new guard in code_execution_tool.py only scans source text, but once the script is approved it can still assemble dangerous argv dynamically at runtime, e.g. I opened #83630 with a no-side-effect reproduction and what I think the acceptance criteria should be: inspect argv at child runtime, cover dynamic assembly and os.system/shell=True/os.exec*, and keep yolo/off from waiving it. If useful I can prepare a follow-up PR. |
… lifecycle guard (#68289) execute_code lacked the lifecycle guard entirely, and Python argv-list forms (subprocess.run([...])) separated command words with brackets and commas the shell-shaped pattern could not see. Mirror the terminal_tool guard in execute_code (ownership-gated per #92560) and strip argv-list punctuation in the token-join re-scan. Salvaged from PR #68289 by @arcimun, adapted to the ownership gate and current guard structure.
… lifecycle guard (#68289) execute_code lacked the lifecycle guard entirely, and Python argv-list forms (subprocess.run([...])) separated command words with brackets and commas the shell-shaped pattern could not see. Mirror the terminal_tool guard in execute_code (ownership-gated per #92560) and strip argv-list punctuation in the token-join re-scan. Salvaged from PR #68289 by @arcimun, adapted to the ownership gate and current guard structure.
|
Merged via PR #93336 (c94ee2e) — the execute_code guard and the argv-list detection were cherry-picked with your authorship preserved in git log, adapted to the PID-ownership gate (#92560) and the current guard structure (argv punctuation stripped in the token-join re-scan rather than widening the raw pattern, keeping prose safe). Your Branch B verb additions (bootout/bootstrap/disable/remove) had already landed via #93297, and the |
What
Closes two gaps in the gateway lifecycle guard that allowed the agent to kill its own gateway process — causing real outages (26h on 2026-07-13, another on 2026-07-19).
Changes
1. cron/lifecycle_guard.py
\s+which doesn't match commas/quotes insubprocess.run([...])formbootout,bootstrap,disable,remove\bhermes\bdoesn't matchhermes_cli.maingateway run/start --replacenot blocked2. tools/code_execution_tool.py
os.system()/subprocess.run()sailed throughTesting
All variants tested 2026-07-19: bootout, kickstart -k, pkill -f hermes_cli.main, gateway run --replace, subprocess.run([...]) list form