Skip to content

fix(cli): register config shell hooks on serve and dashboard startup - #102521

Open
ericmaddox wants to merge 1 commit into
NousResearch:mainfrom
ericmaddox:fix/serve-register-config-shell-hooks
Open

ericmaddox wants to merge 1 commit into
NousResearch:mainfrom
ericmaddox:fix/serve-register-config-shell-hooks

Conversation

@ericmaddox

@ericmaddox ericmaddox commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes a security and lifecycle bug where hermes serve (the Hermes Desktop app's local agent backend) and hermes dashboard silently skipped registering shell hooks defined in config.yaml.

In hermes_cli/main.py:

  1. _AGENT_COMMANDS on line 12785 was defined as {None, "chat", "acp", "rl"}, omitting "serve" and "dashboard".
  2. When the Desktop app or Web Dashboard started, _prepare_agent_startup() early-returned on line 12844 because args.command was not in _AGENT_COMMANDS or _AGENT_SUBCOMMANDS. This bypassed register_from_config(_hooks_cfg) entirely.
  3. Every guard a user configured under hooks: in config.yamlpre_tool_call destructive command guards, tenant guards, and outbound-send blockers — was silently disarmed during Desktop sessions, even though the exact same configuration armed and executed under hermes chat and messaging gateway sessions.
  4. Additionally, _try_fast_serve_launch() (the fast cold-start path taken on every start of the Desktop app) dispatched straight to cmd_dashboard(args) without calling _prepare_agent_startup(args) (unlike _try_fast_chat_launch and _try_termux_fast_cli_launch which properly invoke it).

This PR:

  • Adds "serve" and "dashboard" to _AGENT_COMMANDS so _prepare_agent_startup() executes hook registration.
  • Adds "serve" and "dashboard" to _command_has_dedicated_mcp_startup() to prevent redundant synchronous inline MCP discovery, preserving cmd_dashboard's existing backgrounded / post-socket-bind MCP startup.
  • Invokes _prepare_agent_startup(args) inside _try_fast_serve_launch() prior to calling cmd_dashboard(args).
  • Adds regression unit tests covering hook registration and fast serve launch order.

Related Issue

Fixes #102504

Type of Change

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

Changes Made

  • hermes_cli/main.py:
    • Added "serve" and "dashboard" to _AGENT_COMMANDS.
    • Added args.command in {"acp", "serve", "dashboard"} to _command_has_dedicated_mcp_startup().
    • Added _prepare_agent_startup(args) to _try_fast_serve_launch().
  • tests/hermes_cli/test_serve_shell_hooks.py:
    • test_prepare_agent_startup_registers_shell_hooks_for_serve: Asserts agent.shell_hooks.register_from_config is called with loaded config and accept_hooks=True on serve.
    • test_prepare_agent_startup_registers_shell_hooks_for_dashboard: Asserts shell hooks register on dashboard.
    • test_try_fast_serve_launch_invokes_prepare_agent_startup: Asserts _prepare_agent_startup runs before cmd_dashboard.

How to Test

Run the new serve shell hook regression test suite and sibling CLI tests:

uv run --with pytest pytest tests/hermes_cli/test_serve_shell_hooks.py tests/hermes_cli/test_mcp_startup.py -v

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(cli): register config shell hooks on serve and dashboard startup (#102504))
  • 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 pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: Windows 11 (build 26100)

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Screenshots / Logs

============================= test session starts =============================
platform win32 -- Python 3.11.15, pytest-9.1.1, pluggy-1.6.0
rootdir: C:\Users\EricM\Dev\hermes-agent
configfile: pyproject.toml
plugins: anyio-4.12.1
collected 17 items

tests/hermes_cli/test_serve_shell_hooks.py ...                           [ 17%]
tests/hermes_cli/test_mcp_startup.py ..............                      [100%]

============================= 17 passed in 1.97s ==============================

@andrexibiza andrexibiza 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.

Reviewed exact head 106dba07008794c516b0a251ff42005625c2ad79 against base 63279301bcbdc185c1b07b98a9312eb0c862f26d, including the changed startup gates, fast-serve path, new tests, current cmd_dashboard lifecycle/profile behavior, shell-hook registration semantics, exact-head Actions, and the existing implementation history around #102504/#69825/#61806.

The underlying bug is real, and exact-head CI is green: the single commit has the CI, Nix flake, and Docker workflow suites completed successfully, with the required-check gate passing. I do see merge blockers in this particular wiring, though.

1. _AGENT_COMMANDS is the wrong ownership boundary for dashboard/serve lifecycle commands

Current main() deliberately calls _prepare_agent_startup(args) before command dispatch, with an explicit contract that management/introspection commands should not pay discovery cost or trigger hook consent. cmd_dashboard() then handles --status and --stop as early, dependency-free exits.

By adding both "serve" and "dashboard" to _AGENT_COMMANDS, this PR makes those commands run plugin discovery + config hook registration before cmd_dashboard() gets a chance to take its --status / --stop early exits. That changes a management operation into an agent-startup operation; it can load user code and can trigger shell-hook consent on a command whose current contract is specifically side-effect-light.

That exact concern already exists in the prior work on this defect class: #69832 intentionally registered at the shared dashboard/serve runtime boundary instead of broadening _AGENT_COMMANDS, and #81409 explicitly pins serve --status / --stop as hook-free.

Required fix: keep registration behind the runtime-owning boundary after lifecycle-only exits, or add an equally strong gate that proves --status/--stop never enter agent startup. Please add regression coverage for both management paths.

2. This can invert the documented plugin-before-shell-hook policy precedence

agent/shell_hooks.py documents a load-bearing ordering contract: Python plugins are registered first so their block decisions win ties over config shell hooks. _prepare_agent_startup() does not synchronously establish that order: it starts plugin discovery in a background thread and then continues to register_from_config(...). On the normal dashboard path, cmd_dashboard() later calls synchronous discover_plugins(), but with this patch the shell-hook callbacks may already have been appended to the process-global hook list before that join happens.

So the new route can make callback precedence timing-dependent on startup scheduling. That is a security-policy semantic change, not just startup performance.

Required fix: register config hooks only after plugin discovery is synchronously complete at this boundary (or explicitly join discovery before registration), and pin the invariant in a test where a plugin directive and shell-hook directive conflict and plugin policy wins deterministically.

3. The new tests prove invocation, not the security behavior being repaired

All three new tests replace config/plugin/hook modules or the startup functions with mocks and assert that registration was called. For a config-propagation/security-boundary fix, that misses the failure modes above: callback ordering, real allowlist/config loading, process-global registration, and whether an actual hook blocks an actual pre_tool_call lookup.

The repository guidance specifically calls for real-path validation for config propagation and security boundaries because mocks hide integration bugs. #81409 has a useful shape here: temp HERMES_HOME, real config loader/plugin manager, a real allowlisted hook subprocess, and an assertion on the resulting block decision.

Required fix: add at least one real-import regression that demonstrates the repaired serve/Desktop path reaches a real configured hook and preserves plugin precedence, plus the management-command no-side-effect tests above.

Interlock / attribution / merge order

This is a heavily duplicated defect family, so I would not merge this independently without consolidation:

  • #102504 is already triaged as a duplicate of #69825; #69825 was subsequently linked back to #61806.
  • #61844 is still open with the same serve -> _AGENT_COMMANDS approach.
  • #70461 is another open implementation of that same allowlist approach.
  • #69832 is the earlier focused implementation that moves registration to the dashboard/serve-owned boundary after plugin discovery.
  • #102513 is a fresh competing implementation on the same current base that extracts a shared config-hook helper and registers it inside cmd_dashboard() after synchronous plugin discovery.
  • #81409 is broader, complementary class coverage: it treats profile-scoped Desktop/TUI/compute-host/slash-worker processes as separate registration boundaries, preserves plugin-first precedence, avoids leaking launch-profile hooks into the shared browser Dashboard, and keeps lifecycle-only commands hook-free.

Those are not interchangeable credits: #61844/#70461 are duplicate allowlist implementations; #69832 and #102513 are targeted competing repairs at the dashboard/serve owner; #81409 is the broader profile/process-boundary treatment. Please preserve that lineage when consolidating rather than re-solving the same bug as a new island.

One additional “other side of the shape” to keep explicit: fixing the Desktop serve process does not automatically fix isolated child processes, because hook registries are process-local. If this PR remains intentionally scoped to #102504, that broader coverage should stay credited/tracked with #81409 rather than being implied closed here.

The diagnosis and fast-path attention are useful work, and the exact-head CI is clean. The remaining work is to put the fix on the runtime boundary that actually owns the policy, preserve deterministic plugin precedence, and prove it with the real path. 🚀

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard comp/desktop Electron desktop app (apps/desktop/*) area/config Config system, migrations, profiles labels Sep 3, 2026
@alt-glitch

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Related: this is one of several open PRs fixing the same hermes serve/dashboard shell-hook registration gap (#61806 / #69825 / #102504): #69832 (earliest), #85225, #92655, #102151, and #102513. This PR takes the _AGENT_COMMANDS + _prepare_agent_startup route while #69832/#102513 register hooks inside cmd_dashboard; a maintainer will need to pick one approach.

@ericmaddox

ericmaddox commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review and guidance, @andrexibiza!

I've updated the PR to address all three points:

Ownership Boundary: Reverted _AGENT_COMMANDS and moved config shell hook + outbound webhook registration inside cmd_dashboard() directly after synchronous discover_plugins(), keeping --status and --stop completely hook- and discovery-free.

Plugin Precedence: Ensured synchronous plugin discovery finishes before config shell hook registration runs, maintaining the deterministic plugin-before-shell-hook ordering contract.

Real-Path Tests: Updated tests/hermes_cli/test_serve_shell_hooks.py with real-path tests verifying side-effect-free management exits, real $HERMES_HOME config loading, and callback ordering in the hook registry.

@andrexibiza andrexibiza 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.

Follow-up on exact head 71e9a24c5b3d59643569155dd2bf4c97d1b5b6da after the review-fix commit.

The three behavioral blockers from my prior review are now substantively addressed:

  1. serve / dashboard are no longer broadened into _AGENT_COMMANDS; registration moved inside cmd_dashboard() after the lifecycle-only --status / --stop exits, and both exits are pinned hook/plugin-free by regressions.
  2. Config shell-hook registration now occurs after synchronous discover_plugins(), so callback order is deterministic instead of startup-thread timing-dependent. The new registry-order regression drives cmd_dashboard() through real config loading and real shell_hooks.register_from_config() and confirms plugin callback first, config shell hook second.
  3. The fast-serve path now dispatches directly to the runtime owner instead of invoking _prepare_agent_startup() and reintroducing the earlier ownership problem.

I also rechecked the surviving history rather than transferring the old head receipt. Both commits are hosted-green: 106dba07008794c516b0a251ff42005625c2ad79 has CI 33814046995, Docker 33814046219, Nix 33814046124; 71e9a24c5b3d59643569155dd2bf4c97d1b5b6da has CI 33825794578, Docker 33825791122, Nix 33825791127.

The remaining landing gate is structural/topological, not the original security behavior. This branch still adds runtime ownership inside current-base hermes_cli/main.py, a godfile whose changed hunk is already past line 12,500. #102117 is concurrently decomposing that same owner (and reports main.py at 15,210 lines on this base, 3,534 after its current split), while #102513 is still an open same-base competing carrier for the same #102504 registration boundary. Please compose one implementation into the bounded dashboard/serve owner rather than landing another responsibility into main.py or allowing duplicate carriers to merge by order. The behavior on this head is now the version I would preserve during that composition; fresh exact-object proof is required after rematerialization.

@ericmaddox

Copy link
Copy Markdown
Contributor Author

@andrexibiza Thanks for the follow-up review!

I have rematerialized the fix on current main (a6e10e693f) with the requested architectural composition:

  1. Extracted Topical Module: Created hermes_cli/config_shell_hooks.py owning register_runtime_config_hooks(args). main.py only imports and invokes this after synchronous discover_plugins(), keeping main.py clean and avoiding further additions to the CLI root.
  2. Deterministic Precedence Preserved: Plugin callbacks are discovered and registered first, followed by config shell hooks and outbound webhooks in the hook registry.
  3. Lifecycle Commands Pinned Hook-Free: --status and --stop exit early without triggering plugin discovery or hook registration.
  4. Real-Path & Precedence Tests: Unit tests in tests/hermes_cli/test_dashboard_hooks.py verify --status/--stop early exits, real-path hook and webhook registration order, and plugin callback priority over config shell hooks (5/5 passing).
  5. Attribution Lineage Preserved: Added co-author references for the related defect family PRs (fix(cli): register shell hooks in hermes serve sessions by adding 'serve' to _AGENT_COMMANDS (#61806) #61844, fix: add 'serve' to _AGENT_COMMANDS so register_from_config is called (#69825) #70461, fix(serve): register shell hooks for dashboard/serve startup (#69825) #69832, fix(hooks): register config hooks for serve #102513, fix(hooks): register shell hooks in profile-scoped UI runtimes #81409).

…ousResearch#102504)

Consolidate dashboard/serve runtime hook registration into topical
`hermes_cli/config_shell_hooks.py` following the main.py decomposition.
Ensures deterministic plugin-first ordering after synchronous plugin discovery,
preserves hook-free contracts for early lifecycle exits (--status / --stop),
and includes real-path precedence test coverage.

Co-authored-by: NousResearch#61844 <contributor@nousresearch.com>
Co-authored-by: NousResearch#70461 <contributor@nousresearch.com>
Co-authored-by: NousResearch#69832 <contributor@nousresearch.com>
Co-authored-by: NousResearch#102513 <contributor@nousresearch.com>
Co-authored-by: NousResearch#81409 <contributor@nousresearch.com>

This branch has not been deployed

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

Labels

area/config Config system, migrations, profiles comp/cli CLI entry point, hermes_cli/, setup wizard comp/desktop Electron desktop app (apps/desktop/*) P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shell hooks from config.yaml never register in hermes serve (desktop backend) — _prepare_agent_startup skips the "serve" command

3 participants