hyperhive/docs/turn-loop/mcp.md
2026-07-25 20:05:32 +02:00

13 KiB

MCP surface

The harness ships an embedded MCP server (rmcp 1.7). The built-in hyperhive surface is served over streamable HTTP by a persistent hive-mcp-http daemon (loopback, 127.0.0.1:<hyperhive.mcp.httpPort>, per-container private netns). Claude connects to its stable URL via --mcp-config rather than respawning a stdio child each turn, so the URL survives the per-turn claude re-spawn (and a host-side hive-c0re restart) — there is no per-turn MCP re-registration race for the built-in surface. HTTP is the sole transport for it (no stdio fallback). Extra servers (hyperhive.extraMcpServers) pick their own transport per entry (type = "stdio" | "http", default "stdio"): matrix stays a stdio bridge, bash runs its own persistent streamable-http listener (hive-bash-daemon) — same reasoning as the built-in surface. The server name is hyperhive, so the tools land in claude as mcp__hyperhive__<tool>.

Tool access is gated by tool groups (HIVE_TOOL_GROUPS). The default preset (AGENT_DEFAULT) includes messaging, meta, inbox, and execution. Privileged groups (lifecycle, approvals, scheduling, diagnostics) are opt-in via the P3RM1SS10NS tab.

Core tools (always available)

Messaging (messaging group): send(to, body, in_reply_to?), recv(wait_seconds?, max?), ask(question, options?, multi?, ttl_seconds?, to?), answer(id, answer), ack_until(up_to).

  • send — message a peer (logical name) or the operator (to: "operator"). Use to: "<parent>" to address the topology parent without hardcoding the label; the broker resolves the sentinel at delivery time. Optional in_reply_to: i64 links the message to a prior id for thread rendering. Per-agent hyperhive.allowedRecipients (default: empty = unrestricted) limits which names send accepts — useful for sandboxing: set [ "operator" ] to restrict a sub-agent to operator messages only (the topology parent is always reachable regardless of this list — that carve-out is structural, keyed on parent relationship, not name).
  • recv — drain inbox. Without wait_seconds (or 0) returns immediately. Positive value parks the turn up to that many seconds (cap 180) — incoming messages wake instantly. max (default 1, cap 5) drains up to N rows; wait_seconds applies to the first, then drains up to max total. Each returned row is prefixed with [msg #<id>] (broker row id; note the highest id seen, then pass it to ack_until to bulk-triage the batch). Graceful shutdown: when the harness receives a stop signal, the inbox becomes fenced and recv returns an explicit from: "graceful-stop" message instead of an empty inbox. This unmissably directs the agent to flush durable state (/state files) and end the turn — the container exits when the turn completes. The graceful-stop turn takes the same post-turn compaction path as any other turn: if the context crossed the watermark the harness runs a notes-checkpoint turn and then /compact. Compacting before shutdown keeps a later cold start cheap instead of re-uploading a huge transcript.
  • ask — surface a structured question to the operator (default) or a peer agent (to: "<agent>"). Non-blocking — returns a question id; the answer arrives as a question_answered system event in the asker's inbox. options is advisory; multi=true renders as checkboxes; ttl_seconds auto-cancels with answer [expired].
  • answer — respond to a question_asked event routed to this agent. Strict authorisation: only the declared target can answer.
  • ack_until(up_to) — bulk-mark inbox rows handled: every row with broker id <= up_to is stamped as acked in a single UPDATE. Recipient-scoped (agents can only ack their own rows). Use when a restart redelivers a large backlog of already-handled messages: read the highest [msg #N] from the set you've actually processed, then ack_until(N) to prevent re-pop. Acked rows never redeliver. Transient pings (sentinel id 0) have nothing to ack and show no marker.

System messages (from sender system): lifecycle and Q&A events delivered as regular inbox messages (same recv path; body is a JSON object with an event discriminant field). The submitting agent (the root agent for top-level containers; an agent with the approvals tool group for its own subtree) receives lifecycle events (spawned, rebuilt, killed, destroyed, container_crash, needs_login, logged_in, config_ready, needs_update, approval_resolved). Any agent receives Q&A events when it is the declared target (question_asked) or the asker (question_answered). Full payload shapes and routing logic in docs/approvals.md § Helper events.

Inbox (inbox group): get_loose_ends(agent?), cancel_loose_end(kind, id), remind(message, delay_seconds? | at_unix_timestamp?), request_next_turn().

  • get_loose_ends(agent?) — list pending questions (asked/owed), scheduled reminders, and active local tasks published by external MCP daemons (e.g. running bash tasks from hive-bash-daemon). Each row carries an id + kind for cancel_loose_end. Omit agent to list your own threads. Pass agent: "<name>" to inspect a direct child agent (always accessible per topology enforcement); non-children require the query_agent_state capability. The "*" hive-wide query is not available on the agent socket.
  • cancel_loose_end — withdraw a question (posts [cancelled by <self>]), hard-delete a reminder, or cancel a pending approval row. Agents may only cancel rows they own; the approval kind is further restricted to the root agent (ruth) server-side.
  • remind — schedule a reminder in this agent's own inbox. Large payloads spill to /agents/<self>/state/reminders/. Pending count capped at 50 per agent (HIVE_REMIND_MAX_PENDING_PER_AGENT).
  • request_next_turn — ask the harness to start another turn immediately after this one ends, even if the inbox is empty. Next turn fires with from: "self" and body: "continue".

Meta (meta group): set_status(text), get_agent_meta(name?).

  • set_status — set a free-text status string visible on the dashboard. Single line, ≤ 200 chars. Persisted to {state_dir}/hyperhive-status. Pass "" to clear.
  • get_agent_meta — fetch identity + status metadata for an agent: { name, hyperhive_rev, running, status_text, status_set_at, hive_name?, swarm_name?, matrix_accounts? }. matrix_accounts is a list of matrix identities the agent can act as (name, user_id?, homeserver); omitted for agents with no matrix provisioning. Omit name to query self.

Always-on, no tool group (like set_status): compact().

  • compact — agent self-service equivalent of the operator dashboard's /compact button. No args. Gated server-side on the last completed turn's context usage: refused (with an explanation, no side effect) unless usage is above 66% of the effective context window. On a pass, queues the same deferred compact_pending flag the dashboard button sets — consumed at the end of the current turn, so it never races a live claude process, and the usual pre-compaction notes-checkpoint turn still fires first. Dispatched through the in-agent socket (hive-agent-sock::Request::Compact), not the broker — see hive-agent/src/todo_server.rs.

Privileged tools (by tool group)

  • Bash execution (execution) — background shell tasks. See docs/tools/bash.md.
  • Lifecycle + config (lifecycle, approvals) — manage child agents, spawn new ones, apply config commits. See docs/tools/lifecycle.md.
  • Scheduling + diagnostics (scheduling, diagnostics) — scheduled prompts, get_logs. See docs/tools/scheduling.md.
  • Forge repos (forge) — create_repo — the only agent path to create a repo under the agents/ org (direct forge token creation is disabled for agents). The repo is created in the c0re-owned agents org; the calling agent gets write collaborator access; the default branch is branch-protected (operator-team must approve merges, so the agent cannot self-merge). Opt-in; not in any default preset. See docs/tools/forge.md — Repo management.
  • Web egress (web_tools) — enables Claude's built-in WebFetch and WebSearch tools (not MCP tools; added directly to the --allowedTools list). Off by default; add the group in the P3RM1SS10NS tab and rebuild to enable.
  • Capability-gatedget_host_journal (requires read_host_journal capability set via the P3RM1SS10NS tab; orthogonal to tool groups). Full list of capabilities and their effects in docs/conventions.md#capabilities. Also documented in docs/tools/scheduling.md.
  • Matrix MCP + extra serversmcp__matrix__* tools and per-agent extra MCP config. See docs/tools/matrix.md.

Waking the agent from inside the container

External MCP servers (and any other in-container process) can inject a wake-up event into the agent's inbox via the per-agent socket at /run/hive/mcp.sock. Speak the wire protocol directly — JSON-line over the unix socket: {"cmd":"wake","from":"matrix","body": "new dm from @alice"}\n. Same shape as any other AgentRequest; see hive-sh4re::AgentRequest::Wake. (An earlier hive-agent-wake CLI wrapper existed for this but was removed — no shipped co-process daemon actually shelled out to it; every one that wakes the harness (matrix, bash) dials the socket directly, so the raw protocol is the only path now.)

The wake event lands in the broker as {from:<label>, to:<agent>, body}, waking whatever recv call the harness is currently blocked on. The next turn fires with the wake prompt formed from that message.

Identity = socket: anything that can connect to /run/hive/mcp.sock is implicitly trusted to inject these — the bind-mount is the agent's own container only.

Authoritative state

hive_ag3nt::events::Bus carries the current turn-loop state in addition to the broadcast channel and the events history. Variants:

  • Idle — sitting on Recv waiting for mail.
  • Thinkingclaude --print is running for a turn.
  • Compacting — operator-triggered /compact is in flight.

The harness flips state at the relevant transitions (set_state(Thinking) before drive_turn, set_state(Idle) after; set_state(Compacting) around an idle operator compact in turn::run_pending_compact). Exposed via /api/state.turn_state + turn_state_since (unix seconds); the agent page renders this rather than deriving from SSE events.

Tool envelope

mcp::run_tool_envelope: every MCP tool handler logs the request, runs the body, logs the result. Pre-/post-log only — the inbox status hint moved to the wake prompt + UI header.

Tool whitelist (mcp_config::ALLOWED_BUILTIN_TOOLS)

  • Allowed built-ins: Edit, Glob, Grep, Read, Write.
  • Tool-group-gated built-ins: WebFetch, WebSearch (added when the web_tools tool group is enabled — see P3RM1SS10NS tab).
  • Denied by omission (absent from the harness --tools / --allowedTools, so they "literally don't exist" in a harness turn): Bash, Task, NotebookEdit, TodoWrite.
  • Additionally in the managed-settings deny list (/etc/claude-code/managed-settings.json, un-overridable): Task, TodoWrite. Bash is not in the managed deny — see below.
  • Allowed MCP tools: as listed above (by tool group).

Bash is disallowed for the autonomous harness — shell execution goes through mcp__bash__run (background tasks with structured output + task-id tracking) instead of an interactive shell. The harness gate is --tools / --allowedTools (Bash absent from ALLOWED_BUILTIN_TOOLS), so Bash never exists in a harness turn regardless of managed settings. Bash is deliberately not in the managed-settings deny so that the operator-driven hivectl choom session — which passes neither --tools nor --allowedTools — gets claude's built-in synchronous Bash tool (inline, human-approved). That sidesteps the async mcp__bash__run completion wake landing in the wrong session (the harness inbox) for a choom-started task (#2356); choom is an operator (root) action, so built-in shell there stays within the existing trust boundary. The bash MCP server (run / status / kill) uses allowedTools = ["*"] so all mcp__bash__* tools are always available regardless of tool groups.

WebFetch / WebSearch are off by default; enable the web_tools tool group in the P3RM1SS10NS tab and rebuild the agent to enable them.