From b03635daea1abc9ae1b6aa3caa460f462d903337 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Tue, 30 Jun 2026 02:48:30 -0700 Subject: [PATCH] fix(approval): catch hermes gateway stop/restart behind a profile flag (#55515) The gateway-lifecycle guard's hermes-CLI pattern required `hermes` and `gateway` to be adjacent, so a profile flag slipped the agent past it: `hermes -p ade gateway restart` was not flagged. That is the exact form from the 2026-04-11 ade-profile self-kill loop. Allow an optional run of global flags (`-p ade`, `--profile ade`, multiple flags) between `hermes` and the gateway subcommand. launchctl self-termination is already covered on main by #33071; this narrows the only remaining real gap. --- tests/tools/test_approval.py | 35 +++++++++++++++++++++++++++++++++++ tools/approval.py | 6 ++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/tests/tools/test_approval.py b/tests/tools/test_approval.py index 6f87e0728c3..8693b7e6a12 100644 --- a/tests/tools/test_approval.py +++ b/tests/tools/test_approval.py @@ -965,6 +965,41 @@ class TestGatewayProtection: assert dangerous is True assert "stop/restart" in desc + def test_hermes_gateway_stop_detected(self): + cmd = "hermes gateway stop" + dangerous, key, desc = detect_dangerous_command(cmd) + assert dangerous is True + assert "gateway" in desc.lower() + + def test_hermes_gateway_restart_with_profile_flag_detected(self): + """A profile flag between `hermes` and `gateway` must not slip past + the guard. See the 2026-04-11 ade-profile self-kill incident.""" + cmd = "hermes -p ade gateway restart" + dangerous, key, desc = detect_dangerous_command(cmd) + assert dangerous is True + assert "gateway" in desc.lower() + + def test_hermes_gateway_stop_with_long_profile_flag_detected(self): + cmd = "hermes --profile ade gateway stop" + dangerous, key, desc = detect_dangerous_command(cmd) + assert dangerous is True + + def test_hermes_gateway_multiple_flags_detected(self): + cmd = "hermes -p cocoa --verbose gateway restart" + dangerous, key, desc = detect_dangerous_command(cmd) + assert dangerous is True + + def test_hermes_gateway_status_with_profile_flag_not_flagged(self): + """Read-only subcommands stay allowed even with a profile flag.""" + cmd = "hermes -p ade gateway status" + dangerous, key, desc = detect_dangerous_command(cmd) + assert dangerous is False + + def test_hermes_gateway_start_not_flagged(self): + cmd = "hermes gateway start" + dangerous, key, desc = detect_dangerous_command(cmd) + assert dangerous is False + def test_pkill_hermes_detected(self): """pkill targeting hermes/gateway processes must be caught.""" cmd = 'pkill -f "cli.py --gateway"' diff --git a/tools/approval.py b/tools/approval.py index 3d7a8362af6..b7fab3b4318 100644 --- a/tools/approval.py +++ b/tools/approval.py @@ -424,8 +424,10 @@ DANGEROUS_PATTERNS = [ (r'\bfind\b.*-delete\b', "find -delete"), # Gateway lifecycle protection: prevent the agent from killing its own # gateway process. These commands trigger a gateway restart/stop that - # terminates all running agents mid-work. - (r'\bhermes\s+gateway\s+(stop|restart)\b', "stop/restart hermes gateway (kills running agents)"), + # terminates all running agents mid-work. Allow global flags between + # `hermes` and `gateway` (e.g. `hermes -p ade gateway restart`) so a + # profile flag can't slip the agent past the guard. + (r'\bhermes\s+(?:-{1,2}\S+(?:\s+\S+)?\s+)*gateway\s+(stop|restart)\b', "stop/restart hermes gateway (kills running agents)"), (r'\bhermes\s+update\b', "hermes update (restarts gateway, kills running agents)"), # Docker container lifecycle — any user with docker.sock mounted (a common # Docker Compose pattern) gives the agent the ability to restart/stop/kill