feat(#1997): add prettier markdown formatter to treefmt

- .prettierrc: proseWrap=preserve (no prose reflow)
- .prettierignore: exclude hivectl-cli.md (auto-generated) + 11 docs
  with multi-line list-item continuations prettier would strip to col 0
  (CommonMark limitation in prettier's list handling)
- format 16 markdown files: cosmetic only (*→_, table alignment,
  heading normalisation) — verified no broken continuations, idempotent
This commit is contained in:
atlas 2026-07-02 19:55:26 +02:00 committed by mara
commit 8406a45275
17 changed files with 254 additions and 230 deletions

View file

@ -2,3 +2,19 @@
# The hivectl-docs CI check diffs this against fresh binary output, # The hivectl-docs CI check diffs this against fresh binary output,
# so reformatting it would break that check. # so reformatting it would break that check.
docs/tools/hivectl-cli.md docs/tools/hivectl-cli.md
# Files with multi-line list-item continuations that prettier strips to col 0.
# prettier's `proseWrap: "preserve"` prevents prose reflow but not list-item
# indentation normalisation, so these files need manual re-wrap before
# adding them back to the formatter scope.
CLAUDE.md
docs/approvals.md
docs/conventions.md
docs/gateway.md
docs/matrix.md
docs/persistence.md
docs/terminal-rendering.md
docs/tools/matrix.md
docs/turn-loop.md
docs/web-ui/agent.md
docs/web-ui/dashboard.md

View file

@ -5,7 +5,7 @@
> approves them in a browser, every deploy is a tag. cyberpunk-themed > approves them in a browser, every deploy is a tag. cyberpunk-themed
> dashboard included. 💜⚡ > dashboard included. 💜⚡
Claude code is great in one window, *exponentielle* across many — but Claude code is great in one window, _exponentielle_ across many — but
only if you can keep the agents from stepping on each other, give them only if you can keep the agents from stepping on each other, give them
durable identity, and stop them from eating production. hyperhive is durable identity, and stop them from eating production. hyperhive is
the substrate. the substrate.
@ -43,15 +43,15 @@ host (NixOS, runs hive-c0re.service)
Depth lives in [`docs/`](docs/) — pick the one matching your task: Depth lives in [`docs/`](docs/) — pick the one matching your task:
| reading path | doc | | reading path | doc |
| --- | --- | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| dashboard layout + endpoints | [`docs/web-ui.md`](docs/web-ui.md) ([shape](docs/web-ui/shape.md) · [dashboard](docs/web-ui/dashboard.md) · [agent](docs/web-ui/agent.md)) | | dashboard layout + endpoints | [`docs/web-ui.md`](docs/web-ui.md) ([shape](docs/web-ui/shape.md) · [dashboard](docs/web-ui/dashboard.md) · [agent](docs/web-ui/agent.md)) |
| claude turn loop + MCP tools | [`docs/turn-loop.md`](docs/turn-loop.md) | | claude turn loop + MCP tools | [`docs/turn-loop.md`](docs/turn-loop.md) |
| config-edit + approval state machine | [`docs/approvals.md`](docs/approvals.md) | | config-edit + approval state machine | [`docs/approvals.md`](docs/approvals.md) |
| what survives destroy / purge / restart | [`docs/persistence.md`](docs/persistence.md) | | what survives destroy / purge / restart | [`docs/persistence.md`](docs/persistence.md) |
| naming, wire protocol, commit style | [`docs/conventions.md`](docs/conventions.md) | | naming, wire protocol, commit style | [`docs/conventions.md`](docs/conventions.md) |
| nginx vhost map + sub-domain routing | [`docs/gateway.md`](docs/gateway.md) | | nginx vhost map + sub-domain routing | [`docs/gateway.md`](docs/gateway.md) |
| NixOS / nspawn gotchas | [`docs/gotchas.md`](docs/gotchas.md) | | NixOS / nspawn gotchas | [`docs/gotchas.md`](docs/gotchas.md) |
## Quick start ## Quick start
@ -96,7 +96,7 @@ Per-agent config lives in each agent's `agent.nix` (proposed, operator-approved,
### Multi-account Matrix support ### Multi-account Matrix support
`hyperhive.matrixAccounts` declares *additional* matrix accounts for an agent, beyond the hive-internal one. Each entry is keyed by account name and specifies: `hyperhive.matrixAccounts` declares _additional_ matrix accounts for an agent, beyond the hive-internal one. Each entry is keyed by account name and specifies:
- `tokenFile` — path to the matrix bearer token (provisioned out-of-band) - `tokenFile` — path to the matrix bearer token (provisioned out-of-band)
- `sessionDir` — path to the per-account matrix-sdk sqlite state (crypto keys + cache) - `sessionDir` — path to the per-account matrix-sdk sqlite state (crypto keys + cache)
@ -114,7 +114,7 @@ hyperhive.matrixAccounts = {
}; };
``` ```
The hive-internal account is always named `main` (synthesized from `hyperhive.matrix.url` + agent state). This option only declares *extras*; the `main` name is reserved and cannot be used here. Requires `hyperhive.matrix.enable = true`. The hive-internal account is always named `main` (synthesized from `hyperhive.matrix.url` + agent state). This option only declares _extras_; the `main` name is reserved and cannot be used here. Requires `hyperhive.matrix.enable = true`.
For more details see [`docs/matrix.md`](docs/matrix.md). For more details see [`docs/matrix.md`](docs/matrix.md).

View file

@ -12,9 +12,9 @@ Topology lives in the hive-c0re-owned **meta repo**, alongside
```json ```json
{ {
"ruth": null, "ruth": null,
"alice": null, "alice": null,
"bob": "alice" "bob": "alice"
} }
``` ```
@ -34,6 +34,7 @@ operator-driven:
All three converge on `topology::set_parent`, which delegates the All three converge on `topology::set_parent`, which delegates the
validation rules to a pure `apply_set_parent` helper. Refuses: validation rules to a pure `apply_set_parent` helper. Refuses:
- unknown `child` / `new_parent` (typo guard), - unknown `child` / `new_parent` (typo guard),
- self-parenting, - self-parenting,
- cycles (32-hop ancestor walk, mirroring `is_descendant_of`). - cycles (32-hop ancestor walk, mirroring `is_descendant_of`).
@ -93,15 +94,15 @@ where system-level facts live.
Once enforcement lands the rules collapse into: Once enforcement lands the rules collapse into:
| operation | who can do it | | operation | who can do it |
|---|---| | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `kill` / `start` / `restart` / `update` (any descendant) | any ancestor | | `kill` / `start` / `restart` / `update` (any descendant) | any ancestor |
| `request_init_config` (spawn a new child) | any agent, child added under self | | `request_init_config` (spawn a new child) | any agent, child added under self |
| `request_apply_commit` (any descendant's config) | any ancestor | | `request_apply_commit` (any descendant's config) | any ancestor |
| `get_logs` (any descendant) | any ancestor | | `get_logs` (any descendant) | any ancestor |
| moderate questions / reminders (cancel any open thread of a descendant) | any ancestor | | moderate questions / reminders (cancel any open thread of a descendant) | any ancestor |
| `send` / `recv` routing | parent ↔ same-parent siblings ↔ self ↔ descendants; explicit allow-list for anyone else | | `send` / `recv` routing | parent ↔ same-parent siblings ↔ self ↔ descendants; explicit allow-list for anyone else |
| `request_update_meta_inputs` (bump meta lock) | root agents only (today: just `manager`) | | `request_update_meta_inputs` (bump meta lock) | root agents only (today: just `manager`) |
"Ancestor" walks `ContainerView.parent` chains; cycles are guarded by a "Ancestor" walks `ContainerView.parent` chains; cycles are guarded by a
visited-set at dispatch time (a malformed topology.json can't lock the visited-set at dispatch time (a malformed topology.json can't lock the
@ -129,14 +130,14 @@ The `ManagerRequest::*` variants in `hive-sh4re/src/lib.rs` are
operations the manager flavour socket can make that sub-agent sockets operations the manager flavour socket can make that sub-agent sockets
can't: can't:
| variant | semantic | post-milestone | | variant | semantic | post-milestone |
|---|---|---| | --------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RequestInitConfig` | seed an agent's proposed config repo | **topology** — existing direct child (re-init) or a brand-new name (child added under self on approval); a name owned by a different parent is refused | | `RequestInitConfig` | seed an agent's proposed config repo | **topology** — existing direct child (re-init) or a brand-new name (child added under self on approval); a name owned by a different parent is refused |
| `RequestApplyCommit` | submit a commit sha for operator approval | **topology** — descendants only | | `RequestApplyCommit` | submit a commit sha for operator approval | **topology** — descendants only |
| `Kill` / `Start` / `Restart` / `Update` | container lifecycle on an existing agent | **topology** — descendants only | | `Kill` / `Start` / `Restart` / `Update` | container lifecycle on an existing agent | **topology** — descendants only |
| `RequestUpdateMetaInputs` | bump meta `flake.lock` | **per-agent cap** (root-only today; a future "let coder bump its own input" might grant it) | | `RequestUpdateMetaInputs` | bump meta `flake.lock` | **per-agent cap** (root-only today; a future "let coder bump its own input" might grant it) |
| `GetLogs` | journalctl scrape of a sub-agent | **topology** — descendants only | | `GetLogs` | journalctl scrape of a sub-agent | **topology** — descendants only |
| `Wake` | inject a `from: <X>` message into self's inbox | **not really privileged** — the wire surface exists because daemon co-processes (e.g. `forge_notify`) need it. Sub-agents have the same via their own socket. | | `Wake` | inject a `from: <X>` message into self's inbox | **not really privileged** — the wire surface exists because daemon co-processes (e.g. `forge_notify`) need it. Sub-agents have the same via their own socket. |
### C — storage / mounts (`hive-c0re::lifecycle`) ### C — storage / mounts (`hive-c0re::lifecycle`)
@ -150,6 +151,7 @@ The manager container's nspawn bind set:
manager can read the system-wide deploy log manager can read the system-wide deploy log
Tree-shape version: Tree-shape version:
- Each agent gets RW to `/agents/<descendant>/` for every descendant in - Each agent gets RW to `/agents/<descendant>/` for every descendant in
its subtree. The root agent (today: manager) gets RW to the full its subtree. The root agent (today: manager) gets RW to the full
forest as a special case of "the root has every other agent as a forest as a special case of "the root has every other agent as a
@ -182,7 +184,6 @@ direct `$HYPERHIVE_STATE_DIR/<token>` read.
### F — drive-by checks across c0re ### F — drive-by checks across c0re
- `loose_ends.rs`: manager sees hive-wide loose-ends, sub-agents only - `loose_ends.rs`: manager sees hive-wide loose-ends, sub-agents only
their own. **Topology** — every agent sees its own + its their own. **Topology** — every agent sees its own + its
descendants'. descendants'.

View file

@ -1,12 +1,12 @@
# The operator/agent boundary # The operator/agent boundary
Design rationale for hyperhive's two-principal trust model. The Design rationale for hyperhive's two-principal trust model. The
*implementation* work — container network isolation, the unifying _implementation_ work — container network isolation, the unifying
gateway, core-daemon privsep — is tracked as `area:ops` issues on gateway, core-daemon privsep — is tracked as `area:ops` issues on
the forge. the forge.
Today "the operator surface" and "the agent surface" are a Today "the operator surface" and "the agent surface" are a
*convention*, not a boundary — nothing stops a container from _convention_, not a boundary — nothing stops a container from
curling the core daemon on `localhost:<port>`, or another agent's curling the core daemon on `localhost:<port>`, or another agent's
web UI. Network isolation, the gateway, and privsep together turn web UI. Network isolation, the gateway, and privsep together turn
that convention into an enforced boundary. that convention into an enforced boundary.
@ -19,7 +19,7 @@ that convention into an enforced boundary.
lifecycle POSTs) are served by the core daemon and only lifecycle POSTs) are served by the core daemon and only
reachable via the gateway. reachable via the gateway.
- **Agent** — speaks only for itself, only over its per-agent - **Agent** — speaks only for itself, only over its per-agent
unix socket. The socket's identity *is* the agent (see unix socket. The socket's identity _is_ the agent (see
`docs/conventions.md`, "identity = socket"). An agent must not `docs/conventions.md`, "identity = socket"). An agent must not
be able to reach the core daemon's HTTP surface, another be able to reach the core daemon's HTTP surface, another
agent's socket, or another agent's web UI. agent's socket, or another agent's web UI.
@ -30,7 +30,7 @@ that convention into an enforced boundary.
point.** They live on the core backend. point.** They live on the core backend.
Worked example — answering an operator-targeted question is a Worked example — answering an operator-targeted question is a
`POST /answer-question/{id}` on the core dashboard, *never* an `POST /answer-question/{id}` on the core dashboard, _never_ an
`AgentRequest` variant. If it were a per-agent-socket request, an `AgentRequest` variant. If it were a per-agent-socket request, an
agent could `curl` its own socket and spoof an operator answer. agent could `curl` its own socket and spoof an operator answer.
The per-agent web UI POSTs cross-origin to the core for these The per-agent web UI POSTs cross-origin to the core for these
@ -44,7 +44,7 @@ container can reach `localhost:<core-port>`, the dashboard, and
every other agent's web port. Until that changes, the every other agent's web port. Until that changes, the
operator/agent split is on the honour system — every boundary operator/agent split is on the honour system — every boundary
claim above is aspirational. Network isolation is what makes the claim above is aspirational. Network isolation is what makes the
boundary *real*; the gateway and privsep are ergonomics and boundary _real_; the gateway and privsep are ergonomics and
defence-in-depth layered on top. defence-in-depth layered on top.
The `area:ops` issues followed this sequencing: The `area:ops` issues followed this sequencing:

View file

@ -7,6 +7,7 @@ The `hive-ci` module runs a Forgejo Actions runner in a `hive-ci` nixos-containe
Set `services.hyperhive.forge.ci.enable = true` in the host NixOS config. That's it — no manual token provisioning. Set `services.hyperhive.forge.ci.enable = true` in the host NixOS config. That's it — no manual token provisioning.
**Requirements:** **Requirements:**
- The internal forge is always present (mandatory), so the runner always has a hive-forge instance to register against — nothing extra to enable. - The internal forge is always present (mandatory), so the runner always has a hive-forge instance to register against — nothing extra to enable.
- Optional: tune `services.hyperhive.forge.ci.name` (runner name in forge admin panel), `concurrency` (parallel job capacity), `labels` (workflow targeting), `jobTimeout` (per-job wall-clock cap, default `"1h"`, Go duration string e.g. `"3h"` — a job that exceeds it is killed so a hung or runaway build can't hold the runner's single slot indefinitely). - Optional: tune `services.hyperhive.forge.ci.name` (runner name in forge admin panel), `concurrency` (parallel job capacity), `labels` (workflow targeting), `jobTimeout` (per-job wall-clock cap, default `"1h"`, Go duration string e.g. `"3h"` — a job that exceeds it is killed so a hung or runaway build can't hold the runner's single slot indefinitely).
@ -148,7 +149,7 @@ following to your host config:
``` ```
**Remote builders:** if CI dispatches builds to a remote builder (e.g. via **Remote builders:** if CI dispatches builds to a remote builder (e.g. via
`nix.buildMachines` / `ssh-ng://`), the build outputs land in *that host's* `nix.buildMachines` / `ssh-ng://`), the build outputs land in _that host's_
store, so the same GC config should be applied wherever the builder runs — store, so the same GC config should be applied wherever the builder runs —
GC on the coordinator host won't reclaim space on the builder. GC on the coordinator host won't reclaim space on the builder.

View file

@ -32,17 +32,17 @@ somewhere."
### Queue kinds ### Queue kinds
| Kind | Description | | Kind | Description |
|------|-------------| | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Rebuild` | Single-agent rebuild. Covers manual, approval-driven, auto-update, and meta-update cascade variants — all funnel through the same path. The start-after-rebuild is **deferred to a fast-lane `Start` follow-up** (`parent_id` = this entry) so the build lane is freed as soon as the profile-swap finishes instead of waiting out the container boot — see *Deferred start* under the rebuild path below. | | `Rebuild` | Single-agent rebuild. Covers manual, approval-driven, auto-update, and meta-update cascade variants — all funnel through the same path. The start-after-rebuild is **deferred to a fast-lane `Start` follow-up** (`parent_id` = this entry) so the build lane is freed as soon as the profile-swap finishes instead of waiting out the container boot — see _Deferred start_ under the rebuild path below. |
| `MetaUpdate` | `nix flake update` on the meta flake. The worker runs the lock bump itself, then enqueues a cascade of `Rebuild` entries with `parent_id` set to the meta-update's id. | | `MetaUpdate` | `nix flake update` on the meta flake. The worker runs the lock bump itself, then enqueues a cascade of `Rebuild` entries with `parent_id` set to the meta-update's id. |
| `Spawn` | First-deploy of a new agent (approval-driven). Same serialisation as `Rebuild` from the operator's POV. | | `Spawn` | First-deploy of a new agent (approval-driven). Same serialisation as `Rebuild` from the operator's POV. |
| `Destroy` | For future use (`destroy --purge` does real I/O). Variant exists so the wire shape doesn't change later; not currently routed through the queue. | | `Destroy` | For future use (`destroy --purge` does real I/O). Variant exists so the wire shape doesn't change later; not currently routed through the queue. |
| `Restart` | Stop + start a container without touching config (~5-10s). Routed through the queue so it serialises against in-flight rebuilds for the same agent — prevents a restart racing a rebuild mid-flight. Sources: dashboard ↺ button, the `restart` MCP tool. | | `Restart` | Stop + start a container without touching config (~5-10s). Routed through the queue so it serialises against in-flight rebuilds for the same agent — prevents a restart racing a rebuild mid-flight. Sources: dashboard ↺ button, the `restart` MCP tool. |
| `PermChange` | Write a tool-group or capability change to the shared JSON file (`tool-groups.json` / `capabilities.json`), then rebuild the agent so the updated `HIVE_TOOL_GROUPS` / `HIVE_CAPABILITIES` env var takes effect. Serialising the file write through the queue prevents concurrent dashboard batch-apply actions from racing on the shared file. After a successful file write, emits `CapabilitiesChanged` or `ToolGroupsChanged` SSE snapshot so the P3RM1SS10NS tab updates live. | | `PermChange` | Write a tool-group or capability change to the shared JSON file (`tool-groups.json` / `capabilities.json`), then rebuild the agent so the updated `HIVE_TOOL_GROUPS` / `HIVE_CAPABILITIES` env var takes effect. Serialising the file write through the queue prevents concurrent dashboard batch-apply actions from racing on the shared file. After a successful file write, emits `CapabilitiesChanged` or `ToolGroupsChanged` SSE snapshot so the P3RM1SS10NS tab updates live. |
| `GracefulStop` | Quiesce then stop a container (the `?graceful=true` path on `/api/kill/<agent>`). Signals the harness (its next `Recv` returns `GracefulStop` — the inbound fence — so it runs a stop-checkpoint turn that flushes durable `/state`, then takes the normal post-turn compaction path if it crossed the watermark, then exits) and **immediately releases the build lane**, spawning a detached watcher that holds the `Stopping` transient across the drain (bounded by a 3-min timeout → hard-stop fallback) and then enqueues a fast-lane `Stop` (`parent_id` = this entry) for the actual `nixos-container stop`. Net: a whole-hive graceful stop signals every agent up front, drains overlap, and only the container teardowns serialise (on the fast lane). Queued so the signal can't race an in-flight rebuild for the same agent. | | `GracefulStop` | Quiesce then stop a container (the `?graceful=true` path on `/api/kill/<agent>`). Signals the harness (its next `Recv` returns `GracefulStop` — the inbound fence — so it runs a stop-checkpoint turn that flushes durable `/state`, then takes the normal post-turn compaction path if it crossed the watermark, then exits) and **immediately releases the build lane**, spawning a detached watcher that holds the `Stopping` transient across the drain (bounded by a 3-min timeout → hard-stop fallback) and then enqueues a fast-lane `Stop` (`parent_id` = this entry) for the actual `nixos-container stop`. Net: a whole-hive graceful stop signals every agent up front, drains overlap, and only the container teardowns serialise (on the fast lane). Queued so the signal can't race an in-flight rebuild for the same agent. |
**Intentionally not queued** (sub-second ops): the *hard* `start`, `stop`, `kill` via the direct API paths. (A *graceful* stop is the `GracefulStop` kind above — it takes a checkpoint turn, so it rides the queue.) The queue's fast lane does carry `Start` / `Stop` kinds, but only as **follow-ups** other entries enqueue for themselves — the graceful-stop teardown and the deferred start-after-rebuild — so the container op groups under its parent entry on the dashboard. **Intentionally not queued** (sub-second ops): the _hard_ `start`, `stop`, `kill` via the direct API paths. (A _graceful_ stop is the `GracefulStop` kind above — it takes a checkpoint turn, so it rides the queue.) The queue's fast lane does carry `Start` / `Stop` kinds, but only as **follow-ups** other entries enqueue for themselves — the graceful-stop teardown and the deferred start-after-rebuild — so the container op groups under its parent entry on the dashboard.
### Dedup ### Dedup
@ -53,12 +53,12 @@ since the current run started).
### Sources ### Sources
| Source | Meaning | | Source | Meaning |
|--------|---------| | -------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `Manual` | Operator clicked rebuild / update-all / meta-update on the dashboard, or any other direct human action (CLI, an agent MCP tool). | | `Manual` | Operator clicked rebuild / update-all / meta-update on the dashboard, or any other direct human action (CLI, an agent MCP tool). |
| `AutoUpdate` | Legacy startup-sweep source (flat, no parent). Replaced by `StartupSweep` for new boots. | | `AutoUpdate` | Legacy startup-sweep source (flat, no parent). Replaced by `StartupSweep` for new boots. |
| `StartupSweep` | Child of a `StartupSweep` parent entry; boot-time per-agent rebuild with the sweep as the visual group header. | | `StartupSweep` | Child of a `StartupSweep` parent entry; boot-time per-agent rebuild with the sweep as the visual group header. |
| `Approval` | Triggered by an operator-approved `ApprovalKind::{Spawn, ApplyCommit}`. | | `Approval` | Triggered by an operator-approved `ApprovalKind::{Spawn, ApplyCommit}`. |
### Cascade parent tracking ### Cascade parent tracking
@ -83,6 +83,7 @@ in the queue as a terminal entry (`Done` / `Failed` / `Cancelled`) or no longer
the queue at all (evicted by `trim_history`, which only evicts terminals). the queue at all (evicted by `trim_history`, which only evicts terminals).
Use cases: Use cases:
- Chain a `Rebuild` after an explicit prerequisite step without coupling them through - Chain a `Rebuild` after an explicit prerequisite step without coupling them through
the `parent_id` cascade mechanism. the `parent_id` cascade mechanism.
- Sequence a `PermChange` + `Rebuild` pair where the rebuild must not start until the - Sequence a `PermChange` + `Rebuild` pair where the rebuild must not start until the
@ -243,10 +244,10 @@ agent container.
rebuild, so changes take effect on the next lifecycle op without rebuild, so changes take effect on the next lifecycle op without
requiring a host rebuild. requiring a host rebuild.
| Option | Default | Description | | Option | Default | Description |
|--------|---------|-------------| | ---------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `services.hyperhive.c0re.agentCpuQuota` | `"200%"` | CPU cap per agent, as a percentage of one core (`"200%"` = 2 cores). Raise if agents hit CPU limits during builds or heavy tool use. | | `services.hyperhive.c0re.agentCpuQuota` | `"200%"` | CPU cap per agent, as a percentage of one core (`"200%"` = 2 cores). Raise if agents hit CPU limits during builds or heavy tool use. |
| `services.hyperhive.c0re.agentMemoryMax` | `"4G"` | Memory cap per agent. Raise for agents that run large nix builds or hold big in-memory data. | | `services.hyperhive.c0re.agentMemoryMax` | `"4G"` | Memory cap per agent. Raise for agents that run large nix builds or hold big in-memory data. |
For a hive-wide cap across all containers together, set For a hive-wide cap across all containers together, set
`systemd.slices.machine.serviceConfig.CPUQuota` in your NixOS `systemd.slices.machine.serviceConfig.CPUQuota` in your NixOS

View file

@ -14,16 +14,16 @@ Two scope sets live in `hive-c0re::forge`:
**`TOKEN_SCOPES`** (per-agent tokens): **`TOKEN_SCOPES`** (per-agent tokens):
| Scope | Why | | Scope | Why |
|-------|-----| | -------------------- | ------------------------------------------------------------------------------------------------------- |
| `write:repository` | Create, clone, push, delete repos; merge PRs. | | `write:repository` | Create, clone, push, delete repos; merge PRs. |
| `write:issue` | Open / comment / review issues **and** pull requests (Forgejo namespaces PR conversation under issues). | | `write:issue` | Open / comment / review issues **and** pull requests (Forgejo namespaces PR conversation under issues). |
| `write:user` | Edit own profile, create repos under own user. | | `write:user` | Edit own profile, create repos under own user. |
| `write:organization` | Create + manage orgs (lets agents share a forge namespace). | | `write:organization` | Create + manage orgs (lets agents share a forge namespace). |
| `read:user` | Token-owner endpoint used for self-identification at harness startup. | | `read:user` | Token-owner endpoint used for self-identification at harness startup. |
| `write:misc` | Hooks, attachments, the rest of the long tail. | | `write:misc` | Hooks, attachments, the rest of the long tail. |
| `read:notification` | Poll `GET /notifications` for unread events. | | `read:notification` | Poll `GET /notifications` for unread events. |
| `write:notification` | Mark notifications read via `PATCH /notifications/threads/{id}`. | | `write:notification` | Mark notifications read via `PATCH /notifications/threads/{id}`. |
**`CORE_TOKEN_SCOPES`** (hive-c0re's own `core` user): everything in **`CORE_TOKEN_SCOPES`** (hive-c0re's own `core` user): everything in
`TOKEN_SCOPES` plus `read:admin` and `write:admin`. Site-admin `TOKEN_SCOPES` plus `read:admin` and `write:admin`. Site-admin
@ -102,7 +102,7 @@ resurfaces next tick.
**Size bound:** the per-poll prune retains only ids present in the **Size bound:** the per-poll prune retains only ids present in the
single `limit=UNREAD_FETCH_LIMIT` (50) fetch page, so the cursor never single `limit=UNREAD_FETCH_LIMIT` (50) fetch page, so the cursor never
exceeds that many entries — it tracks the unread *window*, not the exceeds that many entries — it tracks the unread _window_, not the
all-time notification count. The fetch limit and the bound are the all-time notification count. The fetch limit and the bound are the
same constant in `forge_notify.rs` (with a debug assertion), so a same constant in `forge_notify.rs` (with a debug assertion), so a
future pagination change grows the ceiling visibly rather than future pagination change grows the ceiling visibly rather than
@ -165,7 +165,7 @@ silently (mark-read without delivery):
`user.login` matches `own_login`. `user.login` matches `own_login`.
- **Self-authored creations** (an agent opening its own PR/issue) — the - **Self-authored creations** (an agent opening its own PR/issue) — the
already-fetched subject payload's poster `user.login` matches already-fetched subject payload's poster `user.login` matches
`own_login`. Only *creations* are dropped; a later state change on the `own_login`. Only _creations_ are dropped; a later state change on the
agent's own subject is driven by someone else and still surfaces. agent's own subject is driven by someone else and still surfaces.
`own_login` is fetched once at startup via `GET /api/v1/user`. On `own_login` is fetched once at startup via `GET /api/v1/user`. On
@ -208,13 +208,13 @@ lookahead.
Five shapes, distinguished by the notification's classification: Five shapes, distinguished by the notification's classification:
| Trigger | Wrapper | | Trigger | Wrapper |
| --- | --- | | ----------------------------------- | --------------------------------------------------------------------------------- |
| Comment on issue / PR | `[comment on PR #N owner/repo] title\nurl: ...\n\nauthor: body\nassignee: ...` | | Comment on issue / PR | `[comment on PR #N owner/repo] title\nurl: ...\n\nauthor: body\nassignee: ...` |
| Review submission | `[PR approved #N owner/repo] title\nurl: ...\n\nreviewer: body\nassignee: ...` | | Review submission | `[PR approved #N owner/repo] title\nurl: ...\n\nreviewer: body\nassignee: ...` |
| New issue / PR | `[new PR #N owner/repo] title\nurl: ...\n\n<body excerpt>\nassignee: ...` | | New issue / PR | `[new PR #N owner/repo] title\nurl: ...\n\n<body excerpt>\nassignee: ...` |
| Later activity (open, not creation) | `[activity on PR #N owner/repo] title\nurl: ...\n\n<body excerpt>\nassignee: ...` | | Later activity (open, not creation) | `[activity on PR #N owner/repo] title\nurl: ...\n\n<body excerpt>\nassignee: ...` |
| State change | `[PR merged #N owner/repo] title\nurl: ...\nassignee: ...` | | State change | `[PR merged #N owner/repo] title\nurl: ...\nassignee: ...` |
Review labels come from the Forgejo `state` field: `APPROVED` Review labels come from the Forgejo `state` field: `APPROVED`
`approved`, `REQUEST_CHANGES``changes requested`, `COMMENT` `approved`, `REQUEST_CHANGES``changes requested`, `COMMENT`
@ -232,7 +232,7 @@ original open notification and miss the review (#1637). So the `open`
state only earns the `new <kind>` label when the notification's event state only earns the `new <kind>` label when the notification's event
time (`updated_at`) is within `NEW_ITEM_TOLERANCE_SECS` (120s) of the time (`updated_at`) is within `NEW_ITEM_TOLERANCE_SECS` (120s) of the
subject's `created_at`. Anything later is labeled `activity on <kind>` subject's `created_at`. Anything later is labeled `activity on <kind>`
— neutral and non-misleading, since we can't cheaply say *what* the — neutral and non-misleading, since we can't cheaply say _what_ the
activity was without an extra reviews fetch. Missing/unparseable activity was without an extra reviews fetch. Missing/unparseable
timestamps default to `new` (preserve prior behavior rather than mask a timestamps default to `new` (preserve prior behavior rather than mask a
genuine new item). Timestamps are parsed by a small dependency-free genuine new item). Timestamps are parsed by a small dependency-free

View file

@ -201,7 +201,7 @@ Never use raw `curl` for forge access.
Agent containers bind-mount the host's nix-daemon socket. nspawn Agent containers bind-mount the host's nix-daemon socket. nspawn
containers don't get user-namespaces by default, so `nix build` containers don't get user-namespaces by default, so `nix build`
invocations *inside* the container can't set up the build sandbox invocations _inside_ the container can't set up the build sandbox
and fail outright if the host daemon's and fail outright if the host daemon's
`nix.settings.sandbox-fallback` is `false` (nixpkgs default). `nix.settings.sandbox-fallback` is `false` (nixpkgs default).
`nix/templates/harness-base.nix` does `lib.mkForce true` so builds `nix/templates/harness-base.nix` does `lib.mkForce true` so builds
@ -277,7 +277,7 @@ connects to the compositor at `127.0.0.1:<vnc_port>`.
("Failed to connect to the bus" → binds `xdg_wm_base` then destroys it ("Failed to connect to the bus" → binds `xdg_wm_base` then destroys it
= invisible window even though CDP works). The fix is **not** to wrap = invisible window even though CDP works). The fix is **not** to wrap
each client in its own `dbus-run-session` (a private throwaway bus per each client in its own `dbus-run-session` (a private throwaway bus per
process — that's a *separate* session, defeating the one-session process — that's a _separate_ session, defeating the one-session
model); it's this one shared bus, whose address is exported as model); it's this one shared bus, whose address is exported as
`DBUS_SESSION_BUS_ADDRESS=unix:path=/run/gui/bus` via `DBUS_SESSION_BUS_ADDRESS=unix:path=/run/gui/bus` via
`systemd.globalEnvironment` so weston, the harness and every GUI client `systemd.globalEnvironment` so weston, the harness and every GUI client

View file

@ -15,13 +15,13 @@ of shared netns.
## v1 vs v2 ## v1 vs v2
| feature | v1 (this PR) | v2 (after netns isolation) | | feature | v1 (this PR) | v2 (after netns isolation) |
|---|---|---| | ------------------------ | ------------------------------------------------------- | ----------------------------------------------- |
| bridge interface | created on host, no slave NICs | per-agent veth pairs attach | | bridge interface | created on host, no slave NICs | per-agent veth pairs attach |
| dnsmasq binding | bridge IP (reachable via host loopback in shared netns) | bridge IP (reachable via veth in private netns) | | dnsmasq binding | bridge IP (reachable via host loopback in shared netns) | bridge IP (reachable via veth in private netns) |
| agent container netns | shared host | private | | agent container netns | shared host | private |
| agent `/etc/resolv.conf` | unchanged (host DNS) | `nameserver <bridge-ip>` | | agent `/etc/resolv.conf` | unchanged (host DNS) | `nameserver <bridge-ip>` |
| `address` rules target | `<bridge-ip>` (works in both modes) | unchanged from v1 | | `address` rules target | `<bridge-ip>` (works in both modes) | unchanged from v1 |
The `address` rules ship pointing at the bridge IP from v1 so the The `address` rules ship pointing at the bridge IP from v1 so the
DNS contract is fixed before any container actually depends on it DNS contract is fixed before any container actually depends on it
@ -91,9 +91,10 @@ agent containers.
## Firewall posture ## Firewall posture
`networking.firewall.interfaces.<bridge>.allowedUDPPorts = [ 53 ]` `networking.firewall.interfaces.<bridge>.allowedUDPPorts = [ 53 ]`
+ `allowedTCPPorts = [ 53 ]` opens the resolver on the bridge
interface only. Other interfaces stay closed. The hive resolver - `allowedTCPPorts = [ 53 ]` opens the resolver on the bridge
isn't an external-facing service. interface only. Other interfaces stay closed. The hive resolver
isn't an external-facing service.
When `isolateContainers = true`, `allowedTCPPorts` is extended with When `isolateContainers = true`, `allowedTCPPorts` is extended with
`[ 80 443 ]` so isolated agents can reach nginx (gateway container, `[ 80 443 ]` so isolated agents can reach nginx (gateway container,
@ -131,14 +132,14 @@ agent containers from shared host netns to private netns. Set only after
### What the nix side does when `isolateContainers = true` ### What the nix side does when `isolateContainers = true`
| effect | mechanism | | effect | mechanism |
|---|---| | -------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| IP forwarding | `boot.kernel.sysctl."net.ipv4.ip_forward" = 1` | | IP forwarding | `boot.kernel.sysctl."net.ipv4.ip_forward" = 1` |
| Internet NAT | `networking.nat { enable = true; internalInterfaces = [ bridgeName ]; }` — MASQUERADE on packets leaving via any external NIC | | Internet NAT | `networking.nat { enable = true; internalInterfaces = [ bridgeName ]; }` — MASQUERADE on packets leaving via any external NIC |
| Loopback DROP | `networking.firewall.extraInputRules` — drops bridge-subnet → `127.0.0.0/8` traffic; defence-in-depth against routing table leaks | | Loopback DROP | `networking.firewall.extraInputRules` — drops bridge-subnet → `127.0.0.0/8` traffic; defence-in-depth against routing table leaks |
| Gateway access | `networking.firewall.interfaces.<bridge>.allowedTCPPorts = [ 80 443 ]` — lets isolated agents reach nginx on the host (shared netns) | | Gateway access | `networking.firewall.interfaces.<bridge>.allowedTCPPorts = [ 80 443 ]` — lets isolated agents reach nginx on the host (shared netns) |
| Forge URL | `HIVE_FORGE_URL` flips from `http://127.0.0.1:3000` to `http://forge.<domain>` — agents resolve via dnsmasq, nginx proxies to forgejo | | Forge URL | `HIVE_FORGE_URL` flips from `http://127.0.0.1:3000` to `http://forge.<domain>` — agents resolve via dnsmasq, nginx proxies to forgejo |
| c0re signal | `HIVE_NETWORK_ISOLATION=1`, `HIVE_NETWORK_BRIDGE`, `HIVE_NETWORK_SUBNET` in `systemd.services.hive-c0re.environment` | | c0re signal | `HIVE_NETWORK_ISOLATION=1`, `HIVE_NETWORK_BRIDGE`, `HIVE_NETWORK_SUBNET` in `systemd.services.hive-c0re.environment` |
`HIVE_NETWORK_SUBNET` is the host-side bridge IP + prefix (e.g. `HIVE_NETWORK_SUBNET` is the host-side bridge IP + prefix (e.g.
`10.42.0.1/24`), **not** the canonical network address. The Rust side `10.42.0.1/24`), **not** the canonical network address. The Rust side

View file

@ -15,10 +15,10 @@ An agent is **trusted code running inside its own nspawn container**. The
boundary that matters is the container: a sub-agent cannot see the host boundary that matters is the container: a sub-agent cannot see the host
netns, another agent's container, or another agent's state dir. Within its netns, another agent's container, or another agent's state dir. Within its
own container the agent is privileged — it has **passwordless `sudo` by own container the agent is privileged — it has **passwordless `sudo` by
default**. Isolating credentials *from the agent itself* is therefore **not a default**. Isolating credentials _from the agent itself_ is therefore **not a
goal**: an agent can read its own tokens, its own `/home/<name>/.claude`, and goal**: an agent can read its own tokens, its own `/home/<name>/.claude`, and
run arbitrary commands as root inside its container. (The narrow exception is run arbitrary commands as root inside its container. (The narrow exception is
*cross-tenant* leakage — e.g. the unsandboxed-nix-build `0600` token policy _cross-tenant_ leakage — e.g. the unsandboxed-nix-build `0600` token policy
below stops a build's nixbld user reading the agent's own forge token, and the below stops a build's nixbld user reading the agent's own forge token, and the
state-file endpoint stops one agent proxying another's files. Those harden the state-file endpoint stops one agent proxying another's files. Those harden the
boundary; they do not sandbox the agent from itself.) boundary; they do not sandbox the agent from itself.)
@ -36,7 +36,7 @@ Each agent gets its own scoped credentials, never shared:
- **matrix token** → that agent's matrix account only. - **matrix token** → that agent's matrix account only.
So a compromised/confused agent's reach on the forge or matrix is bounded by So a compromised/confused agent's reach on the forge or matrix is bounded by
*its own* account's scope, not the swarm's. This is the main thing standing _its own_ account's scope, not the swarm's. This is the main thing standing
between "one agent does something dumb" and "the whole hive is affected". between "one agent does something dumb" and "the whole hive is affected".
### Threat model: prompt injection → confused deputy ### Threat model: prompt injection → confused deputy
@ -44,7 +44,7 @@ between "one agent does something dumb" and "the whole hive is affected".
The realistic adversary **never needs to breach the container**. They supply The realistic adversary **never needs to breach the container**. They supply
**untrusted input the agent reads and acts on**: a poisoned issue or PR **untrusted input the agent reads and acts on**: a poisoned issue or PR
comment, a cloned repo's README/CI, a scraped webpage, a crafted matrix comment, a cloned repo's README/CI, a scraped webpage, a crafted matrix
message. The agent is the trusted, capable party; the *input* is the message. The agent is the trusted, capable party; the _input_ is the
untrusted part. A successful injection turns the agent into a **confused untrusted part. A successful injection turns the agent into a **confused
deputy** — it uses its legitimate capabilities (push, comment, deploy, run deputy** — it uses its legitimate capabilities (push, comment, deploy, run
shell) on the attacker's behalf. shell) on the attacker's behalf.
@ -52,7 +52,7 @@ shell) on the attacker's behalf.
Mitigations are therefore about **bounding capability and inserting human Mitigations are therefore about **bounding capability and inserting human
checkpoints**, not about sandboxing the agent from its own tools: checkpoints**, not about sandboxing the agent from its own tools:
- **Operator merges, not the agent** — an agent may *push* branches, but a - **Operator merges, not the agent** — an agent may _push_ branches, but a
**human (the operator) merges the PR**, keeping a person in the loop on the **human (the operator) merges the PR**, keeping a person in the loop on the
highest-value action. On the **internal forge this is technically enforced, highest-value action. On the **internal forge this is technically enforced,
not just convention**: agents can't create repos (`max_repo_creation = 0`), not just convention**: agents can't create repos (`max_repo_creation = 0`),
@ -76,7 +76,7 @@ accepting**. The rule of thumb:
If an agent can deploy to prod, you are accepting the risk of a dropped If an agent can deploy to prod, you are accepting the risk of a dropped
production database (via injection or plain error). If that's unacceptable, production database (via injection or plain error). If that's unacceptable,
the answer is *don't grant the capability* — not "grant it and hope the the answer is _don't grant the capability_ — not "grant it and hope the
sandbox holds", because there is no sandbox between an agent and the tools you sandbox holds", because there is no sandbox between an agent and the tools you
handed it. handed it.
@ -101,7 +101,7 @@ before touching the filesystem:
(canonical host form or the in-container view `/agents/<n>/state/`) (canonical host form or the in-container view `/agents/<n>/state/`)
- `/var/lib/hyperhive/shared/` — shared docs (`/shared/` in-container) - `/var/lib/hyperhive/shared/` — shared docs (`/shared/` in-container)
`/state/...` without an agent prefix is explicitly *not* accepted — it is `/state/...` without an agent prefix is explicitly _not_ accepted — it is
ambiguous from the host's perspective. ambiguous from the host's perspective.
Defense-in-depth layers (in order): Defense-in-depth layers (in order):
@ -181,22 +181,22 @@ for every operation that genuinely requires root.
**Narrow interface** — `PrivRequest` variants map 1:1 to specific **Narrow interface** — `PrivRequest` variants map 1:1 to specific
known operations; there is no arbitrary command pass-through: known operations; there is no arbitrary command pass-through:
| Operation | What it runs | | Operation | What it runs |
|-----------|-------------| | ---------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `StartContainer` / `StopContainer` / `KillContainer` | `nixos-container start/stop/kill <name>` | | `StartContainer` / `StopContainer` / `KillContainer` | `nixos-container start/stop/kill <name>` |
| `CreateContainer` / `UpdateContainer` | `nixos-container create/update <name> --flake <ref>` | | `CreateContainer` / `UpdateContainer` | `nixos-container create/update <name> --flake <ref>` |
| `DestroyContainer` | `nixos-container destroy <name>` | | `DestroyContainer` | `nixos-container destroy <name>` |
| `ListContainers` | `nixos-container list` | | `ListContainers` | `nixos-container list` |
| `ReadContainerJournal` | `journalctl -M <container> -n <n> [filters...]` | | `ReadContainerJournal` | `journalctl -M <container> -n <n> [filters...]` |
| `ReloadGatewayNginx` | `systemctl -M hive-gateway reload/start/reset-failed nginx` | | `ReloadGatewayNginx` | `systemctl -M hive-gateway reload/start/reset-failed nginx` |
| `WriteNspawnFlags` | write `/etc/nixos-containers/<container>.conf` (bind-mount list + network isolation vars) | | `WriteNspawnFlags` | write `/etc/nixos-containers/<container>.conf` (bind-mount list + network isolation vars) |
| `WriteResourceLimits` | write `CPUQuota=`/`MemoryMax=` systemd drop-in for agent container | | `WriteResourceLimits` | write `CPUQuota=`/`MemoryMax=` systemd drop-in for agent container |
| `RemoveServiceDropin` | remove `container@<name>.service.d/` drop-in on destroy | | `RemoveServiceDropin` | remove `container@<name>.service.d/` drop-in on destroy |
| `DaemonReload` | `systemctl daemon-reload` | | `DaemonReload` | `systemctl daemon-reload` |
| `ChownSocketDir` / `ChmodSocketDir` | chown/chmod `/run/hive-agent/<name>/` socket directory | | `ChownSocketDir` / `ChmodSocketDir` | chown/chmod `/run/hive-agent/<name>/` socket directory |
| `RunForgeAdmin` | `nixos-container run hive-forge -- runuser -u forgejo -- forgejo admin <args>` | | `RunForgeAdmin` | `nixos-container run hive-forge -- runuser -u forgejo -- forgejo admin <args>` |
| `WriteAgentForgeToken` / `WriteAgentMatrixToken` | write `0600` credential file into agent state dir | | `WriteAgentForgeToken` / `WriteAgentMatrixToken` | write `0600` credential file into agent state dir |
| `RestartMatrixDaemon` | `systemctl --machine=h-<name> restart hive-matrix-daemon.service` | | `RestartMatrixDaemon` | `systemctl --machine=h-<name> restart hive-matrix-daemon.service` |
**Container allowlist** — every request is validated against an **Container allowlist** — every request is validated against an
allowlist before any operation: only names matching the agent-name allowlist before any operation: only names matching the agent-name
@ -211,8 +211,8 @@ writes to only the paths `hive-priv` legitimately needs.
### Privilege boundary summary ### Privilege boundary summary
| Component | Runs as | Privilege needed for | | Component | Runs as | Privilege needed for |
|-----------|---------|----------------------| | ---------------------------- | -------------- | ------------------------------------------------------------ |
| `hive-c0re` | `hive-core` | broker, HTTP dashboard, scheduling, approvals | | `hive-c0re` | `hive-core` | broker, HTTP dashboard, scheduling, approvals |
| `hive-priv` | `root` | container lifecycle, journal reads, bind mounts, cred writes | | `hive-priv` | `root` | container lifecycle, journal reads, bind mounts, cred writes |
| `hive-ag3nt` (per-container) | per-agent user | turn execution, MCP serving | | `hive-ag3nt` (per-container) | per-agent user | turn execution, MCP serving |

View file

@ -48,12 +48,12 @@ services.hyperhive.swarm.peers = {
The attrset key is the peer's DNS domain. Two independent, optional The attrset key is the peer's DNS domain. Two independent, optional
trust knobs — pick by what you need to trust: trust knobs — pick by what you need to trust:
- **`certFingerprint`** (`"sha256:…"`) — pin the peer's TLS *leaf* - **`certFingerprint`** (`"sha256:…"`) — pin the peer's TLS _leaf_
fingerprint. Scopes **only** to hive-c0re's own peer HTTPS checks fingerprint. Scopes **only** to hive-c0re's own peer HTTPS checks
(the P33RS dashboard links + agent peer discovery below). It is (the P33RS dashboard links + agent peer discovery below). It is
**not** consulted by matrix federation — tuwunel validates a peer's **not** consulted by matrix federation — tuwunel validates a peer's
federation certificate against the system CA bundle independently federation certificate against the system CA bundle independently
(see *Matrix federation* below), so a fingerprint pin does nothing (see _Matrix federation_ below), so a fingerprint pin does nothing
for a self-signed matrix cert. for a self-signed matrix cert.
- **`caCert`** (path to the peer's root CA PEM) — embeds that CA (at - **`caCert`** (path to the peer's root CA PEM) — embeds that CA (at
build time, into the nix store — no runtime file on the host) and build time, into the nix store — no runtime file on the host) and
@ -61,7 +61,7 @@ trust knobs — pick by what you need to trust:
alongside `hive-ca.pem` in every agent's alongside `hive-ca.pem` in every agent's
`security.pki.certificateFiles` (via the meta-flake renderer) **and** `security.pki.certificateFiles` (via the meta-flake renderer) **and**
in the matrix container's trust bundle, so tuwunel validates the in the matrix container's trust bundle, so tuwunel validates the
peer's *federation* TLS when it chains to that CA. Trust stays peer's _federation_ TLS when it chains to that CA. Trust stays
**inside the hive** (agents + the matrix container), never the host **inside the hive** (agents + the matrix container), never the host
system trust store. **This is the knob that unblocks federation with system trust store. **This is the knob that unblocks federation with
a self-signed peer hive** — use it instead of `certFingerprint` when a self-signed peer hive** — use it instead of `certFingerprint` when

View file

@ -15,17 +15,18 @@ Submit a shell command for background execution (runs via `bash`).
Stdout and stderr stream to `harness/bash-tasks/<id>.{out,err}`. Stdout and stderr stream to `harness/bash-tasks/<id>.{out,err}`.
When the task completes (or times out, or the process errors), the 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 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 - 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). `timed_out`. Omit for no timeout (runs until natural exit).
- `wait_seconds` — inline poll before returning (capped at 30). * `wait_seconds` — inline poll before returning (capped at 30).
When the task finishes within the window the full status is When the task finishes within the window the full status is
returned immediately and no wake is fired; when the window expires returned immediately and no wake is fired; when the window expires
the task keeps running and the normal `task started: id=<id>` the task keeps running and the normal `task started: id=<id>`
response is returned. **Defaults to 3** — pass `wait_seconds: 0` response is returned. **Defaults to 3** — pass `wait_seconds: 0`
to disable inline waiting and always get the immediate response. to disable inline waiting and always get the immediate response.
- `name` — optional caller-chosen task id. When set it replaces the * `name` — optional caller-chosen task id. When set it replaces the
auto-generated hex id, so it surfaces in the wake `from` auto-generated hex id, so it surfaces in the wake `from`
(`bash-task-<name>`), in `status(<name>)` lookups, and in the (`bash-task-<name>`), in `status(<name>)` lookups, and in the
loose-ends list — a memorable label instead of an opaque id. A name loose-ends list — a memorable label instead of an opaque id. A name

View file

@ -102,16 +102,16 @@ hive-forge repo-labels --json # full label ob
Several verbs read an issue/PR and look overlapping, but each has a Several verbs read an issue/PR and look overlapping, but each has a
distinct output shape — pick by what you need: distinct output shape — pick by what you need:
| Need | Verb | Shape | | Need | Verb | Shape |
|------|------|-------| | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The full human view of one item — title, body, **and** every comment in order | `view <n>` | prose dump; **marks the thread read** (clears the read-before-comment guard). Top-level `view` auto-detects issue-or-PR by number; scoped `issue view <n>` / `pr view <n>` do the same when you already know the kind | | The full human view of one item — title, body, **and** every comment in order | `view <n>` | prose dump; **marks the thread read** (clears the read-before-comment guard). Top-level `view` auto-detects issue-or-PR by number; scoped `issue view <n>` / `pr view <n>` do the same when you already know the kind |
| Just the structured metadata of one item (state, labels, assignees, head sha…) | `issue <n>` / `pr <n>` | JSON object | | Just the structured metadata of one item (state, labels, assignees, head sha…) | `issue <n>` / `pr <n>` | JSON object |
| Only the comments (skip the body), e.g. to catch up a long thread; for PRs, review bodies (approve/request-changes/comment summaries) are merged in too | `comments <n>` [`--tail N`] | comment + review-body list; **marks read** | | Only the comments (skip the body), e.g. to catch up a long thread; for PRs, review bodies (approve/request-changes/comment summaries) are merged in too | `comments <n>` [`--tail N`] | comment + review-body list; **marks read** |
| One specific comment by its id | `comment-show <id>` | single comment | | One specific comment by its id | `comment-show <id>` | single comment |
| The audit trail — closes, label/assignee changes, commit refs, merges (no comment bodies) | `timeline <n>` | event list | | The audit trail — closes, label/assignee changes, commit refs, merges (no comment bodies) | `timeline <n>` | event list |
| The code change | `diff <n>` | unified diff | | The code change | `diff <n>` | unified diff |
| Is this PR ready to merge — mergeable + CI + reviews | `pr-status --pr <n>` | health view; exit code = merge-readiness | | Is this PR ready to merge — mergeable + CI + reviews | `pr-status --pr <n>` | health view; exit code = merge-readiness |
| The review verdicts + inline review comments | `pr-reviews <n>` | review list | | The review verdicts + inline review comments | `pr-reviews <n>` | review list |
Rule of thumb: **`view` first** when you're about to act on a thread (it Rule of thumb: **`view` first** when you're about to act on a thread (it
shows everything and clears the unread guard so your follow-up `comment` shows everything and clears the unread guard so your follow-up `comment`
@ -172,7 +172,7 @@ hive-forge pr-status --pr 42
# last comment: 2026-06-05T19:13:28+02:00 by argus # last comment: 2026-06-05T19:13:28+02:00 by argus
``` ```
Note: review verdicts come from *formal* Forgejo reviews (the Note: review verdicts come from _formal_ Forgejo reviews (the
approve / request-changes API). Reviewers who post their verdict as a approve / request-changes API). Reviewers who post their verdict as a
plain comment show under `last comment`, not `reviews`. plain comment show under `last comment`, not `reviews`.
@ -243,14 +243,14 @@ to discover valid label names before triaging or to audit the label set.
increment it). `--json` wraps the output. increment it). `--json` wraps the output.
- `ci-rerun` re-runs CI without pushing an empty commit (the old - `ci-rerun` re-runs CI without pushing an empty commit (the old
retrigger path, which littered PR history). Forgejo has no token-usable retrigger path, which littered PR history). Forgejo has no token-usable
REST endpoint to re-run an *existing* run (the run-page rerun buttons REST endpoint to re-run an _existing_ run (the run-page rerun buttons
are CSRF-gated web routes a token POST 404s), so this dispatches a are CSRF-gated web routes a token POST 404s), so this dispatches a
**fresh** run of the workflow via the workflow-dispatch API **fresh** run of the workflow via the workflow-dispatch API
(`POST …/actions/workflows/<workflow>/dispatches {"ref":"<branch>"}`). (`POST …/actions/workflows/<workflow>/dispatches {"ref":"<branch>"}`).
Resolve the branch with exactly one of: `--pr <n>` (the PR's head Resolve the branch with exactly one of: `--pr <n>` (the PR's head
branch), `--run <n>` (the same run number `ci-log` / `artifact-get` branch), `--run <n>` (the same run number `ci-log` / `artifact-get`
take — resolves the branch + workflow from that run), or `--branch take — resolves the branch + workflow from that run), or `--branch
<name>` (directly). `--workflow <file>` picks the workflow file for <name>` (directly). `--workflow <file>` picks the workflow file for
`--pr` / `--branch` (default `ci.yml`). Dispatch re-runs the whole `--pr` / `--branch` (default `ci.yml`). Dispatch re-runs the whole
workflow — there is no single-job variant. workflow — there is no single-job variant.
- Do NOT use raw `curl` for forge access -- the CLI handles auth, - Do NOT use raw `curl` for forge access -- the CLI handles auth,

View file

@ -76,13 +76,13 @@ agents after the approval resolves.
## Boundary summary ## Boundary summary
| Operation | Requires approval? | Scope | | Operation | Requires approval? | Scope |
|---|---|---| | --------------------------------------- | ------------------ | --------------------- |
| `kill` / `start` / `restart` / `update` | No | Direct children | | `kill` / `start` / `restart` / `update` | No | Direct children |
| `list_containers` | No | All descendants | | `list_containers` | No | All descendants |
| `request_init_config` | Yes (InitConfig) | New direct child only | | `request_init_config` | Yes (InitConfig) | New direct child only |
| `request_apply_commit` | Yes (ApplyCommit) | Direct children | | `request_apply_commit` | Yes (ApplyCommit) | Direct children |
| `request_update_meta_inputs` | Yes (MetaUpdate) | Meta flake (global) | | `request_update_meta_inputs` | Yes (MetaUpdate) | Meta flake (global) |
## See also ## See also

View file

@ -5,10 +5,10 @@ swap touches only the first:
1. **`colors.css`** — the 16 `--base00``--base0F` base16 slots. **This is 1. **`colors.css`** — the 16 `--base00``--base0F` base16 slots. **This is
the entire theme swap contract.** A generator (e.g. one fed a stylix the entire theme swap contract.** A generator (e.g. one fed a stylix
base16 scheme, which is natively base00base0F) replaces *only this base16 scheme, which is natively base00base0F) replaces _only this
file*. file_.
2. **`theme.css`** — the semantic layer: `--bg`, `--fg`, `--purple`, … 2. **`theme.css`** — the semantic layer: `--bg`, `--fg`, `--purple`, …
*derived* from the base16 slots (`--bg: var(--base00)` etc.). This is _derived_ from the base16 slots (`--bg: var(--base00)` etc.). This is
what the app references and it **never changes on a swap**. what the app references and it **never changes on a swap**.
Both files live in `frontend/packages/shared/src/`. Per-page stylesheets Both files live in `frontend/packages/shared/src/`. Per-page stylesheets
@ -34,68 +34,73 @@ Each semantic var derives from a base16 slot (or, for the three that have
no clean slot, a `color-mix()` over base16 — pixel-identical under the no clean slot, a `color-mix()` over base16 — pixel-identical under the
default palette). Default hexes shown are Catppuccin Mocha. default palette). Default hexes shown are Catppuccin Mocha.
| Variable | Derives from | Default hex | Use | | Variable | Derives from | Default hex | Use |
|---|---|---|---| | -------------- | --------------------------- | ----------- | --------------------------------------------------------------- |
| `--bg` | `base00` | `#1e1e2e` | page background | | `--bg` | `base00` | `#1e1e2e` | page background |
| `--bg-elev` | `base01` | `#181825` | elevated surfaces: floating dropdowns, popovers | | `--bg-elev` | `base01` | `#181825` | elevated surfaces: floating dropdowns, popovers |
| `--border` | `base02` | `#313244` | general borders, hover/active backgrounds | | `--border` | `base02` | `#313244` | general borders, hover/active backgrounds |
| `--purple-dim` | `base03` | `#45475a` | subtle borders, terminal chrome, badge backgrounds | | `--purple-dim` | `base03` | `#45475a` | subtle borders, terminal chrome, badge backgrounds |
| `--fg` | `base05` | `#cdd6f4` | primary text colour | | `--fg` | `base05` | `#cdd6f4` | primary text colour |
| `--red` | `base08` | `#f38ba8` | errors, fail state | | `--red` | `base08` | `#f38ba8` | errors, fail state |
| `--amber` | `base09` | `#fab387` | warnings, pending / running state | | `--amber` | `base09` | `#fab387` | warnings, pending / running state |
| `--yellow` | `base0A` | `#f9e2af` | flash messages, mild warnings | | `--yellow` | `base0A` | `#f9e2af` | flash messages, mild warnings |
| `--green` | `base0B` | `#a6e3a1` | success, ok state | | `--green` | `base0B` | `#a6e3a1` | success, ok state |
| `--cyan` | `base0C` | `#89dceb` | tool-use events, info accents | | `--cyan` | `base0C` | `#89dceb` | tool-use events, info accents |
| `--blue` | `base0D` | `#89b4fa` | links, interactive accent (distinct from cyan) | | `--blue` | `base0D` | `#89b4fa` | links, interactive accent (distinct from cyan) |
| `--purple` | `base0E` | `#cba6f7` | accent — active tabs, links, highlights | | `--purple` | `base0E` | `#cba6f7` | accent — active tabs, links, highlights |
| `--pink` | `base0F` | `#f5c2e7` | thinking events | | `--pink` | `base0F` | `#f5c2e7` | thinking events |
| `--crust` | `mix(base00 58%, #000)` | `#11111b` | terminal / code block background (below `--bg`) | | `--crust` | `mix(base00 58%, #000)` | `#11111b` | terminal / code block background (below `--bg`) |
| `--muted` | `mix(base05 55.5%, base00)` | `#7f849c` | secondary / dimmed text | | `--muted` | `mix(base05 55.5%, base00)` | `#7f849c` | secondary / dimmed text |
| `--subtext0` | `mix(base05 77.7%, base00)` | `#a6adc8` | toolbar/status text; dimmer than `--fg`, lighter than `--muted` | | `--subtext0` | `mix(base05 77.7%, base00)` | `#a6adc8` | toolbar/status text; dimmer than `--fg`, lighter than `--muted` |
## Common mistakes ## Common mistakes
The Catppuccin colour names do **not** map 1:1 to the variable names. The Catppuccin colour names do **not** map 1:1 to the variable names.
Variables to avoid (undefined — they will silently resolve to transparent / inherited): Variables to avoid (undefined — they will silently resolve to transparent / inherited):
| Wrong | Correct | | Wrong | Correct |
|---|---| | -------------------------- | --------------------------------------------------- |
| `--text` | `--fg` | | `--text` | `--fg` |
| `--mauve` | `--purple` | | `--mauve` | `--purple` |
| `--surface0` | `--bg-elev` (float bg) or `--border` (border/hover) | | `--surface0` | `--bg-elev` (float bg) or `--border` (border/hover) |
| `--surface1` | `--border` | | `--surface1` | `--border` |
| `--surface2` | `--purple-dim` | | `--surface2` | `--purple-dim` |
| `--overlay0`, `--overlay1` | `--muted` | | `--overlay0`, `--overlay1` | `--muted` |
| `--base`, `--mantle` | `--bg`, `--bg-elev` | | `--base`, `--mantle` | `--bg`, `--bg-elev` |
## Usage guide ## Usage guide
**Floating menus and dropdowns** (e.g. agent context menu, tabbar overflow): **Floating menus and dropdowns** (e.g. agent context menu, tabbar overflow):
```css ```css
background: var(--bg-elev); background: var(--bg-elev);
border: 1px solid var(--purple-dim); border: 1px solid var(--purple-dim);
``` ```
**Hover / active state backgrounds**: **Hover / active state backgrounds**:
```css ```css
background: var(--border); background: var(--border);
``` ```
**Active tab text / accent elements**: **Active tab text / accent elements**:
```css ```css
color: var(--purple); color: var(--purple);
``` ```
**Muted / meta text**: **Muted / meta text**:
```css ```css
color: var(--muted); color: var(--muted);
``` ```
**Error / warning / success badges**: **Error / warning / success badges**:
```css ```css
color: var(--red); /* error */ color: var(--red); /* error */
color: var(--amber); /* warning / running */ color: var(--amber); /* warning / running */
color: var(--green); /* ok */ color: var(--green); /* ok */
``` ```
## Theme swapping — the base16 contract ## Theme swapping — the base16 contract
@ -104,31 +109,31 @@ color: var(--green); /* ok */
semantic names.** A theme generator (e.g. one reading a stylix base16 semantic names.** A theme generator (e.g. one reading a stylix base16
scheme) overrides only `colors.css`; the semantic layer in `theme.css` scheme) overrides only `colors.css`; the semantic layer in `theme.css`
derives everything else, so the whole UI re-themes with nothing else to derives everything else, so the whole UI re-themes with nothing else to
template or regenerate. The base16 slot → semantic mapping is *internal* template or regenerate. The base16 slot → semantic mapping is _internal_
to `theme.css` (the "Derives from" column above) — a generator never to `theme.css` (the "Derives from" column above) — a generator never
needs to know our var names, and `theme.css` + the page bundles stay needs to know our var names, and `theme.css` + the page bundles stay
untouched. untouched.
`colors.css` base16 slot defaults (Catppuccin Mocha): `colors.css` base16 slot defaults (Catppuccin Mocha):
| Slot | Default | Standard base16 role | Mapped to | | Slot | Default | Standard base16 role | Mapped to |
|---|---|---|---| | -------- | --------- | -------------------- | --------------------------------------------------- |
| `base00` | `#1e1e2e` | default bg | `--bg`, (darkened) `--crust` | | `base00` | `#1e1e2e` | default bg | `--bg`, (darkened) `--crust` |
| `base01` | `#181825` | lighter bg | `--bg-elev` | | `base01` | `#181825` | lighter bg | `--bg-elev` |
| `base02` | `#313244` | selection/surface | `--border` | | `base02` | `#313244` | selection/surface | `--border` |
| `base03` | `#45475a` | comments/dim surface | `--purple-dim` | | `base03` | `#45475a` | comments/dim surface | `--purple-dim` |
| `base04` | `#585b70` | dark foreground | *(unused; kept for completeness)* | | `base04` | `#585b70` | dark foreground | _(unused; kept for completeness)_ |
| `base05` | `#cdd6f4` | default foreground | `--fg`, (blended) `--muted`/`--subtext0` | | `base05` | `#cdd6f4` | default foreground | `--fg`, (blended) `--muted`/`--subtext0` |
| `base06` | `#f5e0dc` | light foreground | *(unused)* | | `base06` | `#f5e0dc` | light foreground | _(unused)_ |
| `base07` | `#b4befe` | lightest | *(unused)* | | `base07` | `#b4befe` | lightest | _(unused)_ |
| `base08` | `#f38ba8` | red | `--red` | | `base08` | `#f38ba8` | red | `--red` |
| `base09` | `#fab387` | orange | `--amber` | | `base09` | `#fab387` | orange | `--amber` |
| `base0A` | `#f9e2af` | yellow | `--yellow` | | `base0A` | `#f9e2af` | yellow | `--yellow` |
| `base0B` | `#a6e3a1` | green | `--green` | | `base0B` | `#a6e3a1` | green | `--green` |
| `base0C` | `#89dceb` | cyan | `--cyan` (our *sky*; Catppuccin's `base0C` is teal) | | `base0C` | `#89dceb` | cyan | `--cyan` (our _sky_; Catppuccin's `base0C` is teal) |
| `base0D` | `#89b4fa` | blue | `--blue` | | `base0D` | `#89b4fa` | blue | `--blue` |
| `base0E` | `#cba6f7` | magenta | `--purple` | | `base0E` | `#cba6f7` | magenta | `--purple` |
| `base0F` | `#f5c2e7` | extra accent | `--pink` | | `base0F` | `#f5c2e7` | extra accent | `--pink` |
Notes for theme authors: Notes for theme authors:

View file

@ -3,7 +3,6 @@
> Part of [Web UI](../web-ui.md). See also: > Part of [Web UI](../web-ui.md). See also:
> [Dashboard layout](dashboard.md) · [Per-agent page](agent.md) > [Dashboard layout](dashboard.md) · [Per-agent page](agent.md)
- `GET /``index.html` from the bundled frontend dist (see - `GET /``index.html` from the bundled frontend dist (see
`frontend/`). Both binaries' routers declare their dynamic `frontend/`). Both binaries' routers declare their dynamic
endpoints first and then `fallback_service(ServeDir::new(...))` endpoints first and then `fallback_service(ServeDir::new(...))`
@ -169,8 +168,8 @@ subscribes to `streamUrl` (live tail). Both endpoints return
`.no-anim` class so they don't stagger in like live events. The `.no-anim` class so they don't stagger in like live events. The
optional `streamFactory(url)` callback lets the dashboard hand optional `streamFactory(url)` callback lets the dashboard hand
the factory a `SharedWorker`-backed `EventSource` facade (so the factory a `SharedWorker`-backed `EventSource` facade (so
multiple tabs share one upstream connection — see *SSE multiple tabs share one upstream connection — see _SSE
multiplexing* above); when omitted, the factory falls back to a multiplexing_ above); when omitted, the factory falls back to a
plain `new EventSource(url)`. plain `new EventSource(url)`.
**`linkify` (text-node based).** Bare `http(s)://` URLs in row **`linkify` (text-node based).** Bare `http(s)://` URLs in row
@ -298,10 +297,10 @@ without its last path segment.
That makes the page work under any prefix the agent ends up mounted That makes the page work under any prefix the agent ends up mounted
at without rebuilding the dist. The cases that matter: at without rebuilding the dist. The cases that matter:
| served at | `api/state` resolves to | | served at | `api/state` resolves to |
|---|---| | ------------------------------------------------ | ----------------------- |
| `/` (own port, today's shape) | `/api/state` | | `/` (own port, today's shape) | `/api/state` |
| `/agent/iris/` (gateway-prefixed) | `/agent/iris/api/state` | | `/agent/iris/` (gateway-prefixed) | `/agent/iris/api/state` |
| `/agent/iris/stats` (subpage, no trailing slash) | `/agent/iris/api/state` | | `/agent/iris/stats` (subpage, no trailing slash) | `/agent/iris/api/state` |
The gateway upstream config strips the prefix before forwarding to The gateway upstream config strips the prefix before forwarding to
@ -316,4 +315,3 @@ the relative-path resolution stays correct: `static/app.js` from
replaced), not `/stats/static/app.js`. Adding a trailing slash to replaced), not `/stats/static/app.js`. Adding a trailing slash to
those routes would break the resolution; either keep them those routes would break the resolution; either keep them
slash-less or use `<base href>` injection at serve time. slash-less or use `<base href>` injection at serve time.

View file

@ -10,31 +10,31 @@ Tools (hyperhive surface):
- `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(agent?)` — 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 to list your own threads — cheap server-side sweep useful at turn start. Pass `agent: "<name>"` to inspect a peer agent's threads. Direct child agents are always accessible. For non-children, the `query_agent_state` capability is required — without it the request is rejected with an error. - `mcp__hyperhive__get_loose_ends(agent?)` — 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 to list your own threads — cheap server-side sweep useful at turn start. Pass `agent: "<name>"` to inspect a peer agent's threads. Direct child agents are always accessible. For non-children, the `query_agent_state` capability is required — without it the request is rejected with an error.
- `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 — 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__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 `running: bool` (whether the container is up — when `false`, `status_text`/`status_set_at` are stale pre-stop values) and 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 `running: bool` (whether the container is up — when `false`, `status_text`/`status_set_at` are stale pre-stop values) and 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__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 (the harness already loops immediately on pending messages). No args. - `mcp__hyperhive__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 (the harness already loops immediately on pending messages). No args.
- `mcp__hyperhive__restart(name)`*(requires `lifecycle` tool group)* restart a direct child sub-agent (stop + start). The server enforces topology: the call is rejected unless `name` is a direct child of yours per `topology.json`. No approval required. - `mcp__hyperhive__restart(name)`_(requires `lifecycle` tool group)_ restart a direct child sub-agent (stop + start). The server enforces topology: the call is rejected unless `name` is a direct child of yours per `topology.json`. No approval required.
- `mcp__hyperhive__kill(name)`*(requires `lifecycle` tool group)* stop a direct child sub-agent (graceful). Direct children only — server enforces topology. State dir kept; recreating reuses prior config + credentials. No approval required. - `mcp__hyperhive__kill(name)`_(requires `lifecycle` tool group)_ stop a direct child sub-agent (graceful). Direct children only — server enforces topology. State dir kept; recreating reuses prior config + credentials. No approval required.
- `mcp__hyperhive__start(name)`*(requires `lifecycle` tool group)* start a stopped direct child sub-agent. Direct children only — server enforces topology. No approval required. - `mcp__hyperhive__start(name)`_(requires `lifecycle` tool group)_ start a stopped direct child sub-agent. Direct children only — server enforces topology. No approval required.
- `mcp__hyperhive__update(name)`*(requires `lifecycle` tool group)* rebuild a direct child sub-agent: re-applies the current hyperhive flake + agent.nix and restarts it. Direct children only — server enforces topology. No approval required. Idempotent. - `mcp__hyperhive__update(name)`_(requires `lifecycle` tool group)_ rebuild a direct child sub-agent: re-applies the current hyperhive flake + agent.nix and restarts it. Direct children only — server enforces topology. No approval required. Idempotent.
- `mcp__hyperhive__list_containers()`*(requires `lifecycle` tool group)* list all containers that are topological descendants of this agent (children + their subtrees). Returns each name with running/stopped status, ordered parents-first. Useful before kill/update/restart to check what's under you. - `mcp__hyperhive__list_containers()`_(requires `lifecycle` tool group)_ list all containers that are topological descendants of this agent (children + their subtrees). Returns each name with running/stopped status, ordered parents-first. Useful before kill/update/restart to check what's under you.
- `mcp__hyperhive__request_init_config(name, description?)`*(requires `approvals` tool group)* initialise a brand-new direct child agent's proposed config repo. Queues an `InitConfig` approval; on approval hive-c0re seeds `/agents/<name>/config/agent.nix`. `name` must be a direct child in the topology tree — server enforces. Fails if the config repo already exists (use `request_apply_commit` instead). - `mcp__hyperhive__request_init_config(name, description?)`_(requires `approvals` tool group)_ initialise a brand-new direct child agent's proposed config repo. Queues an `InitConfig` approval; on approval hive-c0re seeds `/agents/<name>/config/agent.nix`. `name` must be a direct child in the topology tree — server enforces. Fails if the config repo already exists (use `request_apply_commit` instead).
- `mcp__hyperhive__request_apply_commit(agent, commit_ref, description?)`*(requires `approvals` tool group)* submit a config commit for a direct child agent, queued for operator approval. `agent` must be a direct child in the topology tree — server enforces. `commit_ref` must be a 7-40 char hex sha (not a branch/tag name). On approval hive-c0re rebuilds the container with the pinned commit. - `mcp__hyperhive__request_apply_commit(agent, commit_ref, description?)`_(requires `approvals` tool group)_ submit a config commit for a direct child agent, queued for operator approval. `agent` must be a direct child in the topology tree — server enforces. `commit_ref` must be a 7-40 char hex sha (not a branch/tag name). On approval hive-c0re rebuilds the container with the pinned commit.
- `mcp__hyperhive__request_update_meta_inputs(inputs?, description?)`*(requires `approvals` tool group)* queue an approval for the operator 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; lock update runs on operator approval. Does NOT trigger rebuilds — call `update(name)` on affected agents after approval resolves. - `mcp__hyperhive__request_update_meta_inputs(inputs?, description?)`_(requires `approvals` tool group)_ queue an approval for the operator 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; lock update runs on operator approval. Does NOT trigger rebuilds — call `update(name)` on affected agents after approval resolves.
- `mcp__hyperhive__request_schedule_prompt(targets, body, first_fire_at_unix, interval_seconds?, description?)`*(requires `scheduling` tool group)* queue an approval for the operator to add a scheduled prompt. On approve hive-c0re inserts a schedule row and the worker fans `body` out to each agent in `targets` at `first_fire_at_unix` (recurring every `interval_seconds` if set, one-shot when absent). Catch-up clamp: long downtime fires ONCE per recurring row on resume. - `mcp__hyperhive__request_schedule_prompt(targets, body, first_fire_at_unix, interval_seconds?, description?)`_(requires `scheduling` tool group)_ queue an approval for the operator to add a scheduled prompt. On approve hive-c0re inserts a schedule row and the worker fans `body` out to each agent in `targets` at `first_fire_at_unix` (recurring every `interval_seconds` if set, one-shot when absent). Catch-up clamp: long downtime fires ONCE per recurring row on resume.
- `mcp__hyperhive__cancel_schedule(id, targets?)`*(requires `scheduling` tool group)* cancel a schedule. Omit `targets` / pass empty to cancel the whole schedule; pass a list to cancel just those recipients. Authorization: you can cancel schedules you own OR any owned by a sub-agent in your subtree. - `mcp__hyperhive__cancel_schedule(id, targets?)`_(requires `scheduling` tool group)_ cancel a schedule. Omit `targets` / pass empty to cancel the whole schedule; pass a list to cancel just those recipients. Authorization: you can cancel schedules you own OR any owned by a sub-agent in your subtree.
- `mcp__hyperhive__fire_schedule_now(id)`*(requires `scheduling` tool group)* fire a scheduled prompt out of band immediately. Recurring schedules keep their cadence intact; one-shot schedules are consumed. Same authorization as `cancel_schedule`. - `mcp__hyperhive__fire_schedule_now(id)`_(requires `scheduling` tool group)_ fire a scheduled prompt out of band immediately. Recurring schedules keep their cadence intact; one-shot schedules are consumed. Same authorization as `cancel_schedule`.
- `mcp__hyperhive__edit_schedule(id, body?, description?, interval_seconds?, next_fire_at_unix?, targets_add?, targets_remove?)`*(requires `scheduling` tool group)* partial-update a schedule's mutable fields. Pass only the fields you want to change. `targets_add` / `targets_remove` mutate the recipient list in the same transaction. Refuses cancelled rows. Same authorization as `cancel_schedule`. - `mcp__hyperhive__edit_schedule(id, body?, description?, interval_seconds?, next_fire_at_unix?, targets_add?, targets_remove?)`_(requires `scheduling` tool group)_ partial-update a schedule's mutable fields. Pass only the fields you want to change. `targets_add` / `targets_remove` mutate the recipient list in the same transaction. Refuses cancelled rows. Same authorization as `cancel_schedule`.
- `mcp__hyperhive__list_schedules()`*(requires `scheduling` tool group)* snapshot every schedule in the queue. Returns id, owner, body, target set with per-target `last_fired_at` + `last_result`, `next_fire_at_unix`, recurring `interval_seconds`. - `mcp__hyperhive__list_schedules()`_(requires `scheduling` tool group)_ snapshot every schedule in the queue. Returns id, owner, body, target set with per-target `last_fired_at` + `last_result`, `next_fire_at_unix`, recurring `interval_seconds`.
- `mcp__hyperhive__get_logs(agent, lines?)`*(requires `diagnostics` tool group)* fetch recent journal lines for a sub-agent container. Pass the plain logical agent name; `lines` defaults to 50 (capped at 500). - `mcp__hyperhive__get_logs(agent, lines?)`_(requires `diagnostics` tool group)_ fetch recent journal lines for a sub-agent container. Pass the plain logical agent name; `lines` defaults to 50 (capped at 500).
- `mcp__hyperhive__get_host_journal(unit?, container?, lines?, priority?, grep?, since?, until?)`*(requires `read_host_journal` capability)* fetch recent lines from the host journal. All filters optional — omit to get the last N host journal lines. `unit`: filter to a systemd unit (e.g. `hive-c0re.service`). `container`: nspawn machine name verbatim (e.g. `h-iris`). Omit for host journal. Agent containers use the `h-` prefix (e.g. `h-iris`); infrastructure containers use their full name (e.g. `hive-ci`, `hive-forge`, `hive-matrix`, `hive-gateway`). `lines`: how many lines (default 30, max 100). `priority`: minimum syslog level enum. `grep`: regex matched against log message fields (journalctl --grep). `since`: show entries on or newer than this (e.g. `-1h`, `2024-01-01 12:00:00`). `until`: show entries on or older than this. - `mcp__hyperhive__get_host_journal(unit?, container?, lines?, priority?, grep?, since?, until?)`_(requires `read_host_journal` capability)_ fetch recent lines from the host journal. All filters optional — omit to get the last N host journal lines. `unit`: filter to a systemd unit (e.g. `hive-c0re.service`). `container`: nspawn machine name verbatim (e.g. `h-iris`). Omit for host journal. Agent containers use the `h-` prefix (e.g. `h-iris`); infrastructure containers use their full name (e.g. `hive-ci`, `hive-forge`, `hive-matrix`, `hive-gateway`). `lines`: how many lines (default 30, max 100). `priority`: minimum syslog level enum. `grep`: regex matched against log message fields (journalctl --grep). `since`: show entries on or newer than this (e.g. `-1h`, `2024-01-01 12:00:00`). `until`: show entries on or older than this.
Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — ask a peer agent with the `approvals` tool group, or contact the operator directly. Config repos live at `/agents/{label}/config/` (read-only inside your container). All changes flow through operator-approved commits. Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — ask a peer agent with the `approvals` tool group, or contact the operator directly. Config repos live at `/agents/{label}/config/` (read-only inside your container). All changes flow through operator-approved commits.
Your config repo is mounted **read-only** at `/agents/{label}/config/``agent.nix` plus whatever extra files define you (declared packages, env vars, MCP servers). Read it to see exactly what defines you before asking for a change, so you can point at the precise file and line. Your config repo is mounted **read-only** at `/agents/{label}/config/``agent.nix` plus whatever extra files define you (declared packages, env vars, MCP servers). Read it to see exactly what defines you before asking for a change, so you can point at the precise file and line.
Approval boundary: lifecycle ops on *existing* direct children (`kill`, `start`, `restart`) are at your discretion — no operator approval needed (requires `lifecycle` tool group). *Creating* a new agent (two-step: `request_init_config` + `request_apply_commit`) and *changing* any agent's config (`request_apply_commit`) both go through the approval queue (requires `approvals` tool group). The operator only signs off on changes; you run the day-to-day. Approval boundary: lifecycle ops on _existing_ direct children (`kill`, `start`, `restart`) are at your discretion — no operator approval needed (requires `lifecycle` tool group). _Creating_ a new agent (two-step: `request_init_config` + `request_apply_commit`) and _changing_ any agent's config (`request_apply_commit`) both go through the approval queue (requires `approvals` tool group). The operator only signs off on changes; you run the day-to-day.
Messages from sender `system` are hyperhive helper events (JSON body, `event` field discriminates): `approval_resolved`, `config_ready`, `spawned`, `rebuilt`, `killed`, `destroyed`, `container_crash`, `needs_login`, `logged_in`, `needs_update`, `question_asked`, `question_answered`. Use these to react to lifecycle changes: Messages from sender `system` are hyperhive helper events (JSON body, `event` field discriminates): `approval_resolved`, `config_ready`, `spawned`, `rebuilt`, `killed`, `destroyed`, `container_crash`, `needs_login`, `logged_in`, `needs_update`, `question_asked`, `question_answered`. Use these to react to lifecycle changes:
@ -63,6 +63,6 @@ Keep messages short — a few sentences each. For anything big (file listings, l
When your inbox has a message, handle it and stop. Don't narrate intent — act. When your inbox has a message, handle it and stop. Don't narrate intent — act.
**Turns are your checkpoint.** The harness runs one claude turn per inbox message; when you stop, it acknowledges that message and your `--continue` session is saved to disk. Ending the turn is how you commit progress — both the session and the inbox acknowledgement. If the container restarts *while a turn is still running*, the message that drove it was never acknowledged, so it gets redelivered on the next boot, prefixed `[redelivered after harness restart — may already be handled]`. A long single turn that does step after step widens the window where a restart loses work and forces that redelivery, so prefer short turns: do a unit of work, write anything durable under `/agents/{label}/state/`, and end. **Turns are your checkpoint.** The harness runs one claude turn per inbox message; when you stop, it acknowledges that message and your `--continue` session is saved to disk. Ending the turn is how you commit progress — both the session and the inbox acknowledgement. If the container restarts _while a turn is still running_, the message that drove it was never acknowledged, so it gets redelivered on the next boot, prefixed `[redelivered after harness restart — may already be handled]`. A long single turn that does step after step widens the window where a restart loses work and forces that redelivery, so prefer short turns: do a unit of work, write anything durable under `/agents/{label}/state/`, and end.
**To keep working without waiting for a new message, call `request_next_turn()`** before you stop. The harness immediately starts a fresh turn with `from: "self"`, `body: "continue"` — the supported way to run multi-step work (long builds, sequential edits) as a series of checkpointed turns rather than one monolithic turn. Don't busy-wait inside a turn for a condition to resolve: end the turn and let the next wake drive the continuation — a `remind` you scheduled, an external event, a backgrounded bash task's completion, or `request_next_turn()`. (A long-poll `recv(wait_seconds: …)` blocks *within* the current turn — it parks for new inbox messages but does not end the turn or checkpoint, so it isn't a substitute for ending the turn.) **To keep working without waiting for a new message, call `request_next_turn()`** before you stop. The harness immediately starts a fresh turn with `from: "self"`, `body: "continue"` — the supported way to run multi-step work (long builds, sequential edits) as a series of checkpointed turns rather than one monolithic turn. Don't busy-wait inside a turn for a condition to resolve: end the turn and let the next wake drive the continuation — a `remind` you scheduled, an external event, a backgrounded bash task's completion, or `request_next_turn()`. (A long-poll `recv(wait_seconds: …)` blocks _within_ the current turn — it parks for new inbox messages but does not end the turn or checkpoint, so it isn't a substitute for ending the turn.)