Compare commits

..
8 changed files with 287 additions and 383 deletions

View file

@ -354,8 +354,6 @@ docs/
web-ui/agent.md (header, terminal, composer, web-ui/agent.md (header, terminal, composer,
inbox, live view, per-agent endpoints, stats) inbox, live view, per-agent endpoints, stats)
turn-loop.md claude invocation, wake prompt, MCP tool surface turn-loop.md claude invocation, wake prompt, MCP tool surface
tools/ per-group tool docs (bash.md, lifecycle.md,
scheduling.md, matrix.md)
approvals.md approval flow, manager policy, helper events approvals.md approval flow, manager policy, helper events
persistence.md sqlite dbs, retention, state dir layout persistence.md sqlite dbs, retention, state dir layout
terminal-rendering.md per-agent terminal row taxonomy (as built) terminal-rendering.md per-agent terminal row taxonomy (as built)

View file

@ -1,65 +0,0 @@
# Bash execution tools
Background shell execution via `hive-bash-mcp`. Tools land as
`mcp__bash__<tool>` (the MCP server name is `bash`, not `hyperhive`).
Available on every agent unconditionally — `harness-base.nix` always
injects bash into `hyperhive.extraMcpServers` (with `allowedTools =
["*"]`), so `mcp__bash__*` is in `--allowedTools` for every claude
invocation regardless of tool groups.
## Tools
### `run(cmd, timeout_secs?, wait_seconds?)`
Submit a shell command for background execution (`sh -c <cmd>`).
Stdout and stderr stream to `harness/bash-tasks/<id>.{out,err}`.
When the task completes (or times out, or the process errors), the
harness fires a wake with `from: "bash-task-<id>"` and the exit code
+ last stdout lines in the body; handle it on a future turn.
- `timeout_secs` — kill the task after N seconds and mark it
`timed_out`. Omit for no timeout (runs until natural exit).
- `wait_seconds` — inline poll before returning (capped at 30).
When the task finishes within the window the full status is
returned immediately and no wake is fired; when the window expires
the task keeps running and the normal `task started: id=<id>`
response is returned. **Defaults to 3** — pass `wait_seconds: 0`
to disable inline waiting and always get the immediate response.
Exposed as `mcp__bash__run`.
### `status(id, wait_seconds?)`
Poll the status of a task submitted with `run`. Returns:
- `status``pending` / `running` / `done` / `timed_out` / `interrupted`
- `exit_code` — set when done
- run duration
- last 4 KiB of stdout and stderr (full output in the `.out` / `.err` files)
`wait_seconds` — optional inline poll (capped at 30): when the task
finishes within the window the full status is returned immediately.
Useful to avoid a separate round-trip when the task is expected to
finish soon.
Tasks marked `interrupted` had their process killed by a harness
restart; a best-effort wake was still sent so the agent is not
silently blocked.
Exposed as `mcp__bash__status`.
## Namespace note
`run` and `status` live in the `bash` MCP server, not `hyperhive`. So
the tool names in claude are `mcp__bash__run` and `mcp__bash__status`.
The `Bash` built-in tool is blocked — all shell execution goes through
this structured path so tasks get task-id tracking and structured output.
## Relationship to the `execution` tool group
`ToolGroup::Execution` exists and appears in `AGENT_DEFAULT`, but its
`tools()` returns `["run", "status"]` which the harness expands to
`mcp__hyperhive__run` / `mcp__hyperhive__status` — tools that don't
exist in the hyperhive MCP server (dead entries). Removing `execution`
from an agent's groups has no effect on bash availability. Bash is
registered separately via the `extraMcpServers` path described above.

View file

@ -1,91 +0,0 @@
# Lifecycle and approvals tools
Two tool groups govern agent lifecycle management and config changes.
Both are scoped to **direct children only** (topology-enforced: the
server rejects any name that is not a direct child of the calling
agent per `topology.json`). Privileged agents (e.g. ruth) may operate
on any sub-agent — the topology scope applies to all others.
## `lifecycle` tool group
No operator approval required. Direct children only.
### `kill(name)`
Graceful stop. Container state is preserved; recreating the agent
reuses prior config and credentials.
### `start(name)`
Start a stopped sub-agent.
### `restart(name)`
Stop + start in one call.
### `update(name)`
Rebuild: re-applies the current hyperhive flake + `agent.nix`,
then restarts. Idempotent — safe to call repeatedly. Used in response
to `needs_update` system events.
### `list_containers()`
List all **descendant** containers (children + their subtrees) with
running status. Topology-scoped to descendants only.
## `approvals` tool group
Config changes and new-agent spawns route through the operator
approval queue. Direct children only (topology-enforced).
### `request_init_config(name, description?)`
Step 1 of spawning a new direct child agent. Queues an `InitConfig`
approval; on operator approve, hive-c0re seeds the proposed config
repo at `/agents/<name>/config/agent.nix` with a default template and
delivers a `config_ready` system event. Then edit `agent.nix`, commit,
and call `request_apply_commit` with the commit sha — the first
`ApplyCommit` on a freshly-init'd config creates the container.
Fails if a proposed config repo for `name` already exists.
`name` is ≤ 9 characters. The operator can also spawn an empty agent
via the dashboard `◆ R3QU3ST SP4WN` button, which routes via
`HostRequest::RequestSpawn`.
### `request_apply_commit(agent, commit_ref, description?)`
Step 2 of spawning (or updating an existing child's config). Submit a
commit sha from the child's proposed config repo for operator
approval. On approve, hive-c0re rebuilds the container with the
pinned commit.
`commit_ref` must be a 7-40 char hex sha (branch/tag names are
rejected — the approval pins the exact commit). `agent` must be a
direct child. Topology-enforced.
### `request_update_meta_inputs(inputs?, description?)`
Queue an approval to run `nix flake update [inputs...]` on the meta
flake. Pass specific input names (e.g. `["bitburner-agent"]`) or omit
/ pass `[]` for all inputs. Returns immediately; the lock update runs
on operator approval.
Does NOT trigger container rebuilds — call `update(name)` on affected
agents after the approval resolves.
## Boundary summary
| Operation | Requires approval? | Scope |
|---|---|---|
| `kill` / `start` / `restart` / `update` | No | Direct children |
| `list_containers` | No | All descendants |
| `request_init_config` | Yes (InitConfig) | New direct child only |
| `request_apply_commit` | Yes (ApplyCommit) | Direct children |
| `request_update_meta_inputs` | Yes (MetaUpdate) | Meta flake (global) |
## See also
- [`docs/approvals.md`](../approvals.md) — full approval flow, kinds,
helper events (`config_ready`, `approval_resolved`), flake.lock
validation.

View file

@ -1,62 +0,0 @@
# Matrix MCP tools and extra MCP servers
## Built-in matrix MCP (`mcp__matrix__*`)
When `hyperhive.matrix.enable = true` and the host-level matrix
tuwunel is configured, the harness auto-injects `hive-matrix-mcp` as
a second stdio MCP server. Tools land as `mcp__matrix__<name>`:
### Messaging
- `send_message(room, body)` — send a markdown message; `room`
accepts `!id:server` or `#alias:server`, daemon resolves either
- `send_dm(user_id, body)` — send a direct message to a matrix user
- `send_reply(room, event_id, body)` — threaded reply to a specific
event
- `send_reaction(room, event_id, key)` — react to a message with an
emoji key
### Reading
- `read_room(room, limit?)` — recent timeline events
- `list_rooms()` — enumerate joined rooms
(`{ id, canonical_alias, name, member_count }` per room)
- `list_room_members(room)` — members of a room
### Receipts
- `mark_read(room, event_id)` — advance the read receipt
## Architecture
The daemon (`hive-matrix-daemon`) holds the long-running matrix-sdk
`Client` + sync loop; the stdio bridge (`hive-matrix-mcp`) is spawned
per turn and forwards tool calls over `/run/hive-matrix.sock`. Both
silently exit when `<state>/matrix-token` is absent (account not yet
provisioned).
Incoming room events wake the agent via `AgentRequest::Wake` with
`from: "matrix"` and a teaser body
(`[matrix] <sender> in <room>: <first-100c>…`).
See [`docs/matrix.md`](../matrix.md) for the homeserver setup,
provisioning flow, and federation config.
## Extra MCP servers (per-agent)
Each agent's NixOS config can declare additional MCP servers via
`hyperhive.extraMcpServers.<key> = { command, args, env,
allowedTools }`. The module writes the map to
`/etc/hyperhive/extra-mcp.json`; the harness reads it at boot and
merges every entry into `--mcp-config` (under `mcpServers.<key>`)
and `--allowedTools` (as `mcp__<key>__<pattern>`).
The agent's `flake.nix` forwards every flake input to `agent.nix` as
the `flakeInputs` module arg, so external MCP-server flakes are pulled
in by adding them to `inputs.*` and referenced as
`flakeInputs.<name>.packages.${pkgs.system}.default` — the resolved
sha lands in the agent's own `flake.lock` and rolls up to meta's.
`allowedTools` defaults to `["*"]`, which expands to
`mcp__<key>__*` (every tool from that server auto-approved). Restrict
to specific tool names when you want finer control.

View file

@ -1,68 +0,0 @@
# Scheduling and diagnostics tools
## `scheduling` tool group
Scheduled prompts fan a message body out to one or more agent inboxes
at a future time, optionally recurring. All scheduling ops go through
the operator approval queue (even self-targeted schedules — use
`remind` for unapproved self-wake). Authorization for read/cancel/edit
ops: you can act on schedules you own or any owned by a sub-agent in
your topology subtree.
### `request_schedule_prompt(targets, body, first_fire_at_unix, interval_seconds?, description?)`
Queue an operator-approval for a scheduled prompt. On approve,
`body` is fanned out to each agent in `targets` at
`first_fire_at_unix` (Unix timestamp). Recurring when `interval_seconds`
is set, one-shot otherwise.
Catch-up clamp: if hive-c0re is down across multiple intervals, only
ONE delayed fire happens on resume (per recurring schedule). The
skipped-cycle count surfaces in the per-target `last_result` for
audit.
### `edit_schedule(id, body?, description?, interval_seconds?, next_fire_at_unix?, targets_add?, targets_remove?)`
Partial-update a schedule. Pass only the fields to change; absent
fields are left alone. `targets_add` / `targets_remove` mutate the
recipient list in the same transaction — re-adding a previously
cancelled target drops its tombstone and starts fresh. Clearing
`interval_seconds` to null flips recurring → one-shot. Refuses
cancelled rows (terminal state).
### `cancel_schedule(id, targets?)`
Cancel a schedule. Omit `targets` / pass empty to cancel the whole
schedule; pass a list to cancel just those recipients (the schedule
auto-cancels when every target is removed).
### `fire_schedule_now(id)`
Fire a scheduled prompt out of band immediately. Recurring schedules
keep their cadence — the manual fire is additive. One-shot schedules
are consumed by the manual fire and cancelled afterwards.
### `list_schedules()`
Snapshot every schedule (active + cancelled-but-not-reaped): id,
owner, body, per-target `last_fired_at` + `last_result`,
`next_fire_at_unix`, `interval_seconds`. Use to look up an id before
cancelling, or to audit upcoming wake-ups across the swarm.
## `diagnostics` tool group
### `get_logs(agent, lines?)`
Fetch recent journal lines for a sub-agent container. Useful for
diagnosing MCP-registration failures, startup crashes, plugin install
errors, or any harness issue you can't see from inside the container.
Pass the plain logical agent name (e.g. `"gui"`) — hive-c0re resolves
the machine name (`h-<name>`). `lines` defaults to 50, host-capped at 500.
## See also
- `remind` (no-approval self-wake path) — documented in
[`docs/turn-loop.md`](../turn-loop.md).
- [`docs/approvals.md`](../approvals.md) — approval flow for
`request_schedule_prompt`.

View file

@ -5,8 +5,9 @@ claude has access to in return.
## The loop ## The loop
Each agent harness (`hive serve`, role set via `$HIVE_ROLE` — always Each agent harness (`hive serve`, with role picked from `$HIVE_ROLE`
`"agent"`, one binary) runs: `"agent"` for sub-agents, `"manager"` for the manager — one
binary, not two) runs:
1. Long-poll `Recv` on its socket. The host-side broker 1. Long-poll `Recv` on its socket. The host-side broker
(`broker.rs::recv_blocking_batch`) returns immediately if there's (`broker.rs::recv_blocking_batch`) returns immediately if there's
@ -291,75 +292,115 @@ The harness ships an embedded MCP server (rmcp 1.7). Claude launches
it as a stdio child via `--mcp-config`. The hyperhive socket name is it as a stdio child via `--mcp-config`. The hyperhive socket name is
`hyperhive`, so the tools land in claude as `mcp__hyperhive__<tool>`. `hyperhive`, so the tools land in claude as `mcp__hyperhive__<tool>`.
Tool access is gated by tool groups (`HIVE_TOOL_GROUPS`). The default ### Sub-agent tools
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) - `send(to, body, in_reply_to?)` — message a peer (logical agent
name), another agent, or the operator (recipient `operator`,
**Messaging** (`messaging` group): `send(to, body, in_reply_to?)`, surfaces in the dashboard inbox). Use `to: "<parent>"` to
`recv(wait_seconds?, max?)`, `ask(question, options?, multi?, address the agent's topology parent without knowing its label;
ttl_seconds?, to?)`, `answer(id, answer)`. the broker resolves the sentinel at delivery time (falls back
to `operator` for root agents). Optional `in_reply_to: i64`
- `send` — message a peer (logical name) or the operator links this message to a prior message id for thread rendering
(`to: "operator"`). Use `to: "<parent>"` to address the topology in the dashboard message flow and the per-agent inbox.
parent without hardcoding the label; the broker resolves the - `recv(wait_seconds?, max?)` — drain inbox messages. Without
sentinel at delivery time. Optional `in_reply_to: i64` links the `wait_seconds` (or with `0`) returns immediately, a cheap
message to a prior id for thread rendering. "anything pending?" peek. Positive value parks the turn up
- `recv` — drain inbox. Without `wait_seconds` (or `0`) returns to that many seconds (cap 180) — incoming messages wake
immediately. Positive value parks the turn up to that many seconds instantly, otherwise returns empty at the timeout. `max`
(cap 180) — incoming messages wake instantly. `max` (default 1, cap (default 1, server-side cap 32) drains up to N popped rows
32) drains up to N rows; `wait_seconds` applies to the first, then in one round-trip; `wait_seconds` applies to the *first*
drains up to `max` total. message, then the call drains up to `max` total.
- `ask` — surface a structured question to the operator (default) or - `ask(question, options?, multi?, ttl_seconds?, to?)`
a peer agent (`to: "<agent>"`). Non-blocking — returns a question surface a structured question. Same shape as the manager's;
id; the answer arrives as a `question_answered` system event in the recipient defaults to the operator (dashboard) but can be set
asker's inbox. `options` is advisory; `multi=true` renders as to a peer agent name via `to: "<agent>"`. Answer routes back
checkboxes; `ttl_seconds` auto-cancels with answer `[expired]`. to the asker's own inbox as `HelperEvent::QuestionAnswered`
- `answer` — respond to a `question_asked` event routed to this via `coord.notify_agent`. For peer questions the recipient
agent. Strict authorisation: only the declared target can answer. sees a `HelperEvent::QuestionAsked` event and replies with
`answer(id, answer)`.
**Inbox** (`inbox` group): `get_loose_ends()`, - `answer(id, answer)` — respond to a `question_asked` event
`cancel_loose_end(kind, id)`, `remind(message, delay_seconds? | routed to this agent. Authorisation is strict: only the
at_unix_timestamp?)`, `request_next_turn()`. declared target (or the operator via the dashboard) can
answer.
- `get_loose_ends` — list pending questions (asked/owed) and - `get_loose_ends()` — list everything still pending against
scheduled reminders. Each row carries an id + kind for this agent: unanswered questions it asked / was asked, plus
reminders it scheduled. Each row carries an id + kind for
`cancel_loose_end`. `cancel_loose_end`.
- `cancel_loose_end` — withdraw a `question` (posts `[cancelled by - `cancel_loose_end(kind, id)` — withdraw a `question`
<self>]`), hard-delete a `reminder`, or cancel a pending `approval` (posts `[cancelled by <self>]` to unblock the asker), a
row. Agents may only cancel rows they own; the `approval` kind is `reminder` (hard-delete before fire), or (manager only) an
further restricted to the root agent (`ruth`) server-side. `approval` (transitions to `Cancelled`; sub-agents refused with a
- `remind` — schedule a reminder in this agent's own inbox. Large clear error). Sub-agents may only cancel rows they own.
payloads spill to `/agents/<self>/state/reminders/`. Pending count - `remind(message, due)` — schedule a reminder that lands in
capped at 50 per agent (`HIVE_REMIND_MAX_PENDING_PER_AGENT`). this agent's own inbox at a future time (sender shows as
- `request_next_turn` — ask the harness to start another turn `reminder`). Large payloads spill to
immediately after this one ends, even if the inbox is empty. `/agents/<self>/state/reminders/` with the inbox message a
Next turn fires with `from: "self"` and `body: "continue"`. short pointer. Each agent's pending-reminder count is capped
(default 50, override via `HIVE_REMIND_MAX_PENDING_PER_AGENT`);
scheduling a new one fails if the cap is already hit.
- `set_status(text)` — set a free-text status string visible on
the operator dashboard. Persisted to
`{state_dir}/hyperhive-status`; survives harness restarts. Pass
an empty string to clear. Validated: must be a single line and
≤ 200 Unicode characters; the server rejects multi-line or
over-length text with a clear error.
- `get_agent_meta(name?)` — fetch identity + status metadata for
an agent: `{ name, role, hyperhive_rev, running, status_text,
status_set_at, hive_name?, swarm_name? }`. Pass `name` to query
a peer (e.g. check whether a sub-agent is idle before sending it
work). Omit `name` to get your own identity stamp — replaces the
previous `whoami` tool. `running` is `true` when the container is
up. When `running` is `false` the host clears `status_text` /
`status_set_at` (they would be stale snapshots from before the
container stopped) before serving the response. Status fields are
also `None` when the target has never called `set_status` or has
cleared it. `hive_name` and `swarm_name` are present when
`services.hyperhive.hiveName` / `services.hyperhive.swarmName`
are configured on the host; omitted in single-hive deployments.
- `request_next_turn()` — ask the harness to start another turn
immediately after this one ends, even if the inbox is empty. Use for
multi-turn tasks (long builds, sequential steps) where you want to
continue without waiting for an external message. The next turn starts
with `from: "self"` and `body: "continue"`. No-op if new inbox
messages arrive before this turn ends. No args.
- `run(cmd, timeout_secs?)` — submit a shell command for
background execution (`sh -c <cmd>`). Returns a task ID immediately;
the command runs asynchronously in a harness-managed tokio task. Stdout
and stderr stream to `harness/bash-tasks/<id>.{out,err}`. When the
task completes (or times out, or the process errors), the harness wakes
the agent with a summary body — handle on a future turn. Optional
`timeout_secs`: pass a value for a deadline, or omit for no timeout
(runs until natural exit). Requires the `execution` tool group.
Exposed as `mcp__bash__run`.
- `status(id)` — poll the status of a task submitted with
`run`. Returns status (`pending`/`running`/`done`/`timed_out`/
`interrupted`), exit code, run duration, and the last 4 KiB of stdout
and stderr (full output in the `.out`/`.err` files). Tasks marked
`interrupted` had their process killed by a harness restart; a best-
effort wake was still sent so the agent is not silently blocked.
Exposed as `mcp__bash__status`.
**Meta** (`meta` group): `set_status(text)`, `get_agent_meta(name?)`. **Agent lifecycle + config tools** (direct children only; requires
`lifecycle` or `approvals` tool group):
- `set_status` — set a free-text status string visible on the - `kill(name)`, `start(name)`, `restart(name)`, `update(name)` — manage
dashboard. Single line, ≤ 200 chars. Persisted to a direct child sub-agent (graceful stop, start, stop+start, rebuild).
`{state_dir}/hyperhive-status`. Pass `""` to clear. No approval required. Topology-enforced: `name` must be a direct child
- `get_agent_meta` — fetch identity + status metadata for an agent: per `topology.json`. Server rejects all other agent names. Requires
`{ name, hyperhive_rev, running, status_text, status_set_at, `lifecycle` tool group.
hive_name?, swarm_name? }`. Omit `name` to query self. - `list_containers()` — list all descendant containers with running
status. Topology-enforced (descendants only). Requires `lifecycle`
### Privileged tools (by tool group) tool group.
- `request_init_config(name, description?)` — step 1 of spawning a new
- **Bash execution** (`execution`) — background shell tasks. See direct child agent. Queues an `InitConfig` approval; on operator
[`docs/tools/bash.md`](tools/bash.md). approve, hive-c0re seeds the proposed config repo with a default
- **Lifecycle + config** (`lifecycle`, `approvals`) — manage child `agent.nix` template. `name` must be a direct child. Topology-enforced.
agents, spawn new ones, apply config commits. See Fails if config already exists. Requires `approvals` tool group.
[`docs/tools/lifecycle.md`](tools/lifecycle.md). - `request_apply_commit(agent, commit_ref, description?)` — step 2 of
- **Scheduling + diagnostics** (`scheduling`, `diagnostics`) — spawning a new direct child (or updating an existing child's config).
scheduled prompts, `get_logs`. See Submit a commit sha from the child's proposed config repo for operator
[`docs/tools/scheduling.md`](tools/scheduling.md). approval. `agent` must be a direct child. Topology-enforced. `commit_ref`
- **Matrix MCP + extra servers**`mcp__matrix__*` tools and must be a 7-40 char hex sha. Requires `approvals` tool group.
per-agent extra MCP config. See
[`docs/tools/matrix.md`](tools/matrix.md).
### Waking the agent from inside the container ### Waking the agent from inside the container
@ -370,22 +411,170 @@ socket at `/run/hive/mcp.sock`. Two equivalent paths:
- **Shell out to `hive wake --from <label> --body <text>`** - **Shell out to `hive wake --from <label> --body <text>`**
(use `--body -` to read body from stdin). Already on the (use `--body -` to read body from stdin). Already on the
container's `PATH` since the harness binary is in container's `PATH` since the harness binary is in
`systemPackages`. Convenient for shell-script integrations and `systemPackages`. Convenient for shell-script integrations.
co-process daemons (matrix bridge, webhook listeners, scrapers). Works for both `agent` and `manager` roles.
- **Speak the wire protocol directly** — JSON-line over the - **Speak the wire protocol directly** — JSON-line over the
unix socket: `{"cmd":"wake","from":"matrix","body":"new dm unix socket: `{"cmd":"wake","from":"matrix","body":"new dm
from @alice"}\n`. Same shape as any other `AgentRequest`; from @alice"}\n`. Same shape any other AgentRequest uses;
see `hive-sh4re::AgentRequest::Wake`. see `hive-sh4re::AgentRequest::Wake`.
The wake event lands in the broker as `{from:<label>, The wake event lands in the broker as `{from:<label>,
to:<agent>, body}`, waking whatever `recv` call the harness to:<agent>, body}`, which wakes whatever `recv` call the
is currently blocked on. The next turn fires with the wake harness is currently blocked on. Next turn fires with the
prompt formed from that message. wake prompt formed from that message — claude sees "from:
matrix" (or whatever label) and reacts.
Identity = socket: anything that can connect to Identity = socket: anything that can connect to
`/run/hive/mcp.sock` is implicitly trusted to inject these — `/run/hive/mcp.sock` is implicitly trusted to inject these,
the bind-mount is the agent's own container only. which is fine because the bind-mount is the agent's own
container only.
### Built-in matrix MCP (`mcp__matrix__*`)
When `hyperhive.matrix.enable = true` (default) and the host-level
matrix tuwunel is configured, the harness auto-injects `hive-matrix-mcp`
as a second stdio MCP server. Tools land as `mcp__matrix__<name>`:
- `send_message(room, body)` — send a markdown message; `room` accepts
`!id:server` or `#alias:server`, daemon resolves either
- `send_dm(user_id, body)` — send a direct message to a matrix user
- `send_reaction(room, event_id, key)` — react to a message
- `send_reply(room, event_id, body)` — threaded reply
- `mark_read(room, event_id)` — advance the read receipt
- `list_rooms()` — enumerate joined rooms
(`{ id, canonical_alias, name, member_count }` per room)
- `list_room_members(room)` — members of a room
- `read_room(room, limit?)` — recent timeline events
The daemon (`hive-matrix-daemon`) holds the long-running matrix-sdk
`Client` + sync loop; the stdio bridge (`hive-matrix-mcp`) is spawned
per turn and forwards tool calls over `/run/hive-matrix.sock`. Both
silently exit when `<state>/matrix-token` is absent (account not yet
provisioned). Incoming room events wake the agent via `AgentRequest::Wake`
with `from: "matrix"` and a teaser body
(`[matrix] <sender> in <room>: <first-100c>…` where `<sender>` is
the matrix user id and `<room>` is the alias or id as reported by
the SDK).
### Extra MCP servers (per-agent)
Each agent's NixOS config can declare additional MCP servers via
`hyperhive.extraMcpServers.<key> = { command, args, env,
allowedTools }`. The module writes the map to
`/etc/hyperhive/extra-mcp.json`; the harness reads it at boot and
merges every entry into `--mcp-config` (under `mcpServers.<key>`)
and `--allowedTools` (as `mcp__<key>__<pattern>`). The agent's
flake.nix forwards every flake input to `agent.nix` as the
`flakeInputs` module arg, so external MCP-server flakes are pulled
in by adding them to `inputs.*` and referenced as
`flakeInputs.<name>.packages.${pkgs.system}.default` — the
resolved sha lands in the agent's own `flake.lock` and rolls up to
meta's.
### Manager tools (in addition to send/recv)
- `request_init_config(name, description?)` — first step of a
two-step spawn. Queues an `InitConfig` approval (≤9 char name);
on operator approve, hive-c0re seeds the proposed config repo
with a default `agent.nix` template and sends the manager a
`HelperEvent::ConfigReady { agent }`. The manager then edits
`agent.nix`, commits the changes, and calls `request_apply_commit`
with the commit sha — the first ApplyCommit on a freshly-init'd
config creates the container. Fails if a proposed repo for this
name already exists. (The operator can also direct-spawn an empty
agent from the dashboard's `◆ R3QU3ST SP4WN` button, which routes
via `HostRequest::RequestSpawn`.) Also available to agents for direct
children only (requires `approvals` tool group; see agent lifecycle
tools section above).
- `kill(name)` — graceful stop. No approval required. Also available to
agents for direct children only (requires `lifecycle` tool group; see
agent lifecycle tools section above).
- `start(name)` — start a stopped sub-agent. No approval. Also available
to agents for direct children only.
- `restart(name)` — stop + start. No approval. Also available to agents
for direct children only.
- `update(name)` — rebuild (re-applies the current hyperhive flake
+ agent.nix, restarts). No approval, idempotent. Manager calls
this on receipt of a `needs_update` system event. Also available to
agents for direct children only.
- `request_apply_commit(agent, commit_ref)` — submit a config
change for any agent (`root` for the manager's own config) for
operator approval. Also available to agents for direct children only
(requires `approvals` tool group; see agent lifecycle tools section
above).
- `request_update_meta_inputs(inputs?, description?)` — queue an
approval to run `nix flake update [inputs...]` on the meta flake.
Pass specific input names (e.g. `["bitburner-agent"]`) or omit
for all. Returns immediately; lock update runs on operator
approval. Does NOT trigger rebuilds — call `update(name)` on
affected agents after approval resolves.
- `ask(question, options?, multi?, ttl_seconds?, to?)`
surface a structured question to the operator (default) or a
sub-agent (`to: "<agent>"`). Non-blocking — returns the
queued question id; the answer arrives later as
`HelperEvent::QuestionAnswered { id, question, answer,
answerer }` in the asker's inbox. Options always render
alongside a free-text fallback; `multi=true` renders options
as checkboxes. `ttl_seconds` auto-cancels with answer
`[expired]` (and `answerer: "ttl-watchdog"`) after the
deadline (useful for time-sensitive decisions that become moot
if no one has responded). The operator can also manually
cancel with `[cancelled]` via the dashboard.
- `answer(id, answer)` — respond to a `question_asked` event
that was routed to the manager (a sub-agent did
`ask(to: "manager", ...)`). Surfaces in the asker's inbox as
the same `question_answered` event.
- `get_logs(agent, lines?)` — fetch recent journal lines for a
sub-agent container (diagnose MCP-registration failures,
startup crashes, etc.). Pass the plain logical agent name;
hive-c0re resolves the machine name (`h-<name>`, manager
`root`). `lines` defaults to 50, host-capped at 500.
- `request_schedule_prompt(targets, body, first_fire_at_unix, interval_seconds?, description?)`
queue an operator-approval for a scheduled prompt. On approve,
`body` is fanned out to each `targets` agent at
`first_fire_at_unix`; recurring if `interval_seconds` is set,
one-shot otherwise. Even self-targeted schedules go through
approval (use `remind` for unapproved self-wake). Long downtime
fires once per recurring row on resume (catch-up clamp).
- `edit_schedule(id, body?, description?, interval_seconds?, next_fire_at_unix?, targets_add?, targets_remove?)`
partial-update a schedule. Pass only the fields to
change; absent fields are left alone. `targets_add` / `targets_remove`
mutate the recipient list in the same transaction; re-adding a
previously-cancelled target drops its tombstone + history (fresh
start). Clearing a scalar (e.g. `interval_seconds: null`) flips
recurring→one-shot. Refuses cancelled rows. Same authorization as
`cancel_schedule`.
- `cancel_schedule(id, targets?)` — cancel a schedule. Omit
`targets` / pass empty to cancel the whole schedule; pass a list
to cancel just those recipients (auto-cancels when every target
is removed). Authorization: manager can cancel schedules it owns
or any owned by a sub-agent in its topology subtree.
- `fire_schedule_now(id)` — fire a scheduled prompt out of band.
Runs the per-target fan-out once immediately. Recurring schedules
keep their cadence (the manual fire is additive); one-shot
schedules are consumed by the fire and cancelled afterwards. Same
authorization rules as `cancel_schedule`.
- `list_schedules()` — snapshot every schedule (active +
cancelled-but-not-reaped): id, owner, body, per-target
`last_fired_at` + `last_result`, `next_fire_at_unix`,
`interval_seconds`. Use to look up an id before cancelling or to
audit upcoming wake-ups across the swarm.
- `remind` / `get_loose_ends` / `cancel_loose_end` / `set_status`
/ `get_agent_meta` — same as the sub-agent tools above.
`get_loose_ends` scopes to the manager's own items by default;
pass `agent: "*"` for a hive-wide view, or `agent: "<name>"`
to inspect one agent.
`cancel_loose_end` may cancel any agent's row.
The boundary: lifecycle ops on *existing* direct children
(`kill`/`start`/`restart`/`update`) are discretionary — no operator
approval required (manager can do so for any sub-agent; agents can do so
only for direct children with `lifecycle` tool group). Creating a new
agent (`request_init_config``request_apply_commit` for the first sha)
and changing any agent's config (`request_apply_commit`) still go through
the approval queue (same topology scoping for agents: direct children only
with `approvals` tool group).
### Authoritative state ### Authoritative state
@ -415,7 +604,7 @@ status hint moved to the wake prompt + UI header.
`web_tools` tool group is enabled — see P3RM1SS10NS tab). `web_tools` tool group is enabled — see P3RM1SS10NS tab).
- Denied by omission or `claude-settings.json` deny list: `Bash`, - Denied by omission or `claude-settings.json` deny list: `Bash`,
`Task`, `NotebookEdit`, `TodoWrite`. `Task`, `NotebookEdit`, `TodoWrite`.
- Allowed MCP tools: as listed above (by tool group). - Allowed MCP tools: as listed above per flavor.
`Bash` is disallowed — shell execution goes through `Bash` is disallowed — shell execution goes through
`mcp__bash__run` (background tasks with structured output + `mcp__bash__run` (background tasks with structured output +

View file

@ -8,7 +8,7 @@ Tools (hyperhive surface):
- `mcp__hyperhive__ask(question, options?, multi?, ttl_seconds?, to?)` — surface a structured question to the human operator (default, or `to: "operator"`) OR a peer agent (`to: "<agent-name>"`). Returns immediately with a question id — do NOT wait inline. When the recipient answers, a system message with event `question_answered { id, question, answer, answerer }` lands in your inbox; handle it on a future turn. Use this for clarifications, permission for risky actions, choice between options, or peer Q&A without burning regular inbox slots. `options` is advisory: a short fixed-choice list when applicable, otherwise leave empty for free text. `multi: true` lets the answerer pick multiple (checkboxes), answer comes back comma-joined. `ttl_seconds` auto-cancels with answer `[expired]` (and `answerer: "ttl-watchdog"`) when the decision becomes moot. - `mcp__hyperhive__ask(question, options?, multi?, ttl_seconds?, to?)` — surface a structured question to the human operator (default, or `to: "operator"`) OR a peer agent (`to: "<agent-name>"`). Returns immediately with a question id — do NOT wait inline. When the recipient answers, a system message with event `question_answered { id, question, answer, answerer }` lands in your inbox; handle it on a future turn. Use this for clarifications, permission for risky actions, choice between options, or peer Q&A without burning regular inbox slots. `options` is advisory: a short fixed-choice list when applicable, otherwise leave empty for free text. `multi: true` lets the answerer pick multiple (checkboxes), answer comes back comma-joined. `ttl_seconds` auto-cancels with answer `[expired]` (and `answerer: "ttl-watchdog"`) when the decision becomes moot.
- `mcp__hyperhive__answer(id, answer)` — answer a question that was routed to YOU. You'll see one in your inbox as a `question_asked { id, asker, question, options, multi }` system event when a peer or the operator calls `ask(to: "<your-name>", ...)`. The answer surfaces in the asker's inbox as a `question_answered` event. Strict authorisation: you can only answer questions where you are the declared target. - `mcp__hyperhive__answer(id, answer)` — answer a question that was routed to YOU. You'll see one in your inbox as a `question_asked { id, asker, question, options, multi }` system event when a peer or the operator calls `ask(to: "<your-name>", ...)`. The answer surfaces in the asker's inbox as a `question_answered` event. Strict authorisation: you can only answer questions where you are the declared target.
- `mcp__hyperhive__get_loose_ends()` — list your loose ends: unanswered questions where you're asker (waiting on someone) or target (owing a reply), plus reminders you've scheduled that haven't fired. No args, cheap server-side sweep. Useful at turn start to remember what's outstanding without scanning inbox archaeology. - `mcp__hyperhive__get_loose_ends()` — list your loose ends: unanswered questions where you're asker (waiting on someone) or target (owing a reply), plus reminders you've scheduled that haven't fired. No args, cheap server-side sweep. Useful at turn start to remember what's outstanding without scanning inbox archaeology.
- `mcp__hyperhive__cancel_loose_end(kind, id)` — cancel one of your own open threads. `kind` is `"question"` (the asker — you, in this case — gets a `[cancelled by <you>]` answer so the waiter unblocks), `"reminder"` (hard-deleted before it fires), or `"approval"` (withdraws a pending approval you submitted that got superseded — root agent only; the server rejects this kind for all other callers). `id` from the matching `get_loose_ends` row or the original submission reply. - `mcp__hyperhive__cancel_loose_end(kind, id)` — cancel one of your own open threads. `kind` is `"question"` (the asker — you, in this case — gets a `[cancelled by <you>]` answer so the waiter unblocks), `"reminder"` (hard-deleted before it fires), or `"approval"` (withdraws a pending approval you submitted that got superseded — operator-approved path, so requires the `approvals` tool group). `id` from the matching `get_loose_ends` row or the original submission reply.
- `mcp__hyperhive__remind(message, delay_seconds? | at_unix_timestamp?, file_path?)` — schedule a message to land in your *own* inbox at a future time (sender shows as `reminder`). Set exactly one of `delay_seconds` (relative) or `at_unix_timestamp` (absolute). Use for self-paced follow-ups instead of blocking a whole turn on a long `recv` wait. A large `message` auto-spills to a file under `/agents/{label}/state/reminders/`; pass `file_path` to point at one yourself. Each agent's pending-reminder count is capped (default 50) — the tool will error if the cap is already reached. - `mcp__hyperhive__remind(message, delay_seconds? | at_unix_timestamp?, file_path?)` — schedule a message to land in your *own* inbox at a future time (sender shows as `reminder`). Set exactly one of `delay_seconds` (relative) or `at_unix_timestamp` (absolute). Use for self-paced follow-ups instead of blocking a whole turn on a long `recv` wait. A large `message` auto-spills to a file under `/agents/{label}/state/reminders/`; pass `file_path` to point at one yourself. Each agent's pending-reminder count is capped (default 50) — the tool will error if the cap is already reached.
- `mcp__hyperhive__set_status(text)` — set a free-text status visible on the operator dashboard. **Call this at the start of every task** to say what you're working on (e.g. `"processing matrix messages"`, `"fixing #319 model priority"`, `"idle"`). Single line, ≤200 chars — the dashboard renders this as a short chip, so longer multi-line text is rejected. Pass an empty string to clear. Persists across harness restarts. - `mcp__hyperhive__set_status(text)` — set a free-text status visible on the operator dashboard. **Call this at the start of every task** to say what you're working on (e.g. `"processing matrix messages"`, `"fixing #319 model priority"`, `"idle"`). Single line, ≤200 chars — the dashboard renders this as a short chip, so longer multi-line text is rejected. Pass an empty string to clear. Persists across harness restarts.
- `mcp__hyperhive__get_agent_meta(name?)` — fetch identity + status metadata for an agent: canonical `name`, current `hyperhive_rev`, plus self-reported `status` text (set via `set_status`) and how long ago it was set. Also returns the hive + swarm display names (`hive_name`, `swarm_name`) when the operator has configured `services.hyperhive.{hiveName, swarmName}`; both lines omitted when unset. Pass `name` to query a peer (e.g. check whether iris is idle before pinging them). Omit `name` to get your own trustworthy identity stamp — useful for state files, commit messages, cross-agent attribution that won't drift across renames or session-continue boundaries where the system-prompt label could be stale. - `mcp__hyperhive__get_agent_meta(name?)` — fetch identity + status metadata for an agent: canonical `name`, current `hyperhive_rev`, plus self-reported `status` text (set via `set_status`) and how long ago it was set. Also returns the hive + swarm display names (`hive_name`, `swarm_name`) when the operator has configured `services.hyperhive.{hiveName, swarmName}`; both lines omitted when unset. Pass `name` to query a peer (e.g. check whether iris is idle before pinging them). Omit `name` to get your own trustworthy identity stamp — useful for state files, commit messages, cross-agent attribution that won't drift across renames or session-continue boundaries where the system-prompt label could be stale.

View file

@ -383,7 +383,7 @@ fn loose_end_kind_label(kind: hive_sh4re::CancelLooseEndKind) -> &'static str {
} }
/// Format helper for `get_agent_meta`: renders an agent's identity + /// Format helper for `get_agent_meta`: renders an agent's identity +
/// current status as a short human-readable block. `name`, /// current status as a short human-readable block. `name`, `role`,
/// `hyperhive_rev`, and `running` are always shown; `status` only /// `hyperhive_rev`, and `running` are always shown; `status` only
/// appears when one is set, otherwise the line reads `status: <none>`. /// appears when one is set, otherwise the line reads `status: <none>`.
/// When `running` is false the host has already cleared `status_text` /// When `running` is false the host has already cleared `status_text`
@ -978,10 +978,12 @@ impl AgentServer {
// IMPORTANT: this tool is only available when the `lifecycle` tool group // IMPORTANT: this tool is only available when the `lifecycle` tool group
// is granted to this agent. hive-c0re enforces the topology check // is granted to this agent. hive-c0re enforces the topology check
// server-side: the call is rejected unless `name` is a direct child. // server-side: the call is rejected unless `name` is a direct child.
#[tool(description = "Stop a direct child sub-agent container (graceful). \ #[tool(
description = "Stop a direct child sub-agent container (graceful). \
Only succeeds if `name` is a direct child of this agent in the topology \ Only succeeds if `name` is a direct child of this agent in the topology \
tree the server enforces this. No approval required. \ tree the server enforces this. No approval required. \
State dir is kept; recreating the agent reuses prior config + credentials.")] State dir is kept; recreating the agent reuses prior config + credentials."
)]
async fn kill(&self, Parameters(args): Parameters<KillArgs>) -> String { async fn kill(&self, Parameters(args): Parameters<KillArgs>) -> String {
let log = format!("{args:?}"); let log = format!("{args:?}");
let name = args.name.clone(); let name = args.name.clone();
@ -1010,10 +1012,7 @@ impl AgentServer {
let (resp, retries) = self let (resp, retries) = self
.dispatch(hive_sh4re::Request::Update { name: args.name }) .dispatch(hive_sh4re::Request::Update { name: args.name })
.await; .await;
annotate_retries( annotate_retries(format_ack(resp, "update", format!("updated {name}")), retries)
format_ack(resp, "update", format!("updated {name}")),
retries,
)
}) })
.await .await
} }
@ -1030,7 +1029,9 @@ impl AgentServer {
)] )]
async fn list_containers(&self) -> String { async fn list_containers(&self) -> String {
run_tool_envelope("list_containers", String::new(), async move { run_tool_envelope("list_containers", String::new(), async move {
let (resp, retries) = self.dispatch(hive_sh4re::Request::ListDescendants).await; let (resp, retries) = self
.dispatch(hive_sh4re::Request::ListDescendants)
.await;
let body = match resp { let body = match resp {
Ok(SocketReply::Containers(containers)) => { Ok(SocketReply::Containers(containers)) => {
if containers.is_empty() { if containers.is_empty() {
@ -1175,9 +1176,11 @@ impl AgentServer {
// IMPORTANT: this tool is only available when the `lifecycle` tool group // IMPORTANT: this tool is only available when the `lifecycle` tool group
// is granted to this agent. hive-c0re enforces the topology check // is granted to this agent. hive-c0re enforces the topology check
// server-side: the call is rejected unless `name` is a direct child. // server-side: the call is rejected unless `name` is a direct child.
#[tool(description = "Start a stopped direct child sub-agent container. \ #[tool(
description = "Start a stopped direct child sub-agent container. \
Only succeeds if `name` is a direct child of this agent in the topology \ Only succeeds if `name` is a direct child of this agent in the topology \
tree the server enforces this. No approval required.")] tree the server enforces this. No approval required."
)]
async fn start(&self, Parameters(args): Parameters<StartArgs>) -> String { async fn start(&self, Parameters(args): Parameters<StartArgs>) -> String {
let log = format!("{args:?}"); let log = format!("{args:?}");
let name = args.name.clone(); let name = args.name.clone();
@ -1185,14 +1188,12 @@ impl AgentServer {
let (resp, retries) = self let (resp, retries) = self
.dispatch(hive_sh4re::Request::Start { name: args.name }) .dispatch(hive_sh4re::Request::Start { name: args.name })
.await; .await;
annotate_retries( annotate_retries(format_ack(resp, "start", format!("started {name}")), retries)
format_ack(resp, "start", format!("started {name}")),
retries,
)
}) })
.await .await
} }
#[tool( #[tool(
description = "Fetch recent journal log lines for a sub-agent container. Useful \ description = "Fetch recent journal log lines for a sub-agent container. Useful \
for diagnosing MCP server registration failures, startup crashes, plugin install \ for diagnosing MCP server registration failures, startup crashes, plugin install \
@ -1405,7 +1406,9 @@ impl AgentServer {
)] )]
async fn list_schedules(&self) -> String { async fn list_schedules(&self) -> String {
run_tool_envelope("list_schedules", String::new(), async move { run_tool_envelope("list_schedules", String::new(), async move {
let (resp, retries) = self.dispatch(hive_sh4re::Request::ListSchedules).await; let (resp, retries) = self
.dispatch(hive_sh4re::Request::ListSchedules)
.await;
let body = match resp { let body = match resp {
Ok(SocketReply::Schedules(schedules)) => serde_json::to_string(&schedules) Ok(SocketReply::Schedules(schedules)) => serde_json::to_string(&schedules)
.unwrap_or_else(|e| format!("list_schedules: serialise: {e:#}")), .unwrap_or_else(|e| format!("list_schedules: serialise: {e:#}")),