docs: fix write-good.So/ThereIs/Weasel lint findings

Fixes the "obvious ones first" slice of #4042 (mara: do the obvious
ones first) -- 81 hits across write-good.So, write-good.ThereIs, and
write-good.Weasel, all in docs/. Each is a genuine sentence rewrite
(lead with the real subject instead of "There is/are", drop a
sentence-initial "So ", replace a vague intensifier), not a blind
regex substitution -- read every hit in its real file context before
touching it.

3 of the 81 hits were in CI-generated CLI docs (docs/tools/{hivectl,
swarmctl,forge}-cli.md) -- fixed at the clap #[arg(...)]/doc-comment
source in hivectl/src/cli.rs, swarmctl/src/main.rs, and
hive-forge/src/verbs/repo_add_collaborator.rs, then regenerated via
each crate's `markdown-docs` subcommand so CI's freshness check stays
green.

Verified: fresh vale re-run shows 0 remaining So/ThereIs/Weasel hits
and no new hits introduced (983->982, exactly the one incidental fix
this pass also picked up at docs/scheduler/observability.md:48).
cargo fmt --check and clippy clean on the three touched crates.

Remaining write-good backlog (Passive: 726, TooWordy: 207) is
judgment-heavy and left for a follow-up slice of #4042, not bulk-
rewritten here.
This commit is contained in:
iris 2026-09-07 16:47:54 +02:00 committed by mara
commit e82a735745
35 changed files with 99 additions and 99 deletions

View file

@ -178,8 +178,8 @@ nspawn agent. Open questions, not yet wired:
One harness serve binary (`hive-agent`, with its `hive-agent-mcp` One harness serve binary (`hive-agent`, with its `hive-agent-mcp`
sibling), one shared `nix/agent-modules/` tree, one service unit sibling), one shared `nix/agent-modules/` tree, one service unit
(`systemd.services.hive-agent`) for all agents. There is no separate (`systemd.services.hive-agent`) for all agents. No separate manager
manager service name or role distinction in the harness — privilege service name or role distinction exists in the harness — privilege
differences live server-side in the broker socket (which tool groups differences live server-side in the broker socket (which tool groups
and manager-surface calls each agent receives). and manager-surface calls each agent receives).

View file

@ -56,7 +56,7 @@ flow.
Config changes flow through a **forge pull request** on the agent's Config changes flow through a **forge pull request** on the agent's
`agent-configs/<name>` repo — the same surface agents use for code PRs. `agent-configs/<name>` repo — the same surface agents use for code PRs.
There is no bespoke MCP tool for config changes: opening the PR IS the No bespoke MCP tool exists for config changes: opening the PR IS the
request. request.
1. The submitting agent (the child's parent, holding the `approvals` 1. The submitting agent (the child's parent, holding the `approvals`
@ -349,7 +349,7 @@ per-hive config — is the idempotent reconciler called by `spawn`,
`destroy`, `rebuild`, and the startup migration. Renders `flake.nix` `destroy`, `rebuild`, and the startup migration. Renders `flake.nix`
from the agent list; if it differs from disk, runs from the agent list; if it differs from disk, runs
`nix flake lock` + commits as `regenerate meta flake` (or `nix flake lock` + commits as `regenerate meta flake` (or
`seed meta from N agent(s)` on the very first call). `seed meta from N agent(s)` on the first call).
The root agent has `/meta` RO-bound inside its container: The root agent has `/meta` RO-bound inside its container:
`git -C /meta log --oneline` is the swarm-wide deploy log, `git -C /meta log --oneline` is the swarm-wide deploy log,
@ -499,8 +499,8 @@ current config — not an editing surface.
An agent with the `approvals` tool group submits a change the same way An agent with the `approvals` tool group submits a change the same way
any other change is made: **clone the child's config repo from the any other change is made: **clone the child's config repo from the
forge into its own state dir, commit on a branch, open a PR**, and let forge into its own state dir, commit on a branch, open a PR**, and let
the operator review and approve it. There is deliberately no second, the operator review and approve it. By design, no second, mount-shaped
mount-shaped path that reaches the same file without the review. path reaches the same file without the review.
Agents holding the `can_manage_top_level_agents` topology role (see Agents holding the `can_manage_top_level_agents` topology role (see
`hive-c0re/src/agent_config/topology.rs`) get additional host-side `hive-c0re/src/agent_config/topology.rs`) get additional host-side
@ -593,8 +593,8 @@ updates the root agent itself.
The system prompt (`hive-agent/prompts/system.md`, rendered by The system prompt (`hive-agent/prompts/system.md`, rendered by
`hive-agent/src/prompt.rs`) is the **same for every agent**; what `hive-agent/src/prompt.rs`) is the **same for every agent**; what
varies is which MCP tools are surfaced (gated by tool groups and varies is which MCP tools are surfaced (gated by tool groups and
capabilities in `agent.nix`). There is no `role:manager` block that capabilities in `agent.nix`). No `role:manager` block renders only
renders only for the root agent. The root agent's approval-gating for the root agent. The root agent's approval-gating
behaviour comes from its CLAUDE.md / agent-specific instructions, not behaviour comes from its CLAUDE.md / agent-specific instructions, not
the system prompt template. the system prompt template.

View file

@ -72,7 +72,7 @@ power-intent registry:
last_fired_at_unix / last_result`. `ON DELETE CASCADE` from last_fired_at_unix / last_result`. `ON DELETE CASCADE` from
`scheduled_prompts(id)` — requires `PRAGMA foreign_keys = ON` `scheduled_prompts(id)` — requires `PRAGMA foreign_keys = ON`
per connection (set at open). per connection (set at open).
- `agent_power` — one tiny row per agent: `agent PK / wanted (up | - `agent_power` — one row per agent: `agent PK / wanted (up |
offline) / updated_at`, owned by `hive-c0re/src/stores/power.rs`. offline) / updated_at`, owned by `hive-c0re/src/stores/power.rs`.
This is the durable power *intent* the job queue reconciles the This is the durable power *intent* the job queue reconciles the
observed container state against; intent survives hive-c0re observed container state against; intent survives hive-c0re
@ -158,7 +158,7 @@ host-side). Best-effort and created on first write
(`CREATE TABLE IF NOT EXISTS`), so it's absent until a bash (`CREATE TABLE IF NOT EXISTS`), so it's absent until a bash
task runs. task runs.
turn-stats.sqlite has **no vacuum** — it's one tiny row per turn turn-stats.sqlite has **no vacuum** — it's one small row per turn
(~hundreds of KB even over months), read directly by the `/stats` page (~hundreds of KB even over months), read directly by the `/stats` page
and the hive-wide stats view, so pruning it would only lose trend and the hive-wide stats view, so pruning it would only lose trend
history for no space gain. history for no space gain.
@ -409,7 +409,7 @@ Contents:
The root agent has the meta dir RO-mounted at `/meta/`. The root agent has the meta dir RO-mounted at `/meta/`.
There is no longer a `.meta-migration-done` marker: the The `.meta-migration-done` marker no longer exists: the
one-shot container repoint it guarded has been removed, since one-shot container repoint it guarded has been removed, since
containers are rendered onto `meta#<n>` at creation. A stale containers are rendered onto `meta#<n>` at creation. A stale
marker file left over from an older hive is inert and can be marker file left over from an older hive is inert and can be
@ -516,7 +516,7 @@ marker-guarded so the substantive moves only happen once per
container lifetime: container lifetime:
1. **`${homeDir}` exists with the right ownership** — covers the 1. **`${homeDir}` exists with the right ownership** — covers the
very first boot before `useradd`'s `createHome` has had a the first boot before `useradd`'s `createHome` has had a
chance to chown. Also re-applies on every rebuild in case the chance to chown. Also re-applies on every rebuild in case the
meta-flake's per-agent name evolves (rare). meta-flake's per-agent name evolves (rare).
2. **Migrate any leftover `/root/.claude` content into 2. **Migrate any leftover `/root/.claude` content into

View file

@ -117,7 +117,7 @@ read on forge straight away (`PATCH /notifications/threads/{id}`). The
broker inbox is the durable work queue now — each delivered wake is a broker inbox is the durable work queue now — each delivered wake is a
sqlite row with its own ack lifecycle — so the forge unread flag no sqlite row with its own ack lifecycle — so the forge unread flag no
longer needs to track whether the agent has _processed_ a longer needs to track whether the agent has _processed_ a
notification. Clearing it on delivery keeps forge's unread set **tiny notification. Clearing it on delivery keeps forge's unread set **small
by construction**: at rest it holds only threads that failed to by construction**: at rest it holds only threads that failed to
deliver plus whatever arrived since the last 30s poll. deliver plus whatever arrived since the last 30s poll.
@ -140,7 +140,7 @@ wake already carries the notification body, so _delivery is the read_.
An agent that wants the full thread still runs `hive-forge comments` / An agent that wants the full thread still runs `hive-forge comments` /
`view`. `view`.
**In-process dedupe (tiny, ephemeral).** A single-process map (thread **In-process dedupe (small, ephemeral).** A single-process map (thread
id → last-delivered `updated_at`) guards the narrow window where a id → last-delivered `updated_at`) guards the narrow window where a
mark-read call _transiently fails_ and the thread reappears unread in mark-read call _transiently fails_ and the thread reappears unread in
the next poll before its `updated_at` bumps — so a flaky PATCH doesn't the next poll before its `updated_at` bumps — so a flaky PATCH doesn't
@ -165,7 +165,7 @@ The poller starts disabled and stays that way for any of:
- Initial client construction fails (the typed `forgejo-api` client - Initial client construction fails (the typed `forgejo-api` client
for the API calls, or the plain reqwest client kept for the for the API calls, or the plain reqwest client kept for the
best-effort enrichment fetches of server-provided subject URLs; best-effort enrichment fetches of server-provided subject URLs;
both extremely unlikely; treated as fatal-to-the-task only). both unlikely; treated as fatal-to-the-task only).
Disabled = the spawned task returns immediately. All other failure Disabled = the spawned task returns immediately. All other failure
modes (HTTP errors, parse errors, mark-read failures) are modes (HTTP errors, parse errors, mark-read failures) are
@ -260,7 +260,7 @@ path. But a merged/closed subject **keeps** its `latest_comment_url` set,
so a just-merged PR that had any prior discussion would route to the so a just-merged PR that had any prior discussion would route to the
comment path and render `[comment on PR]` (with a stale pre-merge comment comment path and render `[comment on PR]` (with a stale pre-merge comment
body) instead of `[PR merged]` — the agent never learns its PR merged. body) instead of `[PR merged]` — the agent never learns its PR merged.
So when the notification IS the merge/close transition — its When the notification IS the merge/close transition — its
event time (`updated_at`) is within `NEW_ITEM_TOLERANCE_SECS` of the event time (`updated_at`) is within `NEW_ITEM_TOLERANCE_SECS` of the
subject's `closed_at` (set for both `merged` and `closed`) — the subject's `closed_at` (set for both `merged` and `closed`) — the
state-change path wins even with a comment url present state-change path wins even with a comment url present
@ -281,7 +281,7 @@ its `created_at` is strictly **after** the subject's `closed_at`
(`comment_is_after_close`) — that is it raced the merge rather than being the (`comment_is_after_close`) — that is it raced the merge rather than being the
pre-merge last comment the subject keeps — append it as a pre-merge last comment the subject keeps — append it as a
`comment by <author>: <excerpt>` block before the meta suffix `comment by <author>: <excerpt>` block before the meta suffix
(`fresh_post_close_comment_tail`). So the wake carries **both** `[PR merged]` (`fresh_post_close_comment_tail`). The wake carries **both** `[PR merged]`
and the racing comment. The kept pre-merge comment (created before and the racing comment. The kept pre-merge comment (created before
`closed_at`) is left off, a self-authored racing comment is dropped (don't `closed_at`) is left off, a self-authored racing comment is dropped (don't
echo the agent's own write), and a missing/unparseable `created_at`/ echo the agent's own write), and a missing/unparseable `created_at`/
@ -295,7 +295,7 @@ A review submitted with **no body** carries no `latest_comment_url`,
so it misses the comment path and lands on the state-change path with so it misses the comment path and lands on the state-change path with
`state == "open"` — exactly like a freshly opened PR. Labeling that `state == "open"` — exactly like a freshly opened PR. Labeling that
`new PR` is misleading: agents dismiss it as a duplicate of the `new PR` is misleading: agents dismiss it as a duplicate of the
original open notification and miss the review. So the `open` original open notification and miss the review. 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>`

View file

@ -13,8 +13,8 @@ injected into the agent's state dir out of band.
## Enabling ## Enabling
The integration is **on by default** for every agent (`hyperhive.github.enable The integration is **on by default** for every agent (`hyperhive.github.enable
= true`), inert until a PAT is provisioned. There is nothing per-agent to = true`), inert until a PAT is provisioned. No per-agent declaration is
declare — an agent gains GitHub by having a PAT written to its token needed — an agent gains GitHub by having a PAT written to its token
file. file.
To turn it off for the whole hive, set the host option: To turn it off for the whole hive, set the host option:
@ -35,7 +35,7 @@ github.com only. The token **value** never touches nix — it's written to
The PAT is operator-supplied. The primary path is the **dashboard The PAT is operator-supplied. The primary path is the **dashboard
credentials tab** (github sub-tab): paste the PAT for an agent and submit credentials tab** (github sub-tab): paste the PAT for an agent and submit
(`POST /api/github-account`). There is also a CLI path for (`POST /api/github-account`). A CLI path also exists for
recovery/scripting: recovery/scripting:
```sh ```sh

View file

@ -45,7 +45,7 @@ hive-c0re maintains the local clone at
pull`, so agents see the new content on their next turn. pull`, so agents see the new content on their next turn.
A hive that's offline when the event is sent doesn't get it on A hive that's offline when the event is sent doesn't get it on
reconnect — the periodic pull below is what closes that gap. So one reconnect — the periodic pull below is what closes that gap. One
hive briefly showing older `/knowledge` content than another is hive briefly showing older `/knowledge` content than another is
expected, and resolves by itself within the fallback interval. expected, and resolves by itself within the fallback interval.

View file

@ -19,7 +19,7 @@ Single nginx in front of every hyperhive web surface. Runs on the **host**, next
The authelia vhost is declared only by the host that **runs** authelia, not by every hive that uses it — a client hive knows the swarm's `authelia.url` but must not answer for a name it doesn't serve. Its server name is exactly `swarm.authelia.domain`: authelia validates `authelia_url ⊂ session cookie domain` at startup, so a near-miss is a container that refuses to boot. It carries no `auth_basic` — the login page must not sit behind the login mechanism it replaces — and sets the four `X-Forwarded-{Proto,Host,Uri,For}` headers, since authelia decides by the *original* request rather than the hop it sees. The authelia vhost is declared only by the host that **runs** authelia, not by every hive that uses it — a client hive knows the swarm's `authelia.url` but must not answer for a name it doesn't serve. Its server name is exactly `swarm.authelia.domain`: authelia validates `authelia_url ⊂ session cookie domain` at startup, so a near-miss is a container that refuses to boot. It carries no `auth_basic` — the login page must not sit behind the login mechanism it replaces — and sets the four `X-Forwarded-{Proto,Host,Uri,For}` headers, since authelia decides by the *original* request rather than the hop it sees.
⚠️ **A `502` from this vhost usually means authelia has no users yet, not that the proxy is misconfigured.** Authelia treats an empty user store as a fatal startup error, so an enabled-but-unbootstrapped swarm crash-loops the container while the vhost in front of it works perfectly. Check `journalctl -M swarm-authelia -u authelia-swarm` before suspecting anything here; the bootstrap step is in [`swarm/sso.md`](../swarm/sso.md). ⚠️ **A `502` from this vhost typically means authelia has no users yet, not that the proxy is misconfigured.** Authelia treats an empty user store as a fatal startup error, so an enabled-but-unbootstrapped swarm crash-loops the container while the vhost in front of it works perfectly. Check `journalctl -M swarm-authelia -u authelia-swarm` before suspecting anything here; the bootstrap step is in [`swarm/sso.md`](../swarm/sso.md).
Per-agent UIs stay sub-path, forge and matrix get sub-domains — see Per-agent UIs stay sub-path, forge and matrix get sub-domains — see
[Sub-domain shape (rationale)](#sub-domain-shape-rationale) below for why. [Sub-domain shape (rationale)](#sub-domain-shape-rationale) below for why.
@ -34,7 +34,7 @@ Operator points client at `<hive>`. Sequence:
matrix-dart-sdk (FluffyChat etc.) hardcodes `https` for the well-known fetch regardless of input scheme, so the discovery endpoint MUST be https — see "Self-signed TLS" below for the cert generation that backs the default-on path. matrix-dart-sdk (FluffyChat etc.) hardcodes `https` for the well-known fetch regardless of input scheme, so the discovery endpoint MUST be https — see "Self-signed TLS" below for the cert generation that backs the default-on path.
Federation peers fetch `.well-known/matrix/server``{"m.server":"chat.<swarm>:<httpsPort>"}` (the federation delegation always carries an explicit port, even the HTTPS default 443 — the https-implies-443 elision only applies to the client base_url above). Gateway only listens on configured `port` (+ `httpsPort` when TLS on); cross-hive federation needs either an SRV record (`_matrix._tcp.chat.<swarm>` → port 80 / 443) OR `matrix.openFirewall = true` so peers reach tuwunel's federation port directly. Hyperhive is mostly closed/internal, so this rarely bites. Federation peers fetch `.well-known/matrix/server``{"m.server":"chat.<swarm>:<httpsPort>"}` (the federation delegation always carries an explicit port, even the HTTPS default 443 — the https-implies-443 elision only applies to the client base_url above). Gateway only listens on configured `port` (+ `httpsPort` when TLS on); cross-hive federation needs either an SRV record (`_matrix._tcp.chat.<swarm>` → port 80 / 443) OR `matrix.openFirewall = true` so peers reach tuwunel's federation port directly. Hyperhive is closed/internal in most deployments, so this rarely bites.
## SPA fallback (Accept-header pattern) ## SPA fallback (Accept-header pattern)
@ -252,7 +252,7 @@ or make the key world-readable (`0644`) if your threat model allows it. nginx er
### Fronting with an external TLS terminator ### Fronting with an external TLS terminator
There is no http-only mode (see [TLS modes](#tls-modes) above). Two paths No http-only mode exists (see [TLS modes](#tls-modes) above). Two paths
for an operator who wants their own TLS terminator: for an operator who wants their own TLS terminator:
- give the gateway the real cert via `tls.certDir` (or `tls.acme`) so it - give the gateway the real cert via `tls.certDir` (or `tls.acme`) so it

View file

@ -12,7 +12,7 @@ whenever hyperhive is enabled. Configured via
## Network map ## Network map
One picture of the whole hive. There are two planes: **infra One picture of the whole hive two planes: **infra
containers share the host netns** and bind host ports directly; containers share the host netns** and bind host ports directly;
**compute containers (agents + CI) each get a private netns** behind **compute containers (agents + CI) each get a private netns** behind
the bridge. The unix-socket control plane rides the VFS and is the bridge. The unix-socket control plane rides the VFS and is
@ -129,14 +129,14 @@ own resolvers: dnsmasq runs on the host and reads the host's
upstream — they query the bridge IP and dnsmasq does the right thing upstream — they query the bridge IP and dnsmasq does the right thing
per-name. per-name.
There is deliberately no fallback `server=`: dnsmasq queries all known No fallback `server=` exists, by design: dnsmasq queries all known
upstreams in parallel, so a hardcoded public resolver would take a share upstreams in parallel, so a hardcoded public resolver would take a share
of normal traffic, not just cover the gap. of normal traffic, not just cover the gap.
dnsmasq runs on the host and reads the host's `/etc/resolv.conf` dnsmasq runs on the host and reads the host's `/etc/resolv.conf`
directly, so a network change (new router, new lease, laptop moving directly, so a network change (new router, new lease, laptop moving
networks) reaches it the moment openresolv rewrites the file. There is networks) reaches it the moment openresolv rewrites the file. Nothing
nothing to synchronise and no unit watching for it. needs synchronising, and no unit watches for it.
`bind-interfaces` + `interface = [ bridgeName "lo" ]` means the `bind-interfaces` + `interface = [ bridgeName "lo" ]` means the
listener only accepts queries from the bridge interface (plus lo for listener only accepts queries from the bridge interface (plus lo for
@ -228,7 +228,7 @@ creating or updating containers. Both variables are validated **once at
daemon startup**, not per container: they're process-global, so a daemon startup**, not per container: they're process-global, so a
missing or malformed value is a misconfigured daemon rather than one bad missing or malformed value is a misconfigured daemon rather than one bad
container, and failing at boot gives a single diagnostic instead of one container, and failing at boot gives a single diagnostic instead of one
per agent. There is no non-isolated mode to fall back to. `LOCAL_ADDRESS` is left empty so the per agent. No non-isolated mode exists to fall back to. `LOCAL_ADDRESS` is left empty so the
container's dhcpcd acquires an address from the bridge dnsmasq pool container's dhcpcd acquires an address from the bridge dnsmasq pool
(`networking.useDHCP = true` in `nix/agent-modules/network.nix`). This applies uniformly (`networking.useDHCP = true` in `nix/agent-modules/network.nix`). This applies uniformly
to all containers — agents and service containers alike. to all containers — agents and service containers alike.

View file

@ -71,14 +71,14 @@ role this host performs_. A store host sets both --- one to run the
receiver, one only if it also runs a hive that pushes. receiver, one only if it also runs a hive that pushes.
With it set, `hivectl agent <name> subvol snapshot push <label> With it set, `hivectl agent <name> subvol snapshot push <label>
[--parent <label>]` streams a snapshot straight into the store. There is [--parent <label>]` streams a snapshot straight into the store. No
no destination argument, because a swarm has exactly one store (see destination argument exists, because a swarm has exactly one store (see
[One subvolume per agent, not per hive](#one-subvolume-per-agent-not-per-hive)), [One subvolume per agent, not per hive](#one-subvolume-per-agent-not-per-hive)),
and no credential argument, because the mesh is the authentication. and no credential argument, because the mesh is the authentication.
## The mesh is the authentication ## The mesh is the authentication
There are no certificates here, and no key material of its own. That's No certificates live here, and no key material of its own. That's
deliberate rather than an omission. deliberate rather than an omission.
WireGuard's cryptokey routing already binds a peer's source address to WireGuard's cryptokey routing already binds a peer's source address to
@ -106,7 +106,7 @@ the case the store exists to serve.
## What the sender can and can't choose ## What the sender can and can't choose
A `btrfs send` stream carries no notion of _which agent_ it belongs to, A `btrfs send` stream carries no notion of _which agent_ it belongs to,
and the subvolume name inside it's chosen by the sender. So the and the subvolume name inside it's chosen by the sender. The
protocol is one `agent <name>` header line, then the raw stream. protocol is one `agent <name>` header line, then the raw stream.
The rule that matters: The rule that matters:

View file

@ -53,7 +53,7 @@ peer's name when the caller knows it's hive-local.
## Identity = socket ## Identity = socket
There are no auth tokens on the per-agent unix sockets. The socket No auth tokens exist on the per-agent unix sockets. The socket
*path* identifies the principal; perms come from "who has the *path* identifies the principal; perms come from "who has the
bind-mount." A sub-agent only sees its own `/run/hive/mcp.sock`; bind-mount." A sub-agent only sees its own `/run/hive/mcp.sock`;
hive-c0re owns the host admin socket. hive-c0re owns the host admin socket.
@ -490,7 +490,7 @@ check derivations they don't:
- **`hivectl-docs`** regenerates `docs/tools/hivectl-cli.md` from - **`hivectl-docs`** regenerates `docs/tools/hivectl-cli.md` from
hivectl's clap tree and **fails if the committed copy is stale**. hivectl's clap tree and **fails if the committed copy is stale**.
So **after any change to a hivectl verb or flag, regenerate it**: **After any change to a hivectl verb or flag, regenerate it**:
```sh ```sh
nix develop -c cargo run --bin hivectl -- markdown-docs > docs/tools/hivectl-cli.md nix develop -c cargo run --bin hivectl -- markdown-docs > docs/tools/hivectl-cli.md
``` ```

View file

@ -1,7 +1,7 @@
# Gotchas # Gotchas
NixOS + nspawn quirks and lessons we hit the hard way. If something NixOS + nspawn quirks and lessons we hit the hard way. If something
here looks unmotivated in the code, there's usually a story underneath. here looks unmotivated in the code, there's probably a story underneath.
Grouped by area — jump to the section that matches what you're Grouped by area — jump to the section that matches what you're
touching. touching.
@ -64,7 +64,7 @@ path = [ "/run/wrappers" "/run/current-system/sw" ];
``` ```
The bug is silent: `nix eval` succeeds, the unit starts, but PATH The bug is silent: `nix eval` succeeds, the unit starts, but PATH
contains a non-existent directory. The first symptom is usually contains a non-existent directory. The first symptom is typically
`sudo: must be owned by uid 0 and have the setuid bit set` because `sudo: must be owned by uid 0 and have the setuid bit set` because
the setuid sudo wrapper lives at `/run/wrappers/bin/sudo` and the setuid sudo wrapper lives at `/run/wrappers/bin/sudo` and
the path entry resolves to `/run/wrappers/bin/bin` instead. the path entry resolves to `/run/wrappers/bin/bin` instead.
@ -144,7 +144,7 @@ store-path _string_ coerces through `lib.toDerivation`, i.e.
`builtins.storePath`. `environment.systemPackages` and `builtins.storePath`. `environment.systemPackages` and
`systemd.services.<name>.path` both do it (the latter takes plain `systemd.services.<name>.path` both do it (the latter takes plain
strings like `/run/wrappers` happily, but anything under strings like `/run/wrappers` happily, but anything under
`builtins.storeDir` is treated as a package). So a path handed to the `builtins.storeDir` is treated as a package). A path handed to the
container as text has to be wrapped in a real derivation — a symlink container as text has to be wrapped in a real derivation — a symlink
farm built from the interpolated string — before it can go anywhere a farm built from the interpolated string — before it can go anywhere a
package is expected. package is expected.

View file

@ -85,7 +85,7 @@ Cheap — no build slot:
| `KnowledgePull` | one-shot boot-time `/knowledge` pull (`knowledge::pull`), reconciling commits that landed while `hive-c0re` was down. Same rationale as `MatrixSweep`: the periodic hourly re-pull stays a background loop | | `KnowledgePull` | one-shot boot-time `/knowledge` pull (`knowledge::pull`), reconciling commits that landed while `hive-c0re` was down. Same rationale as `MatrixSweep`: the periodic hourly re-pull stays a background loop |
| `WantedPull` | one-shot boot-time pull of the agent set the swarm controller declares for this hive (`wanted::pull`), converging the agents it names. No background loop behind this one — boot is the whole cadence; the deploy event (`swarm_status`) is the fast path, this repairs a missed one. Agentless | | `WantedPull` | one-shot boot-time pull of the agent set the swarm controller declares for this hive (`wanted::pull`), converging the agents it names. No background loop behind this one — boot is the whole cadence; the deploy event (`swarm_status`) is the fast path, this repairs a missed one. Agentless |
There is deliberately **no `GitCommit` node**: `meta.rs` fuses each mutation Deliberately, **no `GitCommit` node** exists: `meta.rs` fuses each mutation
with its commit under its internal `META_LOCK` mutex, so a standalone commit with its commit under its internal `META_LOCK` mutex, so a standalone commit
node would open a dirty-working-tree window between nodes. node would open a dirty-working-tree window between nodes.
@ -217,7 +217,7 @@ resources are free. Resources:
`Reconcile`, `WriteDropin`, `Create`) or at a **brace** (`AgentWindow`, `Reconcile`, `WriteDropin`, `Create`) or at a **brace** (`AgentWindow`,
`DeployWindow`) on behalf of a whole coordinated subtree; held by the owning `DeployWindow`) on behalf of a whole coordinated subtree; held by the owning
DAG until it's terminal, so two DAGs never interleave container ops on the DAG until it's terminal, so two DAGs never interleave container ops on the
same agent. A DAG touching several agents holds one lease per agent. same agent. A DAG touching multiple agents holds one lease per agent.
(`SetWanted` is a store write, not a container op, but takes the lease anyway (`SetWanted` is a store write, not a container op, but takes the lease anyway
so a power-op DAG's intent write + reconcile is atomic — two racing ops can't so a power-op DAG's intent write + reconcile is atomic — two racing ops can't
clobber intent before either reconciles.) **Lease-exempt**: `MetaSync`, clobber intent before either reconciles.) **Lease-exempt**: `MetaSync`,
@ -333,7 +333,7 @@ string) and free-form `data` for what only some kinds have (`agent`,
`approval_id`, `inputs`, `build_log_id`). Group roots ride as ordinary `approval_id`, `inputs`, `build_log_id`). Group roots ride as ordinary
nodes, so a group's state is just the root's own `state`. nodes, so a group's state is just the root's own `state`.
There is **no group-level `agent`** — agent is per-node, so one group can No group-level `agent` exists — agent is per-node, so one group can
span agents; consumers derive a group's agents from its nodes. Build logs span agents; consumers derive a group's agents from its nodes. Build logs
are likewise **per-node**: the dashboard renders the node tree and keys the are likewise **per-node**: the dashboard renders the node tree and keys the
live-log panel off the running node. live-log panel off the running node.
@ -497,7 +497,7 @@ limits, build parallelism, and first-spawn latency.
`buildSlots` (default `1`) sets how many nix-heavy job-queue nodes `buildSlots` (default `1`) sets how many nix-heavy job-queue nodes
(prebuilds, profile swaps, first-spawn creates, meta lock bumps) run (prebuilds, profile swaps, first-spawn creates, meta lock bumps) run
concurrently. The default serializes all heavy nix work like the pre-DAG concurrently. The default serializes all heavy nix work like the pre-DAG
rebuild queue did; raise it on hosts with the cores/RAM to build several rebuild queue did; raise it on hosts with the cores/RAM to build multiple
agent toplevels at once. Per-agent correctness is independent of the count — agent toplevels at once. Per-agent correctness is independent of the count —
each agent's container-affecting ops serialize on its lifecycle lease each agent's container-affecting ops serialize on its lifecycle lease
regardless. regardless.
@ -513,7 +513,7 @@ requiring a host rebuild.
The same drop-in carries `CPUWeight=` / `IOWeight=` from The same drop-in carries `CPUWeight=` / `IOWeight=` from
`agentCpuWeight` / `agentIoWeight`. Those are a different kind of `agentCpuWeight` / `agentIoWeight`. Those are a different kind of
setting: the quota and the memory max are **hard caps** that throttle setting: the quota and the memory max are **hard caps** that throttle
an agent even on a completely idle host, while the weights are cgroup an agent even on an idle host, while the weights are cgroup
v2 **relative shares** that only decide who yields _under contention_. v2 **relative shares** that only decide who yields _under contention_.
A low-weight container still gets the whole machine when nothing else A low-weight container still gets the whole machine when nothing else
wants it. wants it.
@ -560,7 +560,7 @@ trade-off:
binary are added to the host system closure (low single-digit GB binary are added to the host system closure (low single-digit GB
additional). additional).
- **Off** (default): the host closure stays lean; the first spawn - **Off** (default): the host closure stays lean; the first spawn
does all the eval + fetch work at runtime (can take several does all the eval + fetch work at runtime (can take a few
minutes on a fresh store). minutes on a fresh store).
**Note**: toplevels are pinned to `x86_64-linux`. Enabling on an **Note**: toplevels are pinned to `x86_64-linux`. Enabling on an

View file

@ -5,7 +5,7 @@ token usage, cost, tool call counts — to any OTLP-compatible collector via
Claude Code's built-in OpenTelemetry integration. Claude Code's built-in OpenTelemetry integration.
This is a **hive-wide** setting: one switch in the host NixOS config enables it This is a **hive-wide** setting: one switch in the host NixOS config enables it
for every agent container simultaneously. There is no per-agent opt-in or opt-out. for every agent container simultaneously. No per-agent opt-in or opt-out exists.
## Enabling export ## Enabling export
@ -22,7 +22,7 @@ leaves the swarm — optional, because the swarm's own metrics store
telemetry goes to both. See telemetry goes to both. See
[`swarm/services.md`](../swarm/services.md#metrics-victoriametrics--grafana). [`swarm/services.md`](../swarm/services.md#metrics-victoriametrics--grafana).
**There is exactly one way telemetry leaves a hive: through the collector that **Telemetry leaves a hive exactly one way: through the collector that
`enable` starts on the host.** Agents never talk to `endpoint` themselves — `enable` starts on the host.** Agents never talk to `endpoint` themselves —
they export unauthenticated to a bridge address only their own containers can they export unauthenticated to a bridge address only their own containers can
reach. That collector forwards to the swarm's reach. That collector forwards to the swarm's
@ -45,7 +45,7 @@ no longer survives independently of anything host-side.
**It has no application-level auth.** The receiver takes any OTLP that reaches **It has no application-level auth.** The receiver takes any OTLP that reaches
it; what bounds who can reach it's the firewall — `exposeHostPorts` opens the it; what bounds who can reach it's the firewall — `exposeHostPorts` opens the
port on the bridge interface only. So "unauthenticated to a bridge address" port on the bridge interface only "unauthenticated to a bridge address"
means _reachable from an agent container_, not _presents a credential_. means _reachable from an agent container_, not _presents a credential_.
The consequence, stated because it's a choice rather than an oversight: **any The consequence, stated because it's a choice rather than an oversight: **any
@ -76,7 +76,7 @@ is getting a credential:
`hive=` from the connection it authenticated, so _that_ label becomes `hive=` from the connection it authenticated, so _that_ label becomes
unforgeable. unforgeable.
So a verified `hive` is reachable and a verified `agent` isn't — and that falls A verified `hive` is reachable and a verified `agent` isn't — and that falls
out of the topology rather than being a gap someone forgot to close. The swarm out of the topology rather than being a gap someone forgot to close. The swarm
runs one collector, and the mechanism gives it no finer grain: a bearer-token runs one collector, and the mechanism gives it no finer grain: a bearer-token
check never reveals _which_ token matched, and a receiver reads request metadata check never reveals _which_ token matched, and a receiver reads request metadata
@ -150,7 +150,7 @@ somehow — copy it across and name it:
services.hyperhive.otel.clientSecretFile = "/run/secrets/hive-telemetry.secret"; services.hyperhive.otel.clientSecretFile = "/run/secrets/hive-telemetry.secret";
``` ```
**There is no unauthenticated mode.** A hive always presents an identity, so a **No unauthenticated mode exists.** A hive always presents an identity, so a
missing credential is a build error rather than a quieter fallback — the missing credential is a build error rather than a quieter fallback — the
collector has no anonymous route to accept samples on, and every path it serves collector has no anonymous route to accept samples on, and every path it serves
belongs to exactly one hive. belongs to exactly one hive.

View file

@ -49,7 +49,7 @@ both once.**
`domain` is required too, but you no longer _write_ it: it's read from `domain` is required too, but you no longer _write_ it: it's read from
this hive's own entry in the directory, whose `domain` defaults to this hive's own entry in the directory, whose `domain` defaults to
`<name>.<swarm.domain>`. So a conventional swarm states no addresses at `<name>.<swarm.domain>`. A conventional swarm states no addresses at
all, and a hive addressed by something else states it in the one place all, and a hive addressed by something else states it in the one place
the other hives read — `swarm.hives.edge.domain = "edge.elsewhere.example";`. the other hives read — `swarm.hives.edge.domain = "edge.elsewhere.example";`.
@ -134,7 +134,7 @@ contain you derives every hive as a peer and you peer with yourself.
conventionally `<name>.<swarm.domain>`, but a wrong domain that conventionally `<name>.<swarm.domain>`, but a wrong domain that
evaluates cleanly points at a real machine that isn't the one you meant. evaluates cleanly points at a real machine that isn't the one you meant.
> **There is no per-hive CA field, and no per-hive cert pinning.** Trust > **No per-hive CA field exists, and no per-hive cert pinning.** Trust
> inside a swarm comes from the swarm root ([`ca.md`](ca.md)): every > inside a swarm comes from the swarm root ([`ca.md`](ca.md)): every
> hive chains to it, so one anchor replaces per-hive pinning entirely. > hive chains to it, so one anchor replaces per-hive pinning entirely.
> What that genuinely drops is trusting a hive whose root this swarm > What that genuinely drops is trusting a hive whose root this swarm

View file

@ -48,7 +48,7 @@ swarm-services leaf can, and this host signs that one only under
`autoConfigure`. A hive with neither serves the hive leaf on those names `autoConfigure`. A hive with neither serves the hive leaf on those names
and every client sees a name mismatch. and every client sees a name mismatch.
So `hive-tls` emits an eval-time **warning** naming the uncovered names. `hive-tls` emits an eval-time **warning** naming the uncovered names.
It's deliberately not an assertion: this module can see what _it_ is It's deliberately not an assertion: this module can see what _it_ is
able to issue, but not an operator-installed sub-CA in able to issue, but not an operator-installed sub-CA in
`swarm.ca.stateDir`, an external ACME setup, or a certificate delivered `swarm.ca.stateDir`, an external ACME setup, or a certificate delivered

View file

@ -17,7 +17,7 @@ The rule is about what must stay secret, not about credentials generally.
`deploy.nats.calloutUserPublicKey`, is published to every client that connects, `deploy.nats.calloutUserPublicKey`, is published to every client that connects,
so the store is a perfectly good place for it. so the store is a perfectly good place for it.
**The generator and the reader are usually in different containers.** They share **The generator and the reader typically live in different containers.** They share
the host's network namespace, which makes them feel co-located, but their the host's network namespace, which makes them feel co-located, but their
filesystem roots are separate. That's why delivery is a **host-side copy rather filesystem roots are separate. That's why delivery is a **host-side copy rather
than a bind mount**: `nixos-container` refuses to start when a bind source is than a bind mount**: `nixos-container` refuses to start when a bind source is
@ -30,7 +30,7 @@ container that starts after it.
Every row below is read against one of these. This is a different cut Every row below is read against one of these. This is a different cut
from the [deployment shapes](services.md#deployment-shapes) --- those from the [deployment shapes](services.md#deployment-shapes) --- those
say _where services run_, these say _who is responsible for a secret say _where services run_, these say _who is responsible for a secret
file being there_ --- so the two lists don't line up one-to-one, and file being there_ --- the two lists don't line up one-to-one, and
neither is a renaming of the other. neither is a renaming of the other.
| topology | what it means | who places secrets | | topology | what it means | who places secrets |
@ -63,7 +63,7 @@ because nothing outside that container ever reads them. **That's the test worth
applying to any secret added here** — and the client secret's plaintext half is applying to any secret added here** — and the client secret's plaintext half is
the one row that fails it, which is the entire reason a delivery step exists. the one row that fails it, which is the entire reason a delivery step exists.
There are two telemetry collectors and they land on opposite sides of that test. Two telemetry collectors exist, and they land on opposite sides of that test.
The **hive's** collector needs no delivery step. It authenticates to the swarm's The **hive's** collector needs no delivery step. It authenticates to the swarm's
collector as its own hive, and it's a host unit rather than a container, so on collector as its own hive, and it's a host unit rather than a container, so on
@ -136,7 +136,7 @@ when the system builds. The server names the offending file and refuses to run.
Both delivery units wait for authelia's first boot to mint the secret — a Both delivery units wait for authelia's first boot to mint the secret — a
bounded wait, 120s — and then **fail loudly** rather than skipping. A silent skip bounded wait, 120s — and then **fail loudly** rather than skipping. A silent skip
produces a service whose login button always fails, which is a symptom several produces a service whose login button always fails, which is a symptom many
layers from its cause. layers from its cause.
The store's **first reader** is the matrix registration token, and it's worth The store's **first reader** is the matrix registration token, and it's worth
@ -170,7 +170,7 @@ everything else a hive needs does.
A hive CA carries `nameConstraints=permitted;DNS:<hive domain>`, and **a swarm A hive CA carries `nameConstraints=permitted;DNS:<hive domain>`, and **a swarm
service name is a sibling of the hive domain rather than a child** — `forge.<swarm>` service name is a sibling of the hive domain rather than a child** — `forge.<swarm>`
next to `<hive>.<swarm>`. So a hive CA can't issue a certificate for a swarm next to `<hive>.<swarm>`. A hive CA can't issue a certificate for a swarm
service. Not by policy: by construction, and openssl enforces it. service. Not by policy: by construction, and openssl enforces it.
Whatever holds the swarm root is therefore what makes swarm-service certificates Whatever holds the swarm root is therefore what makes swarm-service certificates

View file

@ -143,11 +143,11 @@ Drilldown_ app is deliberately not installed: it only supports Loki, and
no setting here changes that, so Explore is the log browser for this no setting here changes that, so Explore is the log browser for this
swarm. swarm.
| Option | When you'd touch it | | Option | When you'd touch it |
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `deploy.victorialogs.retentionPeriod` | Default `30d`, far shorter than the metrics store's — logs are bulkier per unit of value and are usually read within days of being written. Raise it if you need to answer questions about last quarter. | | `deploy.victorialogs.retentionPeriod` | Default `30d`, far shorter than the metrics store's — logs are bulkier per unit of value and are typically read within days of being written. Raise it if you need to answer questions about last quarter. |
| `swarm.victorialogs.domain` | Only to rename it. | | `swarm.victorialogs.domain` | Only to rename it. |
| `swarm.victorialogs.port` | Only if something else on the services host already claims `9428`. | | `swarm.victorialogs.port` | Only if something else on the services host already claims `9428`. |
Like the metrics store, it binds loopback and takes no credential of its Like the metrics store, it binds loopback and takes no credential of its
own: the gateway vhost is the only way in, and the collector is the only own: the gateway vhost is the only way in, and the collector is the only
@ -169,7 +169,7 @@ port is a coin toss at runtime rather than an error at build time.
Every hive's own collector reaches this one by its gateway name, Every hive's own collector reaches this one by its gateway name,
`swarm.otel.domain` (default `otel.<swarm domain>`) — the same `swarm.otel.domain` (default `otel.<swarm domain>`) — the same
by-domain-through-the-gateway shape every other swarm service uses, not a by-domain-through-the-gateway shape every other swarm service uses, not a
loopback URL an operator has to redirect. There is nothing to set on a hive loopback URL an operator has to redirect. Nothing needs setting on a hive
that doesn't run the swarm's services; the name resolves through the that doesn't run the swarm's services; the name resolves through the
gateway either way. gateway either way.

View file

@ -33,7 +33,7 @@ enabled but not bootstrapped shows a **crash-looping unit** and `502 Bad
Gateway` from the vhost — not a login page with nobody able to use it. Gateway` from the vhost — not a login page with nobody able to use it.
The gateway is working in that state; the upstream isn't up. The gateway is working in that state; the upstream isn't up.
⚠️ So the step below is **required to finish the install**, not an ⚠️ The step below is **required to finish the install**, not an
optional first-login convenience. Run it before concluding anything is optional first-login convenience. Run it before concluding anything is
wrong with the proxy: a 502 here means "no users yet" far more often wrong with the proxy: a 502 here means "no users yet" far more often
than it means a routing fault. than it means a routing fault.
@ -67,7 +67,7 @@ operator typing one command once.
`user add` only ever adds: on a name that already exists it refuses, `user add` only ever adds: on a name that already exists it refuses,
rather than resurfacing as a second account or a silent overwrite. rather than resurfacing as a second account or a silent overwrite.
Editing an existing subject is `user update`, and the flags compose, so Editing an existing subject is `user update`, and the flags compose, so
one call can change several things: one call can change multiple things:
```console ```console
# swarmctl user update mara --add-group admins --email mara@example.com # swarmctl user update mara --add-group admins --email mara@example.com
@ -99,7 +99,7 @@ Every secret in the swarm, with its generator and its path, is tabulated
in one place: [`secrets.md`](secrets.md), including authelia's own keys in one place: [`secrets.md`](secrets.md), including authelia's own keys
(session, JWT, storage-encryption, OIDC HMAC, OIDC issuer) and the two (session, JWT, storage-encryption, OIDC HMAC, OIDC issuer) and the two
halves of each client secret. That page's two rules — a secret is always halves of each client secret. That page's two rules — a secret is always
a path, never a value, and the generator and the reader are usually in a path, never a value, and the generator and the reader typically live in
different containers — are why the client secret's plaintext half needs different containers — are why the client secret's plaintext half needs
the delivery step below and the rest of authelia's keys don't. the delivery step below and the rest of authelia's keys don't.
@ -124,7 +124,7 @@ nixos-container refuses to start a container whose bind source is
missing, and this secret doesn't exist until authelia's first boot has missing, and this secret doesn't exist until authelia's first boot has
run. Binding it would make the service wait on a file that waits on a run. Binding it would make the service wait on a file that waits on a
container that starts after it — on a fresh hive, a permanent stall container that starts after it — on a fresh hive, a permanent stall
presenting as "the forge is broken," several layers from its cause. presenting as "the forge is broken," many layers from its cause.
### 2. Swarm-managed services ### 2. Swarm-managed services
@ -149,7 +149,7 @@ services.hyperhive.swarm = {
**Both are asserted at eval.** A hive that boots with SSO **Both are asserted at eval.** A hive that boots with SSO
half-configured shows a login button that always fails — a symptom half-configured shows a login button that always fails — a symptom
several layers from its cause, and far worse to diagnose than an many layers from its cause, and far worse to diagnose than an
evaluation error. evaluation error.
## Where each relying party differs ## Where each relying party differs
@ -218,7 +218,7 @@ HTML instead of the failure that actually happened:
- `/api/oidc/token`, `/.well-known/openid-configuration` — a client - `/api/oidc/token`, `/.well-known/openid-configuration` — a client
parsing an error page as its JSON document parsing an error page as its JSON document
So authelia's `/api/` and `/.well-known/` prefixes are routed **without** Authelia's `/api/` and `/.well-known/` prefixes are routed **without**
error interception. The split is by _audience_, not by an enumerated path error interception. The split is by _audience_, not by an enumerated path
list: a human gets the page, every machine caller gets the status. list: a human gets the page, every machine caller gets the status.
Enumerating endpoints individually would leave the next one added Enumerating endpoints individually would leave the next one added

View file

@ -84,8 +84,8 @@ Exposed as `mcp__bash__kill`.
## Namespace note ## Namespace note
`run` and `status` live in the `bash` MCP server, not `hyperhive`. So `run` and `status` live in the `bash` MCP server, not `hyperhive`. The
the tool names in claude are `mcp__bash__run` and `mcp__bash__status`. tool names in claude are `mcp__bash__run` and `mcp__bash__status`.
The `Bash` built-in tool is blocked — all shell execution goes through The `Bash` built-in tool is blocked — all shell execution goes through
this structured path so tasks get task-id tracking and structured output. this structured path so tasks get task-id tracking and structured output.

View file

@ -1003,7 +1003,7 @@ Add a collaborator to the active repo (`-r`/`HIVE_FORGE_REPO`) with a permission
###### **Options:** ###### **Options:**
* `--permission <PERMISSION>` — Permission level to grant (default: write — a freshly added collaborator usually needs to push) * `--permission <PERMISSION>` — Permission level to grant (default: write — a freshly added collaborator typically needs to push)
Default value: `write` Default value: `write`

View file

@ -134,7 +134,7 @@ hive-forge repo-labels --json # full label ob
### Which read verb when ### Which read verb when
Several verbs read an issue/PR and look overlapping, but each has a Multiple 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 |

View file

@ -643,7 +643,7 @@ Stream a snapshot to the swarm's snapshot store over the WireGuard mesh — the
Nothing is staged locally: `btrfs send` writes straight into the connection, so a multi-gigabyte agent needs no scratch space on this host. The mesh is the authentication (cryptokey routing binds the sender's address to its key), so there is no credential to pass here. Nothing is staged locally: `btrfs send` writes straight into the connection, so a multi-gigabyte agent needs no scratch space on this host. The mesh is the authentication (cryptokey routing binds the sender's address to its key), so there is no credential to pass here.
There is no destination argument: a swarm has one store, read from `services.hyperhive.swarm.snapshotStore`. No destination argument exists: a swarm has one store, read from `services.hyperhive.swarm.snapshotStore`.
**Usage:** `hivectl agent subvol snapshot push [OPTIONS] <LABEL>` **Usage:** `hivectl agent subvol snapshot push [OPTIONS] <LABEL>`

View file

@ -308,7 +308,7 @@ The URL is resolved from the running daemon (`HostRequest::Urls`), which
reads the per-surface public URLs from c0re's service env — so custom reads the per-surface public URLs from c0re's service env — so custom
forge / matrix domains resolve correctly instead of assuming forge / matrix domains resolve correctly instead of assuming
`forge.<domain>`. The URL is **always printed** (the reliable core, since `forge.<domain>`. The URL is **always printed** (the reliable core, since
the host is usually headless / driven over SSH), then `xdg-open` is tried the host is typically headless / driven over SSH), then `xdg-open` is tried
as a convenience — a missing or failing opener is reported as a note, not as a convenience — a missing or failing opener is reported as a note, not
an error. an error.

View file

@ -67,7 +67,7 @@ Add a user, generating a password for them
Change an existing user's attributes. Change an existing user's attributes.
Every flag is optional and they compose, so one call can set several things at once. Deliberately does **not** touch the password: regenerating a credential is a different intent from editing an attribute, and folded together an attribute edit can invalidate a login by accident. Every flag is optional and they compose, so one call can set multiple things at once. Deliberately does **not** touch the password: regenerating a credential is a different intent from editing an attribute, and folded together an attribute edit can invalidate a login by accident.
**Usage:** `swarmctl user update [OPTIONS] <USERNAME>` **Usage:** `swarmctl user update [OPTIONS] <USERNAME>`

View file

@ -87,7 +87,7 @@ systemd unit. The unit binds `/run/hive/priv.sock` with
to the helper as fd 3 (`LISTEN_FDS`). The helper requires this and to the helper as fd 3 (`LISTEN_FDS`). The helper requires this and
bails if it isn't socket-activated. bails if it isn't socket-activated.
⚠️ There is intentionally no self-bind fallback: if `hive-priv` bound ⚠️ Intentionally, no self-bind fallback exists: if `hive-priv` bound
the socket itself, it would create the file owned by root's primary the socket itself, it would create the file owned by root's primary
group rather than `hive-core`, and a `hive-core` client couldn't group rather than `hive-core`, and a `hive-core` client couldn't
connect the way the socket unit's `SocketGroup` grant intends. connect the way the socket unit's `SocketGroup` grant intends.

View file

@ -34,7 +34,7 @@ Each agent gets its own scoped credentials, never shared:
collaborator grants; can't act as another agent or as `core`). collaborator grants; can't act as another agent or as `core`).
- **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 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."

View file

@ -141,8 +141,8 @@ window from `HIVE_TURN_IDLE_SECS` (`0` disables) and maps the driver's
After the outcome handler, the stats sink records a row. `handle_turn` After the outcome handler, the stats sink records a row. `handle_turn`
reports the result to `serve_loop` via `TurnControl { auth_failed }` reports the result to `serve_loop` via `TurnControl { auth_failed }`
on auth failure the loop parks in `wait_for_login`; otherwise it loops on auth failure the loop parks in `wait_for_login`; otherwise it loops
straight back to the idle wait (step 1). There is no same-turn straight back to the idle wait (step 1). No same-turn
self-continue mechanism: every multi-step continuation rides an self-continue mechanism exists: every multi-step continuation rides an
external wake instead — a new inbox message, a `remind`, or an external wake instead — a new inbox message, a `remind`, or an
in-container todo wake (bash-task completion, forge notification, in-container todo wake (bash-task completion, forge notification,
matrix activity). Ending the turn and letting one of matrix activity). Ending the turn and letting one of

View file

@ -25,7 +25,7 @@ to the bus.
**Which `claude` binary.** The bare name `claude`, resolved off the **Which `claude` binary.** The bare name `claude`, resolved off the
harness unit's PATH. By default that's the `claude-code` in the agent's harness unit's PATH. By default that's the `claude-code` in the agent's
own nixpkgs (the meta flake's `nixpkgs` input) via own nixpkgs (the meta flake's `nixpkgs` input) via
`environment.systemPackages`. Since that's usually a release channel and `environment.systemPackages`. Since that's typically a release channel and
this package moves fast, the operator can pin one hive-wide with this package moves fast, the operator can pin one hive-wide with
`services.hyperhive.c0re.claudeCodePackage`: its store path is written `services.hyperhive.c0re.claudeCodePackage`: its store path is written
into each agent's flake, and `claude` on PATH becomes a symlink to it into each agent's flake, and `claude` on PATH becomes a symlink to it
@ -117,7 +117,7 @@ into its `run`:
_still_ overflows, `run` surfaces `Error::PromptTooLong`; `drive_turn` _still_ overflows, `run` surfaces `Error::PromptTooLong`; `drive_turn`
then archives the session (session lifecycle stays hive-side) and the then archives the session (session lifecycle stays hive-side) and the
serve loop requeues the message so it redelivers into a fresh session serve loop requeues the message so it redelivers into a fresh session
(see [Turn outcomes](README.md#turn-outcomes) — the wake prompt itself is tiny, so (see [Turn outcomes](README.md#turn-outcomes) — the wake prompt itself is small, so
the overflow was the accumulated context the archive clears). the overflow was the accumulated context the archive clears).
- **Proactive** — a turn finishes cleanly but the last inference's context - **Proactive** — a turn finishes cleanly but the last inference's context
size crossed the policy watermark. While the session is still healthy it size crossed the policy watermark. While the session is still healthy it
@ -185,7 +185,7 @@ still applies.
exceeds the assumed prompt-cache TTL (`HIVE_CACHE_TTL_SECS`, default exceeds the assumed prompt-cache TTL (`HIVE_CACHE_TTL_SECS`, default
`3600`). Claude's prompt cache goes cold after a while; once it's cold, `3600`). Claude's prompt cache goes cold after a while; once it's cold,
`--resume`-ing a large session pays the full re-upload cost with no `--resume`-ing a large session pays the full re-upload cost with no
benefit over starting fresh. So `drive_turn` **archives** the current benefit over starting fresh. `drive_turn` **archives** the current
session (same mechanism as the operator reset — rename `<uuid>.jsonl` session (same mechanism as the operator reset — rename `<uuid>.jsonl`
`.archived`) so the next turn's `--resume <title>` misses and starts `.archived`) so the next turn's `--resume <title>` misses and starts
fresh. Unlike proactive compaction the session is dropped entirely, not fresh. Unlike proactive compaction the session is dropped entirely, not

View file

@ -46,7 +46,7 @@ preset (`AGENT_DEFAULT`) includes `messaging`, `meta`, `inbox`, and
The graceful-stop turn takes the same post-turn compaction path as any The graceful-stop turn takes the same post-turn compaction path as any
other turn: if the context crossed the watermark the harness runs a other turn: if the context crossed the watermark the harness runs a
notes-checkpoint turn and then `/compact`. Compacting before shutdown notes-checkpoint turn and then `/compact`. Compacting before shutdown
keeps a later cold start cheap instead of re-uploading a huge transcript. keeps a later cold start cheap instead of re-uploading a large transcript.
- `ack_until(up_to)` — bulk-mark inbox rows handled: every row with - `ack_until(up_to)` — bulk-mark inbox rows handled: every row with
broker id `<= up_to` is stamped as acked in a single UPDATE. broker id `<= up_to` is stamped as acked in a single UPDATE.
Recipient-scoped (agents can only ack their own rows). Use when a Recipient-scoped (agents can only ack their own rows). Use when a
@ -93,7 +93,7 @@ at_unix_timestamp?)`.
payloads spill to `/agents/<self>/state/reminders/`. Pending count payloads spill to `/agents/<self>/state/reminders/`. Pending count
capped at 50 per agent (`HIVE_REMIND_MAX_PENDING_PER_AGENT`). capped at 50 per agent (`HIVE_REMIND_MAX_PENDING_PER_AGENT`).
There is no same-turn self-continue tool — see No same-turn self-continue tool exists — see
[Turn outcomes](README.md#turn-outcomes) for why. Multi-step work rides [Turn outcomes](README.md#turn-outcomes) for why. Multi-step work rides
`remind` for a durable self-wake, or an in-container todo wake `remind` for a durable self-wake, or an in-container todo wake
(bash-task completion, forge notification, matrix activity) for work (bash-task completion, forge notification, matrix activity) for work

View file

@ -46,7 +46,7 @@ structurally rather than for one specific trigger. Two columns:
- **Pills cluster** (`.agent-header-pills`, right-aligned): status - **Pills cluster** (`.agent-header-pills`, right-aligned): status
badges (`StatusChips.tsx`) and flyout triggers together in one row — badges (`StatusChips.tsx`) and flyout triggers together in one row —
a deliberate choice (not the historical default) so the header reads a deliberate choice (not the historical default) so the header reads
as one identity zone + one status/actions zone rather than several as one identity zone + one status/actions zone rather than multiple
separate clusters: separate clusters:
- **Alive badge**: `● alive` (green) / `⊘ rate limited` (red) / - **Alive badge**: `● alive` (green) / `⊘ rate limited` (red) /
`◌ needs login` / `◌ logging in` / `○ offline` / `… connecting`. `◌ needs login` / `◌ logging in` / `○ offline` / `… connecting`.
@ -83,7 +83,7 @@ structurally rather than for one specific trigger. Two columns:
(`GET /api/state`'s `links` field) also feeds (`GET /api/state`'s `links` field) also feeds
`DashboardState.links` for the dashboard card's icon strip — `DashboardState.links` for the dashboard card's icon strip —
`agent_links()` in hive-agent is the single source of truth for `agent_links()` in hive-agent is the single source of truth for
both. There is no separate overflow (`⋯`) menu any more — it used both. No separate overflow (`⋯`) menu exists any more — it used
to hold exactly this dashboard link plus a rebuild-container action to hold exactly this dashboard link plus a rebuild-container action
(mara, hyperhive#3704: "remove rebuild button, move link to (mara, hyperhive#3704: "remove rebuild button, move link to
dashboards into links menu") — rebuild had no real discoverability dashboards into links menu") — rebuild had no real discoverability

View file

@ -141,7 +141,7 @@ dirs; `POST /api/purge-tombstone/{name}`).
⚠️ **Not only *destroyed* agents.** Nothing records a destroy: every ⚠️ **Not only *destroyed* agents.** Nothing records a destroy: every
definition-side artifact (state subvolume, proposed + applied repos, definition-side artifact (state subvolume, proposed + applied repos,
meta registration, topology entry) is written by `Provision` *before* meta registration, topology entry) is written by `Provision` *before*
the container exists and survives `lifecycle::destroy`. So an agent the container exists and survives `lifecycle::destroy`. An agent
part-way through a spawn is byte-identical on disk to a tombstone, and part-way through a spawn is byte-identical on disk to a tombstone, and
both are listed. The pane carries a standing warning to that effect, and both are listed. The pane carries a standing warning to that effect, and
the row badge says `offline` rather than `destroyed` — the absence of a the row badge says `offline` rather than `destroyed` — the absence of a
@ -474,8 +474,8 @@ Both tables share the same visual shape: `.cap-table-wrap` /
and its checkbox cells, where each checkbox carries `data-baseline` (its and its checkbox cells, where each checkbox carries `data-baseline` (its
render-time state) and the row carries `data-agent` for dirty-tracking. render-time state) and the row carries `data-agent` for dirty-tracking.
**Saving — one button for the whole page.** There are no per-row save **Saving — one button for the whole page.** No per-row save
buttons. A single page-level `.perm-save-bar` with a **save all (N buttons exist. A single page-level `.perm-save-bar` with a **save all (N
agents)** button sits at the bottom of the pane, enabled only when some agents)** button sits at the bottom of the pane, enabled only when some
checkbox diverges from its baseline. Clicking it diffs every checkbox checkbox diverges from its baseline. Clicking it diffs every checkbox
across *both* matrices and POSTs one batch to `POST /api/permissions` as across *both* matrices and POSTs one batch to `POST /api/permissions` as
@ -826,7 +826,7 @@ limited` (red, while the harness is parked after a 429), `needs
login`, `needs update` — plus **one `◐ pending-state…` pill per login`, `needs update` — plus **one `◐ pending-state…` pill per
active transient** (replaces buttons during operator-initiated active transient** (replaces buttons during operator-initiated
start / stop / restart / rebuild / destroy). An agent can carry start / stop / restart / rebuild / destroy). An agent can carry
**several transients at once** — for example a lease-exempt `prebuild` **multiple transients at once** — for example a lease-exempt `prebuild`
running alongside a `stop_for_update` on the same agent — and each running alongside a `stop_for_update` on the same agent — and each
renders as its own independent badge rather than being collapsed renders as its own independent badge rather than being collapsed
into one label, matching the existing multi-badge convention this into one label, matching the existing multi-badge convention this
@ -1337,8 +1337,8 @@ payload):
every queue mutation (enqueue, state transition, dedup collapse, every queue mutation (enqueue, state transition, dedup collapse,
terminal-history trim). Unlike the snapshot events below it ships terminal-history trim). Unlike the snapshot events below it ships
no state at all: the client re-fetches `GET /api/jobq/graph`, which no state at all: the client re-fetches `GET /api/jobq/graph`, which
is where it reads the queue from cold too. There is no is where it reads the queue from cold too. No
`/api/state.rebuild_queue` — it went with the typed projection. `/api/state.rebuild_queue` exists — it went with the typed projection.
- `schedules_changed` (seq, schedules: `Vec<WireSchedule>`) — - `schedules_changed` (seq, schedules: `Vec<WireSchedule>`) —
full snapshot of all scheduled prompts. Emitted after every full snapshot of all scheduled prompts. Emitted after every
operator mutation via the `/api/schedules` surface (new / operator mutation via the `/api/schedules` surface (new /

View file

@ -16,7 +16,7 @@ repeated here.
- **Frosted / tinted glass** where it fits — translucency over flat - **Frosted / tinted glass** where it fits — translucency over flat
panels, not a hard rule for every surface. panels, not a hard rule for every surface.
- **Playful use of the multiple accent colours**, without tipping into - **Playful use of the multiple accent colours**, without tipping into
visual noise — the base16 palette has several accent slots visual noise — the base16 palette has multiple accent slots
(`--purple`, `--cyan`, `--pink`, …); use more than one where it adds (`--purple`, `--cyan`, `--pink`, …); use more than one where it adds
distinction, not decoration for its own sake. distinction, not decoration for its own sake.
- **Whimsy** — small, delightful touches are welcome (the per-hive - **Whimsy** — small, delightful touches are welcome (the per-hive
@ -110,7 +110,7 @@ UI later. Concretely: full untruncated `detail`, a copy button, no
"something went wrong, try reloading." "something went wrong, try reloading."
This follows from **power-user-first**, one of the standing principles: This follows from **power-user-first**, one of the standing principles:
swarm-ui instances are mostly self-hosted and operators are techies, so swarm-ui instances are typically self-hosted and operators are techies, so
errors should give them what they need to actually diagnose a problem errors should give them what they need to actually diagnose a problem
rather than a friendly wall. Power-user-first doesn't mean rather than a friendly wall. Power-user-first doesn't mean
newcomer-hostile — the UI should still be self-explanatory, warn or ask newcomer-hostile — the UI should still be self-explanatory, warn or ask

View file

@ -51,7 +51,7 @@ pub struct Args {
/// Collaborator's forge login to add. /// Collaborator's forge login to add.
user: String, user: String,
/// Permission level to grant (default: write — a freshly added /// Permission level to grant (default: write — a freshly added
/// collaborator usually needs to push). /// collaborator typically needs to push).
#[arg(long, value_enum, default_value_t = Permission::Write)] #[arg(long, value_enum, default_value_t = Permission::Write)]
permission: Permission, permission: Permission,
} }

View file

@ -668,7 +668,7 @@ pub enum SnapshotCmd {
/// binds the sender's address to its key), so there is no credential /// binds the sender's address to its key), so there is no credential
/// to pass here. /// to pass here.
/// ///
/// There is no destination argument: a swarm has one store, read /// No destination argument exists: a swarm has one store, read
/// from `services.hyperhive.swarm.snapshotStore`. /// from `services.hyperhive.swarm.snapshotStore`.
Push { Push {
/// Snapshot label passed to `subvol snapshot create --label`. /// Snapshot label passed to `subvol snapshot create --label`.

View file

@ -146,7 +146,7 @@ enum UserVerb {
/// Change an existing user's attributes. /// Change an existing user's attributes.
/// ///
/// Every flag is optional and they compose, so one call can set /// Every flag is optional and they compose, so one call can set
/// several things at once. Deliberately does **not** touch the /// multiple things at once. Deliberately does **not** touch the
/// password: regenerating a credential is a different intent from /// password: regenerating a credential is a different intent from
/// editing an attribute, and folded together an attribute edit can /// editing an attribute, and folded together an attribute edit can
/// invalidate a login by accident. /// invalidate a login by accident.