Fixes the 'genuine' bucket from #4040's triage: whitelist/allowlist (6 hits across 5 files, including a heading rename in turn-loop/mcp.md -- checked no anchor links reference it first) and the simply/obvious/easy condescending-word cluster (10 of 13 hits, real sentence-level rewrites rather than mechanical deletion so nothing reads awkwardly). Left 3 alex.Condescending hits untouched on purpose: integrations/matrix.md:271 and process/conventions.md:442 both say "non-obvious", swarm/ca.md:162 says "not obvious" -- alex matched the substring "obvious" inside a negated phrase, the literal opposite of condescending. Flagging these on the issue rather than silently leaving them out.
12 KiB
MCP surface
The harness ships an embedded MCP server (rmcp 2). 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(max?), ack_until(up_to).
send— message a peer (logical name) or the operator (to: "operator"). Useto: "<parent>"to address the topology parent without hardcoding the label; the broker resolves the sentinel at delivery time. Optionalin_reply_to: i64links the message to a prior id for thread rendering. Per-agenthyperhive.allowedRecipients(default: empty = unrestricted) limits which namessendaccepts — 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. Always an immediate peek, never blocks.max(default 1, cap 5) drains up to N rows. Each returned row is prefixed with[msg #<id>](broker row id; note the highest id seen, then pass it toack_untilto bulk-triage the batch). Graceful shutdown: when the harness receives a stop signal, the inbox becomes fenced andrecvreturns an explicitfrom: "graceful-stop"message instead of an empty inbox. This unmissably directs the agent to flush durable state (/statefiles) 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.ack_until(up_to)— bulk-mark inbox rows handled: every row with broker id<= up_tois 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, thenack_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): the higher-urgency
lifecycle events (hive_sh4re::manager::HelperEvent) are
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 container_crash,
needs_update, and approval_resolved this way. The remaining, lower-urgency lifecycle
notices — spawned, rebuilt, killed, destroyed, needs_login,
logged_in, config_ready — skip the inbox entirely: they land as
todos on the submitting agent's in-container todo socket instead
(Coordinator::push_todo/push_todo_submitter, subsystem = "core"),
which still wakes a turn (the todo-wake path — see Turn
outcomes) but via a generic "call
get_loose_ends" prompt rather than the event body itself. Full
payload shapes and routing logic in
docs/agent-lifecycle/approvals.md § Helper events.
Inbox (inbox group): get_loose_ends(agent?),
cancel_loose_end(kind, id), remind(message, delay_seconds? | at_unix_timestamp?).
get_loose_ends(agent?)— list scheduled reminders, pending approvals you submitted, and active local tasks published by external MCP daemons (e.g. running bash tasks fromhive-bash-daemon). Each row carries an id + kind forcancel_loose_end. Omitagentto list your own threads. Passagent: "<name>"to inspect a direct child agent (always accessible per topology enforcement); non-children require thequery_agent_statecapability. The"*"hive-wide query is not available on the agent socket.cancel_loose_end— hard-delete areminder, cancel a pendingapprovalrow, or clear atodorow (loose-ends-v2). Agents may only cancel rows they own; theapprovalkind 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).
There is no same-turn self-continue tool — see
Turn outcomes for why. Multi-step work rides
remind for a durable self-wake, or an in-container todo wake
(bash-task completion, forge notification, matrix activity) for work
already in flight.
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_accountsis a list of matrix identities the agent can act as (name,user_id?,homeserver); omitted for agents with no matrix provisioning. Omitnameto query self.
Always-on, no tool group (like set_status): compact().
compact— agent self-service equivalent of the operator dashboard's/compactbutton. 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 deferredcompact_pendingflag 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 — seehive-agent/src/todo_server.rs.
Privileged tools (by tool group)
- Bash execution (
execution) — background shell tasks. Seedocs/tools/bash.md. - Lifecycle + config (
lifecycle,approvals) — manage child agents, spawn new ones, apply config commits. Seedocs/tools/lifecycle.md. - Scheduling + diagnostics (
scheduling,diagnostics) — scheduled prompts,get_logs. Seedocs/tools/scheduling.md. - Forge repos (
forge) —create_repo— the only agent path to create a repo under theagents/org (direct forge token creation is disabled for agents). The repo is created in the c0re-ownedagentsorg; 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. Seedocs/tools/forge.md — Repo management. - Web egress (
web_tools) — enables Claude's built-inWebFetchandWebSearchtools (not MCP tools; added directly to the--allowedToolslist). Off by default; add the group in the P3RM1SS10NS tab and rebuild to enable. - Capability-gated —
get_host_journal(requiresread_host_journalcapability set via the P3RM1SS10NS tab; orthogonal to tool groups). Full list of capabilities and their effects indocs/process/conventions.md#capabilities. Also documented indocs/tools/scheduling.md. - Matrix MCP + extra servers —
mcp__matrix__*tools and per-agent extra MCP config. Seedocs/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 request on this
socket; see hive_core_agent_sock::Request::Wake. Every built-in producer that wakes
the harness (matrix, bash) dials the socket directly — there is no
CLI wrapper, just the raw protocol.
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-agent's events::Bus carries the current turn-loop state in
addition to the broadcast channel and the events history. Variants:
Idle— sitting onRecvwaiting for mail.Thinking—claude --printis running for a turn.Compacting— operator-triggered/compactis 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 lives in the wake prompt + UI header, not here.
Tool allowlist (mcp_config::ALLOWED_BUILTIN_TOOLS)
- Allowed built-ins:
Edit,Glob,Grep,Read,Skill,Write.Skillis what makes an installed plugin'sSKILL.mdinvokable — without it, a skill'sdescriptionfrontmatter never gets seen by the model no matter how well it matches the task. - Tool-group-gated built-ins:
WebFetch,WebSearch(added when theweb_toolstool 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.Bashis 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.