Skip to content

fix(approval): extend gateway-lifecycle guard to launchctl and pidof-based kills - #33084

Closed
briandevans wants to merge 1 commit into
NousResearch:mainfrom
briandevans:fix/approval-gateway-launchctl-pidof-33071
Closed

fix(approval): extend gateway-lifecycle guard to launchctl and pidof-based kills#33084
briandevans wants to merge 1 commit into
NousResearch:mainfrom
briandevans:fix/approval-gateway-launchctl-pidof-33071

Conversation

@briandevans

Copy link
Copy Markdown
Contributor

What does this PR do?

The dangerous-command approval layer in tools/approval.py already blocks hermes gateway (stop|restart), pkill/killall hermes|gateway, and kill ... $(pgrep ...). Issue #33071 reports a distinct vector confirmed by alt-glitch as not covered by the existing related security PRs (#30882, #22557, #33057, #29159): the agent can still achieve a gateway restart by driving launchd directly against the service label (launchctl stop ai.hermes.gateway, launchctl kickstart -k system/ai.hermes.gateway) or by substituting pidof for pgrep in the kill-expansion form.

This widens the existing "Gateway lifecycle protection" block to cover both vectors with explicit, narrowly-scoped patterns:

  • launchctl (stop|kickstart|bootout|unload|kill|disable|remove) is flagged only when the target includes a Hermes label (hermes or ai.hermes). Read-only inspection (launchctl print …, launchctl list) and operations against unrelated labels stay unflagged — both verified by regression tests.
  • kill ... $(pidof …) and the backtick form are added alongside the existing pgrep expansion. pidof is the BSD/Linux equivalent and is equally opaque to the (pkill|killall) … hermes name pattern.

Mirrors the existing block's precedence: explicit hermes-named target > generic command shape > literal-PID kills stay safe. The literal-PID case (kill -TERM <numeric_pid> looked up out-of-band) is intentionally still allowed — catching it would require runtime PID state and would break the existing TestPgrepKillExpansion::test_safe_kill_pid_not_flagged contract.

Related Issue

Fixes #33071

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • tools/approval.py — extended the pgrep kill-expansion pattern to also match pidof, and added a launchctl (stop|kickstart|bootout|unload|kill|disable|remove) pattern scoped to Hermes labels in the existing "Gateway lifecycle protection" block.
  • tests/tools/test_approval.py — added TestLaunchctlGatewayLifecycle (6 cases: stop/kickstart/bootout/unload all flagged; launchctl print against an unrelated label and launchctl stop com.example.unrelated stay safe) and two new TestPgrepKillExpansion cases for $(pidof …) and `pidof …`. Existing test_safe_kill_pid_not_flagged preserved as the explicit safety boundary.

How to Test

  1. uv run --with pytest --with pytest-xdist --with pytest-asyncio python3 -m pytest tests/tools/test_approval.py -v -k "Pgrep or Launchctl" — 13 pass (5 pre-existing + 8 new).
  2. Full file as regression guard: uv run --with pytest --with pytest-xdist --with pytest-asyncio python3 -m pytest tests/tools/test_approval.py — 196 pass.
  3. Manual repro of the issue: from tools.approval import detect_dangerous_command; print(detect_dangerous_command('launchctl stop ai.hermes.gateway'))(True, …, 'stop/restart hermes launchd service (kills running agents)').

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run focused tests for the touched code and all pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 15.x

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — N/A; existing # Self-termination protection and # Gateway lifecycle protection comments updated inline
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guidelaunchctl is macOS-only; Linux/systemctl is already covered at line 338 (systemctl … (stop|restart|disable|mask)). Windows service-manager equivalents (sc.exe stop, taskkill /F /IM) are not currently covered by any pattern in this block — could be a follow-up if Windows gateway-as-a-service usage grows.
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

Screenshots / Logs

Audited siblings: confirmed every existing pattern in the "Gateway lifecycle protection" block (lines 365-380) has either a CLI form (hermes gateway …, hermes update), a service-manager form (systemctl … (stop|restart|disable|mask) at line 338), a name-based form (pkill|killall … hermes|gateway), or a shell-expansion form ($(pgrep) / `pgrep`). The two added patterns close the macOS-launchd and pidof gaps respectively. No further widening needed within the same block; a Windows-sc.exe/taskkill follow-up would be cleanly orthogonal if/when the project gains a Windows service manifest. Happy to widen further if preferred.

Copilot Bot review requested due to automatic review settings May 27, 2026 07:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Expands dangerous-command detection to close bypasses that can terminate Hermes agents by using pidof substitutions and direct launchctl service control on macOS.

Changes:

  • Extend kill $(...) and backtick expansion detection to include pidof alongside pgrep.
  • Add detection for launchctl subcommands that can stop/restart/unload Hermes launchd services.
  • Add unit tests covering the new pidof and launchctl detection behaviors.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
tools/approval.py Adds new dangerous-command regex patterns for pidof expansions and launchctl lifecycle operations.
tests/tools/test_approval.py Adds tests validating detection of the new pidof and launchctl patterns plus non-flagged cases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tools/approval.py
Comment on lines +386 to +387
# Catch the operations that stop, restart, or unload it.
(r'\blaunchctl\s+(stop|kickstart|bootout|unload|kill|disable|remove)\b.*\b(hermes|ai\.hermes)\b', "stop/restart hermes launchd service (kills running agents)"),
Comment thread tools/approval.py
# the `hermes gateway stop|restart` pattern above by driving launchd
# directly against the service label (commonly `ai.hermes.gateway`).
# Catch the operations that stop, restart, or unload it.
(r'\blaunchctl\s+(stop|kickstart|bootout|unload|kill|disable|remove)\b.*\b(hermes|ai\.hermes)\b', "stop/restart hermes launchd service (kills running agents)"),
Comment on lines +833 to +836
dangerous, _, desc = detect_dangerous_command(cmd)
assert dangerous is True
assert "pidof" in desc.lower() or "pgrep" in desc.lower()

@alt-glitch alt-glitch added type/security Security vulnerability or hardening P2 Medium — degraded but workaround exists comp/tools Tool registry, model_tools, toolsets labels May 27, 2026
@briandevans
briandevans force-pushed the fix/approval-gateway-launchctl-pidof-33071 branch 6 times, most recently from 5b6864d to f24a93c Compare June 1, 2026 16:14
…based kills

The dangerous-command approval layer already blocks `hermes gateway
(stop|restart)`, `pkill/killall hermes|gateway`, and `kill ... $(pgrep ...)`.
A reporter noted on NousResearch#33071 that the agent can still achieve the same
effect by driving launchd directly against the gateway's service label
(`launchctl stop ai.hermes.gateway`, `launchctl kickstart -k
system/ai.hermes.gateway`, etc.) or by substituting `pidof` for `pgrep`
in the kill-expansion form.

This widens the "Gateway lifecycle protection" block in
`tools/approval.py` to cover both vectors:

- `launchctl (stop|kickstart|bootout|unload|kill|disable|remove)`
  scoped to commands that target a Hermes label (`hermes`,
  `ai.hermes`). Read-only inspection (`launchctl print …`,
  `launchctl list`) and operations against unrelated labels remain
  unflagged.
- `kill ... $(pidof …)` and the backtick form, alongside the existing
  `pgrep` expansion. `pidof` is the BSD/Linux equivalent and is
  equally opaque to the `(pkill|killall) … hermes` name pattern.

Intentionally left out of scope: plain `kill -TERM <numeric_pid>` with
a PID looked up out-of-band. Catching that would require runtime PID
state and would break the existing
`TestPgrepKillExpansion::test_safe_kill_pid_not_flagged` contract,
which guarantees that a plain literal-PID `kill 12345` stays safe.
@teknium1

Copy link
Copy Markdown
Contributor

Merged via #53216 — cherry-picked onto current main with your authorship preserved in git log (commit 3c8d3ec). Thanks @briandevans, and especially for documenting the literal-PID exclusion + rationale in the commit message; that framed the whole review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tools Tool registry, model_tools, toolsets P2 Medium — degraded but workaround exists type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dangerous command approval can be bypassed by sending SIGTERM to gateway process

4 participants