Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/docs/turn-loop/mcp.md
atlas f0844f6da3 agent permissions: one set, tool groups included, set at agent creation
mara on #5170: "dont split caps and perms. its all perms. / dont include
the now empty tool groups / nothing set -> default / also add perms to
agent creation screen".

- hive-sh4re: one `Permission` enum (messaging, meta, inbox, execution,
  web_tools, manage_root_agent) replaces `ToolGroup` and `Capability`;
  `lifecycle`/`forge` are gone. `Permission::default_for` is the default
  for an agent the swarm sets nothing for.
- The record is `{ "permissions": [...] }`. Missing key, unset field or
  no successful read yet = default; `[]` = none; a failed read after a
  good one keeps the last value.
- hive-agent reads its own key every 10s and builds each turn's --tools,
  --allowedTools and MCP config (rewritten before every turn) from it;
  the subagent daemon reads it on each start/continue. Agents get a
  read-only grant on their own key.
- hive-c0re restarts an agent only when its manage_root_agent changes.
  The import now merges meta/capabilities.json and meta/tool-groups.json
  into one list per agent and deletes both; the "import pending, don't
  read" decision is `ImportOutcome::read_allowed`, table-tested.
- swarm-controller: `GET /api/permissions` (names + default) and an
  optional `permissions` on `POST /api/agents`, stored before the job is
  queued. swarm-ui's create-agent form gets a permissions field.
- Deleted: HIVE_TOOL_GROUPS and its forward to the subagent daemon,
  tool_groups.rs, ruth's tool-group seed, the permissions rebuild job
  (PermPayload/WritePermFile/perm_change), the dashboard permissions tab
  and its stale-entries panel.

Part of #5170.
2026-10-15 18:01:40 +02:00

13 KiB

MCP surface

The harness ships an embedded MCP server (rmcp 2). A persistent hive-mcp-http daemon serves the built-in hyperhive surface over streamable HTTP (loopback, 127.0.0.1:<services.hyperhive.agent.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 (services.hyperhive.agent.extraMcpServers) pick their own transport per entry (type = "stdio" | "http", default "stdio"): matrix and subagent each run their own persistent streamable-http listener (hive-matrix-daemon, hive-subagent-daemon), same reasoning as the built-in surface and bash's hive-bash-daemon. The GUI bridge (screen, gated on services.hyperhive.agent.gui.enable) is the one entry on the schema's stdio default — it spawns hive-screen-mcp fresh each turn. The server name is hyperhive, so the tools land in claude as mcp__hyperhive__<tool>. Each entry also has an availableToSubagents toggle (default false) — see docs/tools/subagent.md for what reaches a subagent's own --mcp-config and what doesn't.

The agent's permissions gate tool access (docs/process/conventions.md#permissions). The default (Permission::DEFAULT) is messaging, meta, inbox and execution; swarmctl agent permissions set changes it, and the next turn runs on the change.

Core tools (always available)

Messaging (messaging group): send(to, body, in_reply_to?), recv(max?), ack_until(up_to).

  • send — message a peer on this hive (logical name). Optional in_reply_to: i64 links the message to a prior id for thread rendering. Per-agent services.hyperhive.agent.allowedRecipients (default: empty = unrestricted) limits which names send accepts — useful for sandboxing. The broker refuses to: "operator". The error points an agent that has the matrix MCP tools at matrix (matrix), and tells one without them that the operator reads its set_status line, its turn output on the dashboard, and its logs.
  • recv — drain inbox. Always an immediate peek, never blocks. max (default 1, cap 5) drains up to N rows. recv prefixes each returned row 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 large transcript.
  • ack_until(up_to) — bulk-mark inbox rows handled: it stamps every row with broker id <= up_to 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): the broker delivers hive_sh4re::manager::HelperEvent (one variant, NeedsUpdate — declared, but no call site constructs it) as regular inbox messages (same recv path; body is a JSON object with an event discriminant field). A container crash used to be the other variant (ContainerCrash, routed to ruth) but this tree no longer has that path — a crash now only produces a log line + swarm-level grafana alert, no agent turn at all (docs/scheduler/observability.md). Lifecycle transitions the job-queue scheduler or crash watcher drive directly — stop/kill, destroy, a flake-rev login or logout state change — reach no individual agent: they publish onto a swarm-wide NATS JetStream stream (swarm_notices::notify, hive-c0re/src/swarm_notices.rs); every hive is swarm-controlled.

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

  • get_loose_ends() — list this agent's own open threads: pending reminders, open todos pushed by an in-container subsystem (matrix, bash, forge, or any user-configured MCP server), an undelivered-inbox count, and unread matrix notifications. Always the caller's own rows. Full per-variant wire shape in docs/process/conventions.md § Loose-ends wire shape.
  • cancel_loose_end — hard-delete a reminder or clear a todo row (loose-ends-v2). Agents may only cancel rows they own.
  • 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).

No same-turn self-continue tool exists — 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): get_agent_meta(name?).

  • 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 permission needed: set_status(text), compact(), mark_todos_done(ids).

  • 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. Not gated by the meta group — every agent needs to keep its dashboard status chip current regardless of which optional groups it holds.
  • 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.
  • mark_todos_done(ids) — bulk-clear specific loose-ends-v2 todo rows by id in one call, instead of cancel_loose_ending each one. List-based: pass the ids get_loose_ends actually showed you reviewed; unknown or already-done ids are silently skipped. Dials the same in-agent todo socket as cancel_loose_end's todo kind.

Privileged tools (by permission)

  • Bash execution (execution) — background shell tasks. See docs/tools/bash.md.
  • Subagent spawning — nested sessions on the agent's own runtime (claude or ACP, via hive-runtime) as background tasks, shipped default-on like bash execution; no permission gates it. See docs/tools/subagent.md.
  • Config changes — no permission: they go through a forge PR on agent-configs/<name> — see docs/agent-lifecycle/config-changes.md.
  • 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.
  • Recovery mounts (manage_root_agent) — no tool: every agent's state and config mounted into this agent's container. See docs/process/conventions.md#permissions.
  • Matrix MCP + extra servers — mcp__matrix__* tools and per-agent extra MCP config. See docs/tools/matrix.md.

Waking the agent from inside the container

The built-in producers (matrix, bash, forge) upsert a todo on the harness's in-agent socket (HIVE_AGENT_SOCKET, UpsertTodo — see docs/tools/bash.md § Completion as a todo (loose-ends v2) for the full upsert/signal/clear mechanism): a new or changed summary makes the harness signal its own turn loop, with no hive-c0re round-trip. An external MCP server (or any other in-container process) can push its own todo the same way — subsystem is any string.

The host-served per-agent socket at /run/hive/mcp.sock carries a lower-level Wake request (hive_core_agent_sock::Request::Wake { from, body } — JSON-line, {"cmd":"wake","from":"matrix","body":"new dm from @alice"}\n) that drops the body straight into the broker inbox as {from:<label>, to:<agent>, body}, bypassing the todo layer entirely. hive-c0re handles it; matrix, bash and forge don't call it. Identity = socket: anything that can connect to /run/hive/mcp.sock is implicitly trusted to inject one — 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 on Recv waiting for mail.
  • Thinking — claude --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 lives in the wake prompt + UI header, not here.

Tool allowlist (hive_sh4re::permissions::ALLOWED_BUILTIN_TOOLS)

The built-in list and the --tools value it resolves to live in hive-sh4re alongside Permission, not in the harness, because the subagent daemon spawns its own claude and must resolve the same set — see docs/tools/subagent.md.

  • Allowed built-ins: Edit, Glob, Grep, Read, Skill, Write. Skill is what makes an installed plugin's SKILL.md invokable — without it, a skill's description frontmatter never gets seen by the model no matter how well it matches the task.
  • Permission-gated built-ins: WebFetch, WebSearch (added once the agent holds web_tools).
  • 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 isn't in the managed deny — see below.
  • Allowed MCP tools: as listed above (by permission).

The autonomous harness disallows Bash — 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 agent <name> 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; choom is an operator action (root or hive-admin), 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 available whenever the agent holds execution.

WebFetch / WebSearch are off by default; grant web_tools (swarmctl agent permissions set) to enable them from the next turn.