11310068c6
Part A — pre_command observer hook (observer-first per #64182 ground rule 3): - New VALID_HOOKS event `pre_command`: fires when a recognized slash command is about to be dispatched, BEFORE the handler runs, on both surfaces: - CLI: cli.py process_command (right after alias resolution) - Gateway: gateway/run.py _handle_message cold-path canonical dispatch - Payload: surface ('cli'|'gateway'), command (canonical), alias_used, args_raw, session_key, platform. Return values IGNORED in v1; a plugin returning a directive-shaped dict gets a debug log so future block/rewrite adopters are discoverable (#64231 taxonomy). - Deliberately NOT fired on the gateway running-agent intercept path (/stop, /approve, busy_policy dispatch during an active run): those are control-plane escape hatches on an in-flight run and must stay outside plugin observation/veto reach. - fire_pre_command_hook() helper never raises, so broken plugin infra can never break command dispatch. Part B — ctx.call_mcp (capability-gated, default-off, ground rule 4): - PluginContext.call_mcp(server, tool, arguments, timeout=30): synchronous, callable from plugin hooks/tools, routes through the EXISTING native MCP client machinery (tools.mcp_tool._make_tool_handler: background loop, trust-tier gates, circuit breaker, reconnect) — never a parallel client. - Gate: plugins.entries.<id>.mcp_allowlist (list of server names). Absent key / unreadable config / non-list value => default-deny. Unlisted server raises PermissionError naming the exact config key. TODO seam left for the #64228 declared-capability model. - Bounded: timeout clamped to 1-600s and forwarded to the MCP loop call; results capped at 64KB with truncation marker; stable {ok, result|error, structuredContent?, truncated?} envelope. Tests (transport mocked, no live MCP servers): - tests/hermes_cli/test_pre_command_hook.py: both surfaces fire, canonical alias reporting (/exit->quit, /q->queue), hook-before-handler ordering, control-plane exclusion, hook failure non-fatal, observer-only directive handling. - tests/hermes_cli/test_plugin_call_mcp.py: default-deny (absent entry, unreadable config, non-list, '*'), allowlist enforced per-server, denied calls never touch transport, timeout forwarding/clamping, result truncation, error/structuredContent envelopes. Docs: hooks.md shipped-catalog row for pre_command; plugins.md "Calling MCP servers from plugins" section with the security note. Closes #64204