Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

..
205 changed files with 6049 additions and 10075 deletions

View file

@ -1,11 +1,7 @@
# Generated by `<tool> markdown-docs` — do not format. The matching # Auto-generated by `hivectl markdown-docs` — do not format.
# *-docs-fresh check in nix/checks.nix diffs each against fresh binary # The hivectl-docs CI check diffs this against fresh binary output,
# output, so any reformatting fails that check permanently: the # so reformatting it would break that check.
# generator, not the formatter, owns these files' layout. Every CLI
# reference doc belongs here — adding one and forgetting this line is
# what made swarmctl's fail.
docs/tools/hivectl-cli.md docs/tools/hivectl-cli.md
docs/tools/swarmctl-cli.md
# Files with multi-line list-item continuations that prettier strips to col 0. # Files with multi-line list-item continuations that prettier strips to col 0.
# prettier's `proseWrap: "preserve"` prevents prose reflow but not list-item # prettier's `proseWrap: "preserve"` prevents prose reflow but not list-item

View file

@ -33,7 +33,7 @@ claude --name <memorable> --model <cheaper-than-you> --dangerously-skip-permissi
by name for follow-ups. by name for follow-ups.
- **`--model <cheaper-than-you>`** - think about which model the task - **`--model <cheaper-than-you>`** - think about which model the task
actually needs; don't spend a bigger model's tokens than your own on actually needs; don't spend a bigger model's tokens than your own on
mechanical work a cheaper one handles fine. Never use a _bigger_ model mechanical work a cheaper one handles fine. Never use a *bigger* model
than yourself for a sub-agent - if the task needs that much capability, than yourself for a sub-agent - if the task needs that much capability,
it's not the "mechanical batch" case this skill is for. it's not the "mechanical batch" case this skill is for.
- **`--dangerously-skip-permissions`** - required in headless (`-p`) - **`--dangerously-skip-permissions`** - required in headless (`-p`)
@ -68,7 +68,7 @@ work while the sub-agent grinds.
cases: "leave it as-is where genuinely unclear; do not guess." cases: "leave it as-is where genuinely unclear; do not guess."
- **Ask for a report file** - per-item results + anything skipped and - **Ask for a report file** - per-item results + anything skipped and
why, so you can verify without re-deriving. why, so you can verify without re-deriving.
- **Tune on ONE item first**, eyeball the result, fix the prompt, _then_ - **Tune on ONE item first**, eyeball the result, fix the prompt, *then*
turn it loose on the full batch. A prompt bug replicated across 100 turn it loose on the full batch. A prompt bug replicated across 100
items is 100 cleanups. items is 100 cleanups.
@ -98,7 +98,7 @@ running at once instead of handing the whole thing to one.
loop. loop.
- **Mind the container's memory cap before picking N.** Your whole - **Mind the container's memory cap before picking N.** Your whole
container shares one `MemoryMax` (a few GB by default) with every container shares one `MemoryMax` (a few GB by default) with every
subagent you spawn _and_ your own process. A `claude` process plus its subagent you spawn *and* your own process. A `claude` process plus its
MCP servers can hold several hundred MB to ~1GB depending on the task; MCP servers can hold several hundred MB to ~1GB depending on the task;
spawning a dozen at once on a small container doesn't just slow spawning a dozen at once on a small container doesn't just slow
things down, it can OOM the whole container — taking your own things down, it can OOM the whole container — taking your own

View file

@ -15,7 +15,7 @@ steps in the right order.
## 1. Check for a duplicate before filing ## 1. Check for a duplicate before filing
`hive-forge list` has a server-side full-text `--search` flag that covers `hive-forge list` has a server-side full-text `--search` flag that covers
title _and_ body - use it before writing anything: title *and* body - use it before writing anything:
```sh ```sh
hive-forge list --search "keyword or two" --state all hive-forge list --search "keyword or two" --state all
@ -43,8 +43,8 @@ Prefer naming the broken behavior or missing capability over a specific
error string or one-off symptom - "hive-forge list has no way to filter by error string or one-off symptom - "hive-forge list has no way to filter by
milestone" beats "list command confusing" or "can't find issues for milestone" beats "list command confusing" or "can't find issues for
milestone 11 easily". If you're not sure which framing is more durable, milestone 11 easily". If you're not sure which framing is more durable,
lean toward whatever a colleague hitting the _same underlying gap_ through lean toward whatever a colleague hitting the *same underlying gap* through
a _different_ symptom would still recognize. a *different* symptom would still recognize.
## 3. Body: what's wrong, not how to fix it ## 3. Body: what's wrong, not how to fix it

View file

@ -20,7 +20,7 @@ dimension directly.
hardcoded, so check `repo-labels` first if you don't already know the hardcoded, so check `repo-labels` first if you don't already know the
scopes in use. scopes in use.
- **PRs with no formally requested reviewer** - `hive-forge lint - **PRs with no formally requested reviewer** - `hive-forge lint
no-reviewer` flags PRs with zero requested reviewers at all; add no-reviewer` flags PRs with zero requested reviewers at all; add
`--reviewer <name>` to instead flag PRs where that specific person `--reviewer <name>` to instead flag PRs where that specific person
isn't among the requested reviewers. This checks the forge's actual isn't among the requested reviewers. This checks the forge's actual
reviewer-request state, not a text `@name` mention anywhere in the reviewer-request state, not a text `@name` mention anywhere in the
@ -42,13 +42,12 @@ no-reviewer` flags PRs with zero requested reviewers at all; add
Run the checks relevant to what you're verifying rather than assuming Run the checks relevant to what you're verifying rather than assuming
one covers everything - "is triage caught up" usually means unassigned one covers everything - "is triage caught up" usually means unassigned
+ unlabeled at minimum, with no-reviewer added if reviews matter for
- unlabeled at minimum, with no-reviewer added if reviews matter for your workflow. Each verb supports `--json` if you want to fold the
your workflow. Each verb supports `--json` if you want to fold the results into something else instead of reading the human-readable output.
results into something else instead of reading the human-readable output.
This is reactive/on-demand coverage checking, not a scheduled rollup or This is reactive/on-demand coverage checking, not a scheduled rollup or
historical trend - if you need to track compliance _over time_ (was historical trend - if you need to track compliance *over time* (was
triage worse last week than this week), that's a different, bigger triage worse last week than this week), that's a different, bigger
ask than running these checks - don't build that unprompted just ask than running these checks - don't build that unprompted just
because the checks exist. because the checks exist.

View file

@ -6,7 +6,7 @@ description: Take a real screenshot of an HTML/CSS page from inside a container
# Headless Screenshot # Headless Screenshot
"No browser in this container" usually means no browser is "No browser in this container" usually means no browser is
_pre-installed_, not that one is unreachable. `nixpkgs#chromium` is a *pre-installed*, not that one is unreachable. `nixpkgs#chromium` is a
`nix shell` away, and a single headless invocation renders a page and `nix shell` away, and a single headless invocation renders a page and
writes a PNG without a running server, a display, or any scripted writes a PNG without a running server, a display, or any scripted
interaction library. interaction library.
@ -65,7 +65,7 @@ peer agent's GUI-testing capability, try rendering it yourself first:
This is a static render, not a browser automation tool. Verifying an This is a static render, not a browser automation tool. Verifying an
interaction (a click, a hover state, a form submission) needs interaction (a click, a hover state, a form submission) needs
puppeteer/playwright or similar - reach for those only when the puppeteer/playwright or similar - reach for those only when the
question is genuinely about _behavior_, not appearance. A missing question is genuinely about *behavior*, not appearance. A missing
emoji/glyph rendering as a box in the screenshot is usually a font emoji/glyph rendering as a box in the screenshot is usually a font
availability artifact of the headless environment, not a real bug - availability artifact of the headless environment, not a real bug -
don't mistake one for the other when reporting results. don't mistake one for the other when reporting results.
@ -80,7 +80,7 @@ judge icon/glyph layout, not just to note "there'd be an icon here").
- **Tofu boxes instead of emoji.** Plain `nixpkgs#chromium` has no - **Tofu boxes instead of emoji.** Plain `nixpkgs#chromium` has no
color-emoji font, so any 🖥/📡/🔨-style glyph renders as an empty color-emoji font, so any 🖥/📡/🔨-style glyph renders as an empty
box. Add `nixpkgs#noto-fonts-color-emoji` to the `nix shell` - but a box. Add `nixpkgs#noto-fonts-color-emoji` to the `nix shell` - but a
`FONTCONFIG_FILE` that lists _only_ the emoji dir throws away the `FONTCONFIG_FILE` that lists *only* the emoji dir throws away the
base fonts and blanks all normal text instead, which is its own base fonts and blanks all normal text instead, which is its own
false alarm. Merge, don't override: false alarm. Merge, don't override:
``` ```

View file

@ -10,7 +10,7 @@ work in your own style. That framing should hold throughout.
## Before you start ## Before you start
1. **Read the linked issue/task** - understand _why_ the change exists, 1. **Read the linked issue/task** - understand *why* the change exists,
not just what it does. not just what it does.
2. **Check CI status first.** A failing build or test suite is an 2. **Check CI status first.** A failing build or test suite is an
automatic block - don't write a detailed review of code that doesn't automatic block - don't write a detailed review of code that doesn't

View file

@ -28,7 +28,7 @@ requester-as-parent edge written at its `init_config` approval (so
agent, including the bootstrap container (`ruth`) — it's just another agent, including the bootstrap container (`ruth`) — it's just another
root. The manager is reparentable like any other agent; there's no root. The manager is reparentable like any other agent; there's no
"structurally root" carve-out. Its privileges live on its MCP socket, "structurally root" carve-out. Its privileges live on its MCP socket,
not its tree position (see _Manager special-casing today_ below). not its tree position (see *Manager special-casing today* below).
### Reparenting ### Reparenting
@ -89,7 +89,7 @@ umount-old / mount-new / restart-cascade step.
## Planned topology semantics (once ancestor-based enforcement lands) ## Planned topology semantics (once ancestor-based enforcement lands)
| 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 |
| config change via forge PR (any descendant's config) | any ancestor | | config change via forge PR (any descendant's config) | any ancestor |
@ -138,11 +138,11 @@ other agents don't:
capability. capability.
- **Prompt/tools** — the system prompt uses `<!-- role:agent -->` / - **Prompt/tools** — the system prompt uses `<!-- role:agent -->` /
`<!-- role:manager -->` marker blocks, and a `Flavor::{Agent, `<!-- role:manager -->` marker blocks, and a `Flavor::{Agent,
Manager}` switch picks the MCP tool allow-list claude sees. Both are Manager}` switch picks the MCP tool allow-list claude sees. Both are
already parametrised on a single flavour value, so the planned already parametrised on a single flavour value, so the planned
per-capability-group version (`cap:<group>` prompt blocks + a per-capability-group version (`cap:<group>` prompt blocks + a
matching tool allow-list) is additive rather than a rewrite. matching tool allow-list) is additive rather than a rewrite.
- **State dirs** — _not_ special-cased: `HYPERHIVE_STATE_DIR` is - **State dirs** — *not* special-cased: `HYPERHIVE_STATE_DIR` is
injected uniformly via `systemd.globalEnvironment` for every injected uniformly via `systemd.globalEnvironment` for every
container including the manager, so all token/state paths resolve container including the manager, so all token/state paths resolve
through it the same way everywhere. through it the same way everywhere.

View file

@ -114,7 +114,7 @@ Nothing to run — it is served on the swarm apex
decide whether you can actually open it: decide whether you can actually open it:
- **You are in `admins`** (step 3). The gateway asks authelia whether - **You are in `admins`** (step 3). The gateway asks authelia whether
you have a session; the rule that makes it mean _operator_ wants the you have a session; the rule that makes it mean *operator* wants the
group. Without it you log in and still get bounced. group. Without it you log in and still get bounced.
- **The name resolves to this host.** It is published to the hive's own - **The name resolves to this host.** It is published to the hive's own
resolver and to `/etc/hosts` when `gateway.localHostsEntry` is on; from resolver and to `/etc/hosts` when `gateway.localHostsEntry` is on; from

View file

@ -112,7 +112,7 @@ On a **successful** broker delivery, `forge_notify` marks the thread
read on forge straight away (`PATCH /notifications/threads/{id}`). The 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 **tiny
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.
@ -132,19 +132,19 @@ own read-state is the only durable record this design needs.
**Read-before-comment guard doesn't block a fresh wake.** hive-forge's **Read-before-comment guard doesn't block a fresh wake.** hive-forge's
read-before-comment guard (which keys off forge unread-state) does not read-before-comment guard (which keys off forge unread-state) does not
force the agent to view a thread before commenting on it: the broker force the agent to view a thread before commenting on it: the broker
wake already carries the notification body, so _delivery is the read_. 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 (tiny, 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
re-fire the wake. It is **not persisted** and resets on restart (forge re-fire the wake. It is **not persisted** and resets on restart (forge
read-state covers the durable case). Each poll prunes it to the ids in read-state covers the durable case). Each poll prunes it to the ids in
the single `limit=UNREAD_FETCH_LIMIT` (50) fetch page, so it can never the single `limit=UNREAD_FETCH_LIMIT` (50) fetch page, so it can never
exceed that many entries (a debug assertion pins the invariant; the exceed that many entries (a debug assertion pins the invariant; the
fetch limit and the bound are the same constant). A failed _delivery_ fetch limit and the bound are the same constant). A failed *delivery*
is left unread and out of the map, so it resurfaces next tick. is left unread and out of the map, so it resurfaces next tick.
Self-echo notifications (the agent's own writes, see below) are marked Self-echo notifications (the agent's own writes, see below) are marked
@ -226,7 +226,7 @@ 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\nauthor: body\nassignee: ...` | | Review submission | `[PR approved #N owner/repo] title\nurl: ...\n\nauthor: body\nassignee: ...` |
| New issue / PR | `[new PR #N owner/repo] title\nurl: ...\nassignee: ...` | | New issue / PR | `[new PR #N owner/repo] title\nurl: ...\nassignee: ...` |

View file

@ -109,7 +109,7 @@ Forgejo's notifications API is modelled on GitHub's, so one tolerant
parse serves both: `id`, `repository.full_name`, parse serves both: `id`, `repository.full_name`,
`subject {title,url,latest_comment_url}` and `updated_at` line up field `subject {title,url,latest_comment_url}` and `updated_at` line up field
for field. The two real differences are absorbed by lenient for field. The two real differences are absorbed by lenient
deserializers — GitHub sends the thread id as a _string_ where Forgejo deserializers — GitHub sends the thread id as a *string* where Forgejo
sends a number, and says `PullRequest` where Forgejo says `Pull`. Todo sends a number, and says `PullRequest` where Forgejo says `Pull`. Todo
keys are prefixed `gh:<id>` so a github thread id cannot collide with a keys are prefixed `gh:<id>` so a github thread id cannot collide with a
forge one. forge one.
@ -118,16 +118,16 @@ Two host differences worth knowing before touching this code:
- **Auth scheme, not just value.** Forgejo takes - **Auth scheme, not just value.** Forgejo takes
`Authorization: token <t>`; GitHub wants `Bearer <t>` plus `Accept: `Authorization: token <t>`; GitHub wants `Bearer <t>` plus `Accept:
application/vnd.github+json`, `X-GitHub-Api-Version` and a application/vnd.github+json`, `X-GitHub-Api-Version` and a
`User-Agent`. Sending Forgejo's form to GitHub does not error — it `User-Agent`. Sending Forgejo's form to GitHub does not error — it
authenticates as _nobody_ and silently drops to the unauthenticated authenticates as *nobody* and silently drops to the unauthenticated
rate limit. The cheap way to tell the two apart is the rate-limit rate limit. The cheap way to tell the two apart is the rate-limit
header: `x-ratelimit-remaining` near 5000 is an authenticated user, header: `x-ratelimit-remaining` near 5000 is an authenticated user,
near 60 is anonymous. near 60 is anonymous.
- **GitHub sets the cadence.** It returns `X-Poll-Interval` (60s in - **GitHub sets the cadence.** It returns `X-Poll-Interval` (60s in
practice, slower than our own tick) and rate-limits callers who ignore practice, slower than our own tick) and rate-limits callers who ignore
it, so the loop re-arms to the server's interval whenever that is it, so the loop re-arms to the server's interval whenever that is
_slower_ than ours. A hint faster than our own tick is not a reason to *slower* than ours. A hint faster than our own tick is not a reason to
poll harder. poll harder.
⚠️ **This needs the `notifications` scope on the PAT.** A token minted ⚠️ **This needs the `notifications` scope on the PAT.** A token minted

View file

@ -42,7 +42,7 @@ hive-c0re maintains the local clone at
`internal/knowledge` (see `docs/swarm/README.md` § Swarm-wide forge `internal/knowledge` (see `docs/swarm/README.md` § Swarm-wide forge
webhooks). On any push to main, including merge commits, it sends an webhooks). On any push to main, including merge commits, it sends an
event to every hive over the swarm queue and each hive runs `git event to every hive over the swarm queue and each hive runs `git
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 is offline when the event is sent does not get it on A hive that is offline when the event is sent does not 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. So one

View file

@ -44,7 +44,7 @@ untouched by any of it.
``` ```
| container | netns | IPv4 | listens / reached via | | container | netns | IPv4 | listens / reached via |
| -------------- | ----------------------- | -------------- | ----------------------------------------------------------------------------------------------- | | -------------- | ----------------------- | -------------------- | -------------------------------------------------------------------------------------------- |
| `hive-gateway` | host (shared) | host addresses | nginx `:80`/`:443` (every vhost); dnsmasq `bridgeIp:53` + DHCP `:67` on the bridge | | `hive-gateway` | host (shared) | host addresses | nginx `:80`/`:443` (every vhost); dnsmasq `bridgeIp:53` + DHCP `:67` on the bridge |
| `hive-forge` | host (shared) | host addresses | forgejo `:3000` http, `:2222` git-ssh; fronted by the `forge.<swarm-domain>` vhost | | `hive-forge` | host (shared) | host addresses | forgejo `:3000` http, `:2222` git-ssh; fronted by the `forge.<swarm-domain>` vhost |
| `hive-matrix` | host (shared) | host addresses | tuwunel `:8008` (+ optional federation port); fronted by the matrix vhost | | `hive-matrix` | host (shared) | host addresses | tuwunel `:8008` (+ optional federation port); fronted by the matrix vhost |
@ -57,7 +57,7 @@ The flows, end to end:
host firewall (udp 67 hole) → dnsmasq pool → lease + router option. host firewall (udp 67 hole) → dnsmasq pool → lease + router option.
- **DNS** — agents and the service containers query `bridgeIp:53`; hive - **DNS** — agents and the service containers query `bridgeIp:53`; hive
zones are answered authoritatively with the bridge IP, everything else zones are answered authoritatively with the bridge IP, everything else
forwards to the host's resolvers (see _Resolver behaviour_ below). Each forwards to the host's resolvers (see *Resolver behaviour* below). Each
container points its own `resolv.conf` there, and one that instead container points its own `resolv.conf` there, and one that instead
inherits the host's resolves no swarm name at all — those records exist inherits the host's resolves no swarm name at all — those records exist
only on the bridge. only on the bridge.
@ -169,7 +169,7 @@ agent containers.
The **host** firewall is the only firewall. The shared-netns infra The **host** firewall is the only firewall. The shared-netns infra
containers (gateway, forge, matrix) set containers (gateway, forge, matrix) set
`networking.firewall.enable = false`: a NixOS firewall inside a `networking.firewall.enable = false`: a NixOS firewall inside a
shared-netns container runs against the _host_ ruleset — at container shared-netns container runs against the *host* ruleset — at container
boot its `firewall-start` flushes the `nixos-fw` chains, rebuilds them boot its `firewall-start` flushes the `nixos-fw` chains, rebuilds them
from the container's (empty) port list, and deletes the host's from the container's (empty) port list, and deletes the host's
`nixos-nat-*` chains without recreating them, silently wiping the `nixos-nat-*` chains without recreating them, silently wiping the
@ -183,7 +183,7 @@ By default agents can only reach the host on 80/443 (+53 DNS), so a
host-side service on another port — e.g. a dev OTLP collector you want host-side service on another port — e.g. a dev OTLP collector you want
agents to reach directly — is unreachable. (hyperhive's own telemetry agents to reach directly — is unreachable. (hyperhive's own telemetry
needs none of this: `otel.enable` opens its collector's port itself, and needs none of this: `otel.enable` opens its collector's port itself, and
`otel.endpoint` is the _upstream_, which no agent ever dials. See `otel.endpoint` is the *upstream*, which no agent ever dials. See
`docs/scheduler/observability.md`.) `docs/scheduler/observability.md`.)
`services.hyperhive.network.exposeHostPorts = [ 4318 ];` opens each `services.hyperhive.network.exposeHostPorts = [ 4318 ];` opens each
@ -207,7 +207,7 @@ veth pair attached to the bridge. The following table summarises what
the nix side sets up unconditionally: the nix side sets up unconditionally:
| 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 |
@ -262,7 +262,7 @@ wiring is runtime:
**Why isolation is safe**: hive-c0re's control-plane sockets are unix **Why isolation is safe**: hive-c0re's control-plane sockets are unix
domain sockets bind-mounted into containers, not network listeners — see domain sockets bind-mounted into containers, not network listeners — see
the _Control plane (no network)_ bullet under [Network the *Control plane (no network)* bullet under [Network
map](#network-map) above. `PRIVATE_NETWORK=1` has no effect on a path map](#network-map) above. `PRIVATE_NETWORK=1` has no effect on a path
that never touches the network stack. that never touches the network stack.

View file

@ -43,8 +43,8 @@ Note that the mesh is gated on `swarm.wireguard.enable`, **not** on
## Pointing a hive at it ## Pointing a hive at it
The block above configures the host that _receives_. Every hive that The block above configures the host that *receives*. Every hive that
_pushes_ separately needs to be told where the store is: *pushes* separately needs to be told where the store is:
```nix ```nix
services.hyperhive.swarm.snapshotStore = { services.hyperhive.swarm.snapshotStore = {
@ -65,9 +65,9 @@ inconsistency:
Note the option lives under `swarm.*` while the receiving host's lives Note the option lives under `swarm.*` while the receiving host's lives
under `services.hyperhive.snapshotStore`. That is the distinction the under `services.hyperhive.snapshotStore`. That is the distinction the
two namespaces carry throughout: `swarm.*` describes _the swarm_ as seen two namespaces carry throughout: `swarm.*` describes *the swarm* as seen
from this host, and a bare `services.hyperhive.<service>` describes _a from this host, and a bare `services.hyperhive.<service>` describes *a
role this host performs_. A store host sets both --- one to run the 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>
@ -88,7 +88,7 @@ that address provably came from the holder of that private key. A
packet that reaches the receiver has therefore already been packet that reaches the receiver has therefore already been
authenticated by the kernel. authenticated by the kernel.
Layering TLS client certs on top would authenticate _the same fact_ a Layering TLS client certs on top would authenticate *the same fact* a
second time, and add a credential with an expiry --- a migration that second time, and add a credential with an expiry --- a migration that
fails because a renewal quietly didn't happen, discovered on the day fails because a renewal quietly didn't happen, discovered on the day
you need to move an agent. you need to move an agent.
@ -98,14 +98,14 @@ you need to move an agent.
The destination is keyed by **agent**. The destination is keyed by **agent**.
This is not cosmetic. After a migration, an agent's next incremental This is not cosmetic. After a migration, an agent's next incremental
send arrives from a _different_ hive than the previous one. Keying by send arrives from a *different* hive than the previous one. Keying by
hive would split that agent's snapshot chain across two directories, hive would split that agent's snapshot chain across two directories,
and `btrfs send -p` would fail to find its parent --- breaking exactly and `btrfs send -p` would fail to find its parent --- breaking exactly
the case the store exists to serve. the case the store exists to serve.
## What the sender can and cannot choose ## What the sender can and cannot 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 is chosen by the sender. So the and the subvolume name inside it is chosen by the sender. So the
protocol is one `agent <name>` header line, then the raw stream. protocol is one `agent <name>` header line, then the raw stream.
@ -129,7 +129,7 @@ address being set are assertions, not documentation --- bound to
state. state.
Binding is not sufficient on its own. NixOS's firewall is default-deny Binding is not sufficient on its own. NixOS's firewall is default-deny
and filters in netfilter, _before_ a packet reaches a bound socket, so and filters in netfilter, *before* a packet reaches a bound socket, so
the port is opened explicitly --- and scoped to the mesh interface: the port is opened explicitly --- and scoped to the mesh interface:
```nix ```nix
@ -176,8 +176,8 @@ and means it should get the treatment a backup host gets --- restricted
access, and a decision (rather than an omission) on encryption at rest. access, and a decision (rather than an omission) on encryption at rest.
The trap is the label: this box holds backup-grade data while not being The trap is the label: this box holds backup-grade data while not being
called a backup, so it can end up with backup-grade _exposure_ and called a backup, so it can end up with backup-grade *exposure* and
non-backup-grade _controls_. Nobody puts a migration staging area on non-backup-grade *controls*. Nobody puts a migration staging area on
the access-review list. the access-review list.
### What a snapshot contains ### What a snapshot contains
@ -202,7 +202,7 @@ gap is tracked separately.
### Retention ### Retention
Retention lives on the _sending_ side (last-N by count, swept Retention lives on the *sending* side (last-N by count, swept
periodically), not here. Count rather than age is deliberate: a count periodically), not here. Count rather than age is deliberate: a count
is bounded by construction, whereas an age policy silently scales disk is bounded by construction, whereas an age policy silently scales disk
usage with how hot a hive runs. usage with how hot a hive runs.

View file

@ -78,8 +78,8 @@ existing containers can't be started.
### `RestrictAddressFamilies` fails as "Address family not supported by protocol" ### `RestrictAddressFamilies` fails as "Address family not supported by protocol"
A unit whose `RestrictAddressFamilies` omits a family gets `EAFNOSUPPORT` A unit whose `RestrictAddressFamilies` omits a family gets `EAFNOSUPPORT`
(errno 97) back from `socket()`. Clients surface that as _"tcp open error: (errno 97) back from `socket()`. Clients surface that as *"tcp open error:
Address family not supported by protocol"_ — the message names the Address family not supported by protocol"* — the message names the
**protocol** and never the **sandbox**, so it reads like a dead network, a **protocol** and never the **sandbox**, so it reads like a dead network, a
missing route, or an IPv6 problem. missing route, or an IPv6 problem.
@ -98,7 +98,7 @@ re-checks it when the program changes.** A unit that only served a unix
socket when it was written is correct at `[ "AF_UNIX" ]` and silently wrong socket when it was written is correct at `[ "AF_UNIX" ]` and silently wrong
the day someone adds an HTTP client. Check the unit in the same commit as the day someone adds an HTTP client. Check the unit in the same commit as
the client — and when narrowing it, prefer a test that derives the required the client — and when narrowing it, prefer a test that derives the required
families from the code (which fails on the _next_ client too) over one that families from the code (which fails on the *next* client too) over one that
asserts today's list. asserts today's list.
### `register_agent` is idempotent ### `register_agent` is idempotent
@ -120,7 +120,7 @@ operator's host-level `allowUnfree` does **not** propagate in.
Operators don't need to set anything on their side. Operators don't need to set anything on their side.
That same isolation is why an agent can't pick a claude out of a That same isolation is why an agent can't pick a claude out of a
_different_ nixpkgs by itself: a container only ever sees the one *different* nixpkgs by itself: a container only ever sees the one
nixpkgs the meta flake injects, so an `agent.nix` naming the host's nixpkgs the meta flake injects, so an `agent.nix` naming the host's
`nixpkgs-unstable` has nothing to name. A release channel can trail `nixpkgs-unstable` has nothing to name. A release channel can trail
unstable by weeks on this package, which is what unstable by weeks on this package, which is what
@ -140,7 +140,7 @@ an input only because a docs tree has no runtime dependencies.
The `storePath` trap is worth spelling out, because it is not confined The `storePath` trap is worth spelling out, because it is not confined
to options the operator writes: **any** option of type `package` fed a to options the operator writes: **any** option of type `package` fed a
store-path _string_ coerces through `lib.toDerivation`, i.e. 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

View file

@ -8,7 +8,7 @@ human review that already happened.
Whether a PR can merge, and what counts toward "can", is configured Whether a PR can merge, and what counts toward "can", is configured
per repo in its branch-protection settings — not a fact true of every per repo in its branch-protection settings — not a fact true of every
hive or every repo. The pieces a repo _can_ require: hive or every repo. The pieces a repo *can* require:
- **CI is green** — the repo's required status checks pass on the - **CI is green** — the repo's required status checks pass on the
PR's current head commit, if the repo requires any. PR's current head commit, if the repo requires any.
@ -40,8 +40,8 @@ independently.
Auto-merge isn't "no human ever looked at this." Whoever arms it has Auto-merge isn't "no human ever looked at this." Whoever arms it has
already judged the PR sound at a coarse level — the signal it sends is already judged the PR sound at a coarse level — the signal it sends is
roughly _"apart from maybe minor tweaks a reviewer can still catch, roughly *"apart from maybe minor tweaks a reviewer can still catch,
I think this is fine."_ That's the human-in-the-loop step, and it I think this is fine."* That's the human-in-the-loop step, and it
already happened. No large changes are expected to surface after already happened. No large changes are expected to surface after
that point — a reviewer's job past that point is to flag it if one that point — a reviewer's job past that point is to flag it if one
does, not to assume none ever will. does, not to assume none ever will.

View file

@ -18,11 +18,11 @@ Two things worth knowing before you rely on it:
already has forge access; if you take outside/fork contributions, already has forge access; if you take outside/fork contributions,
gate CI behind Forgejo's fork-PR-approval setting or restrict the gate CI behind Forgejo's fork-PR-approval setting or restrict the
workflow to push-only triggers (forks can't push to your branches). workflow to push-only triggers (forks can't push to your branches).
See _Security_ below for the full threat model. See *Security* below for the full threat model.
- **Watch your disk.** CI builds through the host's nix store with no - **Watch your disk.** CI builds through the host's nix store with no
automatic garbage collection of its own — a busy CI day can fill automatic garbage collection of its own — a busy CI day can fill
`/nix/store` until every job fails with `ENOSPC`. Add the daily + `/nix/store` until every job fails with `ENOSPC`. Add the daily +
disk-pressure GC config from _Host store maintenance_ below to your disk-pressure GC config from *Host store maintenance* below to your
host's NixOS config (not optional if you plan to actually use this). host's NixOS config (not optional if you plan to actually use this).
Everything below this point is implementation detail: exact Everything below this point is implementation detail: exact
@ -37,8 +37,8 @@ three are required checks (forge branch protection) — a hit on any of them
blocks merge. blocks merge.
| Job | What it runs | | Job | What it runs |
| ---------------------- | --------------------------------------------------------------------------------- | | --- | --- |
| **nix flake check** | treefmt formatting, `cargo clippy -D warnings`, `cargo test`, module evaluation | | **nix flake check** | treefmt + rustfmt formatting, `cargo clippy -D warnings`, `cargo test`, module evaluation |
| **tracker-tag lint** | flags `#NNN` issue tags in source and comments (`scripts/check-issue-refs.sh`) | | **tracker-tag lint** | flags `#NNN` issue tags in source and comments (`scripts/check-issue-refs.sh`) |
| **comment-block lint** | flags contiguous comment blocks over 30 lines (`scripts/check-comment-blocks.sh`) | | **comment-block lint** | flags contiguous comment blocks over 30 lines (`scripts/check-comment-blocks.sh`) |
@ -69,7 +69,7 @@ wastes a remote-builder slot. Use the devshell equivalents instead:
```sh ```sh
nix develop -c cargo clippy --all-targets -- -D warnings nix develop -c cargo clippy --all-targets -- -D warnings
nix develop -c cargo test nix develop -c cargo test
nix fmt # authoritative; the formatter set is nix/treefmt.nix nix develop -c treefmt # same as nix fmt; treefmt covers rustfmt + nixfmt + taplo
sh scripts/check-issue-refs.sh # tracker-tag lint sh scripts/check-issue-refs.sh # tracker-tag lint
sh scripts/check-comment-blocks.sh # comment-block lint sh scripts/check-comment-blocks.sh # comment-block lint
``` ```
@ -90,7 +90,7 @@ slow); run those manually before pushing Rust changes.
The internal forge is always present (mandatory), so the runner always has a The internal forge is always present (mandatory), so the runner always has a
hive-forge instance to register against — nothing extra to enable beyond hive-forge instance to register against — nothing extra to enable beyond
`services.hyperhive.deploy.forgejo.ci.enable = true` (see _For operators_ above). `services.hyperhive.deploy.forgejo.ci.enable = true` (see *For operators* above).
Optional tuning: `services.hyperhive.deploy.forgejo.ci.name` (runner name in forge Optional tuning: `services.hyperhive.deploy.forgejo.ci.name` (runner name in forge
admin panel), `concurrency` (parallel job capacity), `labels` (workflow admin panel), `concurrency` (parallel job capacity), `labels` (workflow
@ -131,7 +131,7 @@ Gated on `HYPERHIVE_FORGE_CI_ENABLED` (the nix module sets it on `hive-c0re.serv
- The container boots immediately — nothing gates its start on registration. - The container boots immediately — nothing gates its start on registration.
- tmpfiles seeds `/run/hive-ci/runner-token` with `TOKEN=placeholder` so the runner's `EnvironmentFile` always exists. - tmpfiles seeds `/run/hive-ci/runner-token` with `TOKEN=placeholder` so the runner's `EnvironmentFile` always exists.
- `gitea-runner-hive.service` has an `ExecStartPre` precond (ahead of the nix-daemon wait) that **fails fast** unless it is already registered (`.runner` present) or a real, non-placeholder token is in place. `Restart=on-failure` (no start-limit cap) self-heals it: a runner that precond-fails at boot keeps retrying until hive-c0re writes the token (c0re's explicit restart is the primary path; the retry is the safety net). - `gitea-runner-hive.service` has an `ExecStartPre` precond (ahead of the nix-daemon wait) that **fails fast** unless it is already registered (`.runner` present) or a real, non-placeholder token is in place. `Restart=on-failure` (no start-limit cap) self-heals it: a runner that precond-fails at boot keeps retrying until hive-c0re writes the token (c0re's explicit restart is the primary path; the retry is the safety net).
- **Convergence**: because the token write targets the _host_ file, even if c0re's restart races the container being down, the container later starts, reads the now-real token, passes the precond, and registers on its own. - **Convergence**: because the token write targets the *host* file, even if c0re's restart races the container being down, the container later starts, reads the now-real token, passes the precond, and registers on its own.
## Actions checkout mirror ## Actions checkout mirror

View file

@ -19,7 +19,7 @@ changes) is submitted to the global job-DAG queue (`hive-c0re/src/job_queue/`)
as a **DAG of primitive nodes**. One scheduler task drives all DAGs; as a **DAG of primitive nodes**. One scheduler task drives all DAGs;
concurrency comes from the resource classes below, not from multiple workers. concurrency comes from the resource classes below, not from multiple workers.
Special cases like graceful stop, deferred starts, and the meta-update Special cases like graceful stop, deferred starts, and the meta-update
cascade need no bespoke code paths — each is expressed as a DAG _shape_ cascade need no bespoke code paths — each is expressed as a DAG *shape*
built from the same primitive nodes. built from the same primitive nodes.
### Two levels: DAG and node ### Two levels: DAG and node
@ -43,7 +43,7 @@ there is no malformed spec to reject.
Nix-heavy — hold one of the `buildSlots` permits for the node's duration: Nix-heavy — hold one of the `buildSlots` permits for the node's duration:
| Node | Wraps | | Node | Wraps |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `Prebuild` | `lifecycle::prebuild_toplevel` — build the toplevel out-of-band while the container keeps serving (its meta preamble is the upstream `MetaSync` node). Skipped when the container is already down; `Swap` builds inline instead | | `Prebuild` | `lifecycle::prebuild_toplevel` — build the toplevel out-of-band while the container keeps serving (its meta preamble is the upstream `MetaSync` node). Skipped when the container is already down; `Swap` builds inline instead |
| `Swap` | drop-in rewrite + `nixos-container update` profile-swap (requires the container stopped); the post-swap bookkeeping tail lives in the sibling `RebuildBookkeeping` node | | `Swap` | drop-in rewrite + `nixos-container update` profile-swap (requires the container stopped); the post-swap bookkeeping tail lives in the sibling `RebuildBookkeeping` node |
| `Create` | first-spawn `nixos-container create` proper; assumes the upstream `Provision` node already registered the agent in meta | | `Create` | first-spawn `nixos-container create` proper; assumes the upstream `Provision` node already registered the agent in meta |
@ -54,7 +54,7 @@ Nix-heavy — hold one of the `buildSlots` permits for the node's duration:
Cheap — no build slot: Cheap — no build slot:
| Node | Behavior | | Node | Behavior |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `MergeVerify` | the deploy's pre-merge gate — PR-head drift check, fetch, `verify_commit` eval. Mutates nothing, so a rejection here needs no compensation | | `MergeVerify` | the deploy's pre-merge gate — PR-head drift check, fetch, `verify_commit` eval. Mutates nothing, so a rejection here needs no compensation |
| `DeployTail` | the deploy's `AfterAny` compensation + bookkeeping tail: (1) rolls `applied/main` back from the parked `refs/hyperhive/rollback/<id>` and aborts the staged meta lock when the deploy never confirmed good; (2) mirrors whichever deploy tag landed to the forge config repo, always, best-effort; (3) posts the failing build log back onto the config PR when the deploy failed. Named for (2)/(3), which run on the success path too — not `AbortDeploy`. Infallible by construction | | `DeployTail` | the deploy's `AfterAny` compensation + bookkeeping tail: (1) rolls `applied/main` back from the parked `refs/hyperhive/rollback/<id>` and aborts the staged meta lock when the deploy never confirmed good; (2) mirrors whichever deploy tag landed to the forge config repo, always, best-effort; (3) posts the failing build log back onto the config PR when the deploy failed. Named for (2)/(3), which run on the success path too — not `AbortDeploy`. Infallible by construction |
| `MetaSync` | the rebuild's meta preamble — rebuild-dir prep, idempotent meta `sync_agents`, optional per-agent relock. Holds the `MetaWindow` resource (below); deliberately its own node so the window never covers `Prebuild`'s multi-minute build | | `MetaSync` | the rebuild's meta preamble — rebuild-dir prep, idempotent meta `sync_agents`, optional per-agent relock. Holds the `MetaWindow` resource (below); deliberately its own node so the window never covers `Prebuild`'s multi-minute build |
@ -75,7 +75,7 @@ Cheap — no build slot:
| `SetWanted` | write the durable power intent (`wanted = Up`/`Offline`) as the head node of a power-op DAG, replacing the old pre-submit side effect. Takes the agent lease even though it's a store write, so the intent write and the tail `Reconcile` are atomic per-agent — two racing power ops can't clobber each other's intent before either reconciles | | `SetWanted` | write the durable power intent (`wanted = Up`/`Offline`) as the head node of a power-op DAG, replacing the old pre-submit side effect. Takes the agent lease even though it's a store write, so the intent write and the tail `Reconcile` are atomic per-agent — two racing power ops can't clobber each other's intent before either reconciles |
| `FinalizeDeploy` | deploy phase 3 — drop the rollback ref, plant `deployed/<id>`, commit the staged `flake.lock`. The first two git steps are fatal on purpose, so a confirmed-good deploy's outcome and the repo's state can't disagree | | `FinalizeDeploy` | deploy phase 3 — drop the rollback ref, plant `deployed/<id>`, commit the staged `flake.lock`. The first two git steps are fatal on purpose, so a confirmed-good deploy's outcome and the repo's state can't disagree |
| `ResolveApproval` | tail of an approval-carrying DAG — resolve the approval row from how the work ended (`AfterAny`, one node emitted per outcome). Agentless: the approval row already names its agent | | `ResolveApproval` | tail of an approval-carrying DAG — resolve the approval row from how the work ended (`AfterAny`, one node emitted per outcome). Agentless: the approval row already names its agent |
| `EmitRebuilt` | tail of a rebuild/perm-change — emit the agent's `Rebuilt` manager event (ok/fail per outcome, nothing on cancel). One node per agent _and_ per outcome | | `EmitRebuilt` | tail of a rebuild/perm-change — emit the agent's `Rebuilt` manager event (ok/fail per outcome, nothing on cancel). One node per agent *and* per outcome |
| `WriteDropin` | `set_nspawn_flags` + `set_resource_limits` + daemon-reload | | `WriteDropin` | `set_nspawn_flags` + `set_resource_limits` + daemon-reload |
| `WritePermFile` | commit `tool-groups.json` / `capabilities.json` (single git commit under `META_LOCK`) + emit the P3RM1SS10NS snapshots | | `WritePermFile` | commit `tool-groups.json` / `capabilities.json` (single git commit under `META_LOCK`) + emit the P3RM1SS10NS snapshots |
| `Reparent` | `set-parent` / `set-parent-bulk`: apply every `(child, new_parent)` move under one `META_LOCK` commit (`meta::bulk_commit_topology`), send the per-agent move notifications, rescan + diff-emit. Agentless like `MetaLock` — a bulk move can span multiple agents, and a reparent touches the meta repo, not any one container. `moves` is typed `(Ident, Option<Ident>)` pairs, not raw strings. Rides the existing `Template::MetaUpdate` variant rather than a dedicated one — it's internal-only (never reaches the graph wire), so the stand-in only affects `terminal_hook` dispatch (resolves to no hook either way) and history-retention bucketing | | `Reparent` | `set-parent` / `set-parent-bulk`: apply every `(child, new_parent)` move under one `META_LOCK` commit (`meta::bulk_commit_topology`), send the per-agent move notifications, rescan + diff-emit. Agentless like `MetaLock` — a bulk move can span multiple agents, and a reparent touches the meta repo, not any one container. `moves` is typed `(Ident, Option<Ident>)` pairs, not raw strings. Rides the existing `Template::MetaUpdate` variant rather than a dedicated one — it's internal-only (never reaches the graph wire), so the stand-in only affects `terminal_hook` dispatch (resolves to no hook either way) and history-retention bucketing |
@ -89,7 +89,7 @@ There is deliberately **no `GitCommit` node**: `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.
Two further layers protect the meta repo across _windows_ that span multiple Two further layers protect the meta repo across *windows* that span multiple
`META_LOCK` acquisitions — above all the approval deploy's prepare→finalize `META_LOCK` acquisitions — above all the approval deploy's prepare→finalize
span, which keeps a bumped `flake.lock` **staged uncommitted** for the whole span, which keeps a bumped `flake.lock` **staged uncommitted** for the whole
container build: container build:
@ -104,7 +104,7 @@ container build:
because a resource is held by a subtree root across its whole subtree, which because a resource is held by a subtree root across its whole subtree, which
a `MutexGuard` (bounded by one executor fn) cannot — that is what lets a a `MutexGuard` (bounded by one executor fn) cannot — that is what lets a
multi-node deploy own one window. For the same reason the window must stay multi-node deploy own one window. For the same reason the window must stay
_off_ long store-only work: the rebuild's meta preamble is its own *off* long store-only work: the rebuild's meta preamble is its own
`MetaSync` node, a sibling of (never a parent of) `Prebuild`, so the `MetaSync` node, a sibling of (never a parent of) `Prebuild`, so the
toplevel build runs outside the window and `buildSlots > 1` still gives toplevel build runs outside the window and `buildSlots > 1` still gives
concurrent rebuilds across agents. concurrent rebuilds across agents.
@ -119,10 +119,10 @@ container build:
The `stop` / `start` power ops write the durable `wanted` intent via a head The `stop` / `start` power ops write the durable `wanted` intent via a head
`SetWanted` node (not a pre-submit side effect) — it holds the agent lease, `SetWanted` node (not a pre-submit side effect) — it holds the agent lease,
so intent-write + reconcile is atomic per-agent. `restart` is the exception: so intent-write + reconcile is atomic per-agent. `restart` is the exception:
it writes _no_ intent (no `SetWanted` head) — it bounces the container and it writes *no* intent (no `SetWanted` head) — it bounces the container and
lets the tail `Reconcile` converge to the agent's existing `wanted`, so a lets the tail `Reconcile` converge to the agent's existing `wanted`, so a
deliberately-stopped agent is not forced back up by a hive-wide restart. The deliberately-stopped agent is not forced back up by a hive-wide restart. The
hive-wide power ops — `restart`, `stop`, and `start` — take an agent _list_: hive-wide power ops — `restart`, `stop`, and `start` — take an agent *list*:
a hive-wide `hivectl restart` / `stop` / `start` is ONE DAG with a per-agent a hive-wide `hivectl restart` / `stop` / `start` is ONE DAG with a per-agent
subgraph each (independent roots, run concurrently on their own leases), not subgraph each (independent roots, run concurrently on their own leases), not
N separate DAGs. N separate DAGs.
@ -132,12 +132,12 @@ async `lifecycle::is_running` read), so they live in `job_queue/power.rs`,
not the pure/sync `templates.rs`. Per-agent shape rule: `stop`/`start` carry not the pure/sync `templates.rs`. Per-agent shape rule: `stop`/`start` carry
a head `SetWanted` (intent) — `restart` does not; the tail `Reconcile` a head `SetWanted` (intent) — `restart` does not; the tail `Reconcile`
(convergence guarantee — cheap, noops when already converged) is ALWAYS (convergence guarantee — cheap, noops when already converged) is ALWAYS
present; only the _mechanical_ nodes (`Signal`/`Drain`/`StopForUpdate`) are present; only the *mechanical* nodes (`Signal`/`Drain`/`StopForUpdate`) are
state-conditional — skipped for a _down_ agent (nothing to quiesce/stop). Keeping `Reconcile` in every shape state-conditional — skipped for a *down* agent (nothing to quiesce/stop). Keeping `Reconcile` in every shape
closes the TOCTOU window: if an agent flips state between the `is_running` closes the TOCTOU window: if an agent flips state between the `is_running`
read and node exec, the tail `Reconcile` still converges it in-DAG (with read and node exec, the tail `Reconcile` still converges it in-DAG (with
`StopForUpdate`-noop as the backstop) — no reliance on an external reconcile `StopForUpdate`-noop as the backstop) — no reliance on an external reconcile
sweep. `start` folds the per-agent stale-rev upgrade in (a _down + stale_ sweep. `start` folds the per-agent stale-rev upgrade in (a *down + stale*
agent's subgraph is a rebuild-then-start). agent's subgraph is a rebuild-then-start).
```text ```text
@ -163,7 +163,7 @@ Notable collapses:
leaves it stopped). leaves it stopped).
- **The swap-failure recovery-start is structural**: `Reconcile` deps on - **The swap-failure recovery-start is structural**: `Reconcile` deps on
`Swap` with the one `AfterAny` edge in the system — it runs after `Swap` `Swap` with the one `AfterAny` edge in the system — it runs after `Swap`
terminal ok _or_ fail, bringing a wanted-up agent back on its old config. terminal ok *or* fail, bringing a wanted-up agent back on its old config.
- **Deferred start is automatic**: `Reconcile` holds no build slot, so the - **Deferred start is automatic**: `Reconcile` holds no build slot, so the
next DAG's `Prebuild` starts as soon as `Swap` frees the slot. next DAG's `Prebuild` starts as soon as `Swap` frees the slot.
- **Graceful stop needs no watcher thread**: `Signal`/`Drain` are cheap, so a - **Graceful stop needs no watcher thread**: `Signal`/`Drain` are cheap, so a
@ -171,7 +171,7 @@ Notable collapses:
drains overlap; each DAG's tail `Reconcile` does the actual stop. drains overlap; each DAG's tail `Reconcile` does the actual stop.
- **The meta-update cascade grows in the same DAG on completion**: - **The meta-update cascade grows in the same DAG on completion**:
`MetaLock`'s executor computes the affected agent set after the bump lands `MetaLock`'s executor computes the affected agent set after the bump lands
and grows one `rebuild` subgraph per agent into its _own_ DAG via and grows one `rebuild` subgraph per agent into its *own* DAG via
`append_subgraph` (rooted on the `MetaLock`, `relock = false` so the cascade `append_subgraph` (rooted on the `MetaLock`, `relock = false` so the cascade
doesn't revert the bump). Not child DAGs — one DAG, no `parent_id`. A failed doesn't revert the bump). Not child DAGs — one DAG, no `parent_id`. A failed
bump appends nothing (no cancel-children dance). Same shape as the startup bump appends nothing (no cancel-children dance). Same shape as the startup
@ -180,9 +180,9 @@ Notable collapses:
### Desired-state (spec vs status) ### Desired-state (spec vs status)
Per-agent power _intent_ — `wanted: Up | Offline` — is durable as the Per-agent power *intent* — `wanted: Up | Offline` — is durable as the
`agent_power` table in the coordinator DB (`hive-c0re/src/stores/power.rs`). `agent_power` table in the coordinator DB (`hive-c0re/src/stores/power.rs`).
`container_view` remains the observed _status_; `Reconcile` nodes converge the `container_view` remains the observed *status*; `Reconcile` nodes converge the
two. Setting `wanted` is never a queued node: the power layer two. Setting `wanted` is never a queued node: the power layer
(`job_queue/power.rs`) writes the row synchronously, then inserts the DAG (`job_queue/power.rs`) writes the row synchronously, then inserts the DAG
whose `Reconcile` reads the fresh value — rapid toggles are last-writer-wins. whose `Reconcile` reads the fresh value — rapid toggles are last-writer-wins.
@ -237,15 +237,15 @@ than to one DAG shape it happens to appear in. A **brace** is the one sanctioned
exception: a pure-resource-holder root that declares on behalf of a subtree exception: a pure-resource-holder root that declares on behalf of a subtree
coordinated with itself, whose members then declare nothing. coordinated with itself, whose members then declare nothing.
It is forced rather than stylistic. Declaring a resource means _"I need this It is forced rather than stylistic. Declaring a resource means *"I need this
exclusively"_, and the agent lease is single-unit — so **two siblings that both exclusively"*, and the agent lease is single-unit — so **two siblings that both
declared it could never run concurrently.** For a subtree whose whole point is declared it could never run concurrently.** For a subtree whose whole point is
concurrency (`Prebuild` beside the `Signal` → `Drain` quiesce window), declaring concurrency (`Prebuild` beside the `Signal` → `Drain` quiesce window), declaring
the requirement truthfully on every node and running those nodes in parallel are the requirement truthfully on every node and running those nodes in parallel are
mutually exclusive. One holder above them speaks for the subtree. mutually exclusive. One holder above them speaks for the subtree.
This is the opposite of the failure the declare-your-own rule exists to prevent, This is the opposite of the failure the declare-your-own rule exists to prevent,
not a relapse into it: there the requirement was _implicit_, inferred from a not a relapse into it: there the requirement was *implicit*, inferred from a
node's kind and true only by accident of placement. Here it is explicit, on one node's kind and true only by accident of placement. Here it is explicit, on one
node, with the omission below it documented on the brace itself. node, with the omission below it documented on the brace itself.
@ -257,7 +257,7 @@ Two consequences worth knowing:
leave a gap another DAG could claim the agent in, mid-bounce. A brace supplies leave a gap another DAG could claim the agent in, mid-bounce. A brace supplies
that continuity directly, so the nesting is no longer load-bearing. that continuity directly, so the nesting is no longer load-bearing.
- **Observability is unaffected.** `running_transients` keys off a node's - **Observability is unaffected.** `running_transients` keys off a node's
_payload_ agent, not off a declared lease edge, so every child still lights its *payload* agent, not off a declared lease edge, so every child still lights its
own dashboard pill and still reports its own `takes_container_down` to the own dashboard pill and still reports its own `takes_container_down` to the
crash watcher. A brace itself reports `false`: it parents the stopping nodes crash watcher. A brace itself reports `false`: it parents the stopping nodes
but does not stop anything, and claiming otherwise would widen crash but does not stop anything, and claiming otherwise would widen crash
@ -323,12 +323,12 @@ cancelled-while-queued, which fails the approval instead of dangling it).
### Wire shape ### Wire shape
`RebuildQueueChanged { seq }` (event name kept) — **a bare trigger, no `RebuildQueueChanged { seq }` (event name kept) — **a bare trigger, no
payload.** It says _the queue changed_; a client that wants to know how payload.** It says *the queue changed*; a client that wants to know how
re-fetches `GET /api/jobq/graph`. re-fetches `GET /api/jobq/graph`.
That endpoint serves the graph generically (`hive-jobq-wire`): every node That endpoint serves the graph generically (`hive-jobq-wire`): every node
carries `id`, `parent`, `deps`, `state`, `label` (the node kind's own wire carries `id`, `parent`, `deps`, `state`, `label` (the node kind's own wire
string — the kind _is_ the phase label, there is no separate sub-step string — the kind *is* the phase label, there is no separate sub-step
string) and free-form `data` for what only some kinds have (`agent`, 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`.
@ -341,7 +341,7 @@ live-log panel off the running node.
The event carries no payload by design: shipping a typed projection of The event carries no payload by design: shipping a typed projection of
the whole queue in the event itself would be a second rendering of the the whole queue in the event itself would be a second rendering of the
same graph that has to be kept in agreement by hand with the endpoint same graph that has to be kept in agreement by hand with the endpoint
every consumer actually reads. Telling a client _when_ to refetch is every consumer actually reads. Telling a client *when* to refetch is
the event's whole job. the event's whole job.
--- ---
@ -362,7 +362,7 @@ On startup, `auto_update::run` classifies every agent by rev freshness (the
per-agent `.{name}.hyperhive-rev` marker under `/var/lib/hyperhive/applied/` per-agent `.{name}.hyperhive-rev` marker under `/var/lib/hyperhive/applied/`
vs the current flake path) and persisted `wanted` intent, then: vs the current flake path) and persisted `wanted` intent, then:
1. **Config path** — when _any_ marker is stale, submit one `Boot` 1. **Config path** — when *any* marker is stale, submit one `Boot`
DAG: a `MetaLock` (hyperhive input bump, non-fatal) that grows an in-DAG DAG: a `MetaLock` (hyperhive input bump, non-fatal) that grows an in-DAG
`Rebuild` subgraph for each stale agent whose `wanted = Up` (topology-sorted, `Rebuild` subgraph for each stale agent whose `wanted = Up` (topology-sorted,
parents first). Stale but wanted-offline agents get no boot-time nix work — their parents first). Stale but wanted-offline agents get no boot-time nix work — their
@ -453,7 +453,7 @@ graft's completion then plants the deploy tag — so "did the agent come back
up?" is answered by `Reconcile` succeeding, the same way it is for every up?" is answered by `Reconcile` succeeding, the same way it is for every
other rebuild, instead of by a fused inline start. other rebuild, instead of by a fused inline start.
The grafted nodes land _inside_ `DeployWindow`'s subtree, so they re-enter The grafted nodes land *inside* `DeployWindow`'s subtree, so they re-enter
the meta window and build slot it already holds rather than deadlocking the meta window and build slot it already holds rather than deadlocking
against it. against it.
@ -514,7 +514,7 @@ 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 a completely 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.
@ -529,7 +529,7 @@ Two things to know about the weights:
- They are **hive-wide** — unlike the caps there is no per-agent - They are **hive-wide** — unlike the caps there is no per-agent
override in `meta/resource-limits.json`, so every agent carries the override in `meta/resource-limits.json`, so every agent carries the
same value and the weight does _not_ rank agents against each other. same value and the weight does *not* rank agents against each other.
What `80` buys is that agents yield to everything **not** on this What `80` buys is that agents yield to everything **not** on this
drop-in path: host services (nginx and dnsmasq among them) and the drop-in path: host services (nginx and dnsmasq among them) and the
infra containers (`hive-ci`, `hive-forge`, `hive-gateway`, infra containers (`hive-ci`, `hive-forge`, `hive-gateway`,

View file

@ -2,7 +2,7 @@
Every container operation — rebuild, first-spawn, a config-PR deploy, Every container operation — rebuild, first-spawn, a config-PR deploy,
power changes — runs through one shared job queue. This page explains power changes — runs through one shared job queue. This page explains
what the job queue _is_, as a general idea, independent of what any one what the job queue *is*, as a general idea, independent of what any one
subsystem uses it for. For the hive-c0re-specific step catalogue and the subsystem uses it for. For the hive-c0re-specific step catalogue and the
engineering internals (scheduler, leases, resource windows) see engineering internals (scheduler, leases, resource windows) see
[`coordinator.md`](coordinator.md) instead. [`coordinator.md`](coordinator.md) instead.
@ -23,7 +23,7 @@ Two ideas are all there is to it:
that's the whole reason things queue instead of all firing at once. that's the whole reason things queue instead of all firing at once.
The engine's whole job is: whenever a step's ordering and resource needs The engine's whole job is: whenever a step's ordering and resource needs
are both satisfied, run it. It has no opinion on what the steps _do_ — are both satisfied, run it. It has no opinion on what the steps *do* —
that's supplied by whoever builds the graph. hive-c0re is the one thing that's supplied by whoever builds the graph. hive-c0re is the one thing
building graphs on it today, but nothing about the engine is specific to building graphs on it today, but nothing about the engine is specific to
containers or rebuilds; there's nothing stopping another subsystem from containers or rebuilds; there's nothing stopping another subsystem from
@ -38,7 +38,7 @@ nested under it are that job's steps, in order (occasionally a couple run
side by side). A step shows one of: side by side). A step shows one of:
| Glyph | Meaning | | Glyph | Meaning |
| ----- | ------------------------------------------------------- | | ----- | -------------------------------------------------------- |
| `⏸` | queued, waiting its turn | | `⏸` | queued, waiting its turn |
| `▶` | running | | `▶` | running |
| `◐` | its own work is done, waiting on a step nested under it | | `◐` | its own work is done, waiting on a step nested under it |

View file

@ -46,7 +46,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 is the firewall — `exposeHostPorts` opens the it; what bounds who can reach it is the firewall — `exposeHostPorts` opens the
port on the bridge interface only. So "unauthenticated to a bridge address" port on the bridge interface only. So "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 is a choice rather than an oversight: **any The consequence, stated because it is a choice rather than an oversight: **any
agent can push arbitrary OTLP, and it is forwarded on under the operator's agent can push arbitrary OTLP, and it is forwarded on under the operator's
@ -56,10 +56,10 @@ smuggled out in resource attributes on an otherwise-legitimate export.
That is a **different risk from the one the collector fixes**, and strictly That is a **different risk from the one the collector fixes**, and strictly
smaller than what preceded it: before, every agent held the upstream credential smaller than what preceded it: before, every agent held the upstream credential
itself, so it could do all of the above _and_ use the token anywhere else. The itself, so it could do all of the above *and* use the token anywhere else. The
collector removes the token and keeps the pipe. Agents are inside the trust collector removes the token and keeps the pipe. Agents are inside the trust
boundary (`docs/trust-boundary/security.md`: capability = accepted risk), so an agent being boundary (`docs/trust-boundary/security.md`: capability = accepted risk), so an agent being
able to _send_ is an accepted extension of that boundary — but it is not able to *send* is an accepted extension of that boundary — but it is not
closed by this design, and nothing here should be read as closing it. closed by this design, and nothing here should be read as closing it.
**The `agent` label is self-reported, and no planned authentication changes **The `agent` label is self-reported, and no planned authentication changes
@ -73,13 +73,13 @@ is getting a credential:
- **agent→collector** (this section's hop) stays open on the bridge. Nothing - **agent→collector** (this section's hop) stays open on the bridge. Nothing
downstream can tell one agent's export from another's. downstream can tell one agent's export from another's.
- **hive→swarm** is where the planned ingest auth goes. The swarm tier stamps - **hive→swarm** is where the planned ingest auth goes. The swarm tier stamps
`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` is not — and that falls So a verified `hive` is reachable and a verified `agent` is not — 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
rather than the claims it authenticated with. rather than the claims it authenticated with.
If you need per-agent numbers you can act on, take them from the agent's own If you need per-agent numbers you can act on, take them from the agent's own
@ -101,7 +101,7 @@ Telemetry crosses two collectors, and which one you configure depends on what
the host is: the host is:
| | runs where | receives from | does | | | runs where | receives from | does |
| ------------------------------------ | ---------------------- | --------------------------------- | --------------------------------------------------------------------- | |---|---|---|---|
| **hive tier** — `otel.enable` | every hive with agents | that hive's agents, on the bridge | forwards to the swarm tier. Holds no credential, picks no destination | | **hive tier** — `otel.enable` | every hive with agents | that hive's agents, on the bridge | forwards to the swarm tier. Holds no credential, picks no destination |
| **swarm tier** — `deploy.swarm-otel` | once per swarm | every hive's collector | writes the swarm's store and exports upstream | | **swarm tier** — `deploy.swarm-otel` | once per swarm | every hive's collector | writes the swarm's store and exports upstream |
@ -119,16 +119,16 @@ services.hyperhive.otel = {
every agent needs the credential to authenticate — and the harness delivers every agent needs the credential to authenticate — and the harness delivers
that token into the agent's own `~/.claude/settings.json`, a file the agent can that token into the agent's own `~/.claude/settings.json`, a file the agent can
read. `0600` protects it from other containers, not from the agent itself. As read. `0600` protects it from other containers, not from the agent itself. As
long as the direct path stays _selectable_, that hole stays selectable; an long as the direct path stays *selectable*, that hole stays selectable; an
option that can reintroduce it is a hole with extra steps. option that can reintroduce it is a hole with extra steps.
**Why the tiers stay separate on one box.** They are not collapsed when **Why the tiers stay separate on one box.** They are not collapsed when
co-located: an all-local hive is a statement about _where_ processes run, not co-located: an all-local hive is a statement about *where* processes run, not
about the shape of the deployment. A boundary that disappears locally is one about the shape of the deployment. A boundary that disappears locally is one
the local deployment stops testing. the local deployment stops testing.
**`endpoint` keeps meaning "where telemetry goes upstream."** Neither tier **`endpoint` keeps meaning "where telemetry goes upstream."** Neither tier
redefines it — the agent-facing value is _derived_ redefines it — the agent-facing value is *derived*
(`http://<bridgeIp>:<collector.port>`), so an existing deployment's `endpoint` (`http://<bridgeIp>:<collector.port>`), so an existing deployment's `endpoint`
keeps working unchanged. The bridge port is contributed to `exposeHostPorts` keeps working unchanged. The bridge port is contributed to `exposeHostPorts`
automatically; there is nothing to open by hand. automatically; there is nothing to open by hand.
@ -160,9 +160,9 @@ swarm tier and no metrics appearing for that hive.
⚠️ **`endpoint` must be valid for `protocol`.** The upstream exporter follows ⚠️ **`endpoint` must be valid for `protocol`.** The upstream exporter follows
`otel.protocol` (`grpc` → the gRPC exporter, otherwise OTLP/HTTP), and the gRPC `otel.protocol` (`grpc` → the gRPC exporter, otherwise OTLP/HTTP), and the gRPC
exporter takes an _address_: `https://host/path` is a legal exporter takes an *address*: `https://host/path` is a legal
`OTEL_EXPORTER_OTLP_ENDPOINT` for HTTP but fails as gRPC with _"missing port in `OTEL_EXPORTER_OTLP_ENDPOINT` for HTTP but fails as gRPC with *"missing port in
address"_. The collector's config is validated at build time, so a mismatch is address"*. The collector's config is validated at build time, so a mismatch is
a build error naming the reason rather than telemetry silently going nowhere. a build error naming the reason rather than telemetry silently going nowhere.
## Network access ## Network access
@ -176,7 +176,7 @@ services.hyperhive.network.exposeHostPorts = [ 5432 ];
``` ```
and point whatever consumes it at `10.42.0.1:5432` rather than loopback: inside and point whatever consumes it at `10.42.0.1:5432` rather than loopback: inside
a container, loopback is the _container_. The bridge IP is the host's address on a container, loopback is the *container*. The bridge IP is the host's address on
the `hive-br0` bridge. The service must also bind an address the bridge can the `hive-br0` bridge. The service must also bind an address the bridge can
reach — a `127.0.0.1`-only listener stays unreachable no matter what the reach — a `127.0.0.1`-only listener stays unreachable no matter what the
firewall allows. See `docs/networking/network.md::Reaching host services` for details. firewall allows. See `docs/networking/network.md::Reaching host services` for details.
@ -197,7 +197,7 @@ Every agent's export therefore includes these resource attributes
automatically: automatically:
| Attribute | Value | | Attribute | Value |
| -------------- | ------------------------------------------------------------ | |-----------|-------|
| `service.name` | `hyperhive-agent` (constant) | | `service.name` | `hyperhive-agent` (constant) |
| `agent` | agent logical name (e.g. `iris`) | | `agent` | agent logical name (e.g. `iris`) |
| `hive` | hive display name (`services.hyperhive.hiveName`) | | `hive` | hive display name (`services.hyperhive.hiveName`) |
@ -223,7 +223,7 @@ exists, so off-the-shelf OTEL/Grafana container dashboards work. Resource
(= the `h-<agent>` machine) and the hive `agent` label: (= the `h-<agent>` machine) and the hive `agent` label:
| Metric | Unit | Kind | Source | | Metric | Unit | Kind | Source |
| ----------------------------------- | ---- | ------- | ----------------------------------------------------------------------------------------------------------- | |--------|------|------|--------|
| `container.cpu.time` | `s` | counter | cumulative `cpu.stat` `usage_usec` → seconds | | `container.cpu.time` | `s` | counter | cumulative `cpu.stat` `usage_usec` → seconds |
| `container.memory.usage` | `By` | gauge | `memory.current` | | `container.memory.usage` | `By` | gauge | `memory.current` |
| `hyperhive.container.memory.limit` | `By` | gauge | `memory.max` (custom — semconv has no `.limit` metric; omitted when unlimited) | | `hyperhive.container.memory.limit` | `By` | gauge | `memory.max` (custom — semconv has no `.limit` metric; omitted when unlimited) |
@ -253,7 +253,7 @@ boundaries. Token usage, cost, and tool-call counts are **not** duplicated
here; that's already covered by Claude's own export. here; that's already covered by Claude's own export.
| Metric | Unit | Kind | Attributes | | Metric | Unit | Kind | Attributes |
| -------------------------------------- | ---- | --------- | ----------------------------------------------------------------------------------------------------------- | |--------|------|------|------------|
| `hyperhive.agent.turn.duration` | `ms` | histogram | `wake_from`, `result_kind`, `model` | | `hyperhive.agent.turn.duration` | `ms` | histogram | `wake_from`, `result_kind`, `model` |
| `hyperhive.agent.turn.count` | — | counter | `wake_from`, `result_kind`, `model` | | `hyperhive.agent.turn.count` | — | counter | `wake_from`, `result_kind`, `model` |
| `hyperhive.agent.session.count` | — | counter | `model` (incremented once per fresh, non-`--continue`'d session) | | `hyperhive.agent.session.count` | — | counter | `model` (incremented once per fresh, non-`--continue`'d session) |
@ -278,7 +278,7 @@ distinguishable from "this hive is gone". Select them with
`{hive!="",agent=""}`. `{hive!="",agent=""}`.
| Metric | Unit | Kind | Meaning | | Metric | Unit | Kind | Meaning |
| ------------------------- | ---- | ----- | -------------------------------------------------------------------------------------------------------------------- | |--------|------|------|---------|
| `process.uptime` | `s` | gauge | seconds since this hive's `hive-c0re` started exporting; a restart reads as a drop to ~0 | | `process.uptime` | `s` | gauge | seconds since this hive's `hive-c0re` started exporting; a restart reads as a drop to ~0 |
| `hyperhive.hive.degraded` | `1` | gauge | `1` while the hive reports itself unhealthy — the same verdict `/health/ready` gives and the swarm status view shows | | `hyperhive.hive.degraded` | `1` | gauge | `1` while the hive reports itself unhealthy — the same verdict `/health/ready` gives and the swarm status view shows |
| `hyperhive.hive.warnings` | `1` | gauge | how many warnings are currently raised, split by a `level` attribute (`warn`, `crit`) | | `hyperhive.hive.warnings` | `1` | gauge | how many warnings are currently raised, split by a `level` attribute (`warn`, `crit`) |
@ -304,7 +304,7 @@ counts of durable rows (issues, comments, repos), and neither a commit nor a
push is stored anywhere as a row to count. push is stored anywhere as a row to count.
| Metric | Unit | Kind | Attributes | | Metric | Unit | Kind | Attributes |
| ---------------------------- | ---- | ------- | ------------------- | |--------|------|------|------------|
| `hyperhive.vcs.commit.count` | — | counter | `repo` (`org/repo`) | | `hyperhive.vcs.commit.count` | — | counter | `repo` (`org/repo`) |
| `hyperhive.vcs.push.count` | — | counter | `repo` (`org/repo`) | | `hyperhive.vcs.push.count` | — | counter | `repo` (`org/repo`) |

View file

@ -7,7 +7,7 @@ the additional config needed when the swarm spans multiple hosts.
For the full option reference rather than prose: `services.hyperhive.swarm.*` For the full option reference rather than prose: `services.hyperhive.swarm.*`
(swarm-wide facts, identical on every host) and `services.hyperhive.deploy.*` (swarm-wide facts, identical on every host) and `services.hyperhive.deploy.*`
(this host's own deployment decisions — does _this_ machine run grafana, (this host's own deployment decisions — does *this* machine run grafana,
the swarm controller, authelia, …) are separate generated pages, `nix the swarm controller, authelia, …) are separate generated pages, `nix
build .#docs-swarm` / `.#docs-deploy` or the website's `/options/swarm.html` build .#docs-swarm` / `.#docs-deploy` or the website's `/options/swarm.html`
/ `/options/deploy.html`. / `/options/deploy.html`.
@ -20,7 +20,7 @@ build .#docs-swarm` / `.#docs-deploy` or the website's `/options/swarm.html`
- **swarm** — one or more hives whose operators have declared them - **swarm** — one or more hives whose operators have declared them
as peers. Agents can be qualified as `agent@hive-domain`. as peers. Agents can be qualified as `agent@hive-domain`.
- **peer hive** — any hive in `services.hyperhive.swarm.hives` other - **peer hive** — any hive in `services.hyperhive.swarm.hives` other
than this one. Peers are _derived_, not declared: the directory lists than this one. Peers are *derived*, not declared: the directory lists
every hive including yourself, and `hiveName` says which one you are. every hive including yourself, and `hiveName` says which one you are.
## Hive identity config ## Hive identity config
@ -47,7 +47,7 @@ and deploys — an eval failure asking the operator to write the address
down is the cheaper outcome. **Upgrading past this release means setting down is the cheaper outcome. **Upgrading past this release means setting
both once.** both once.**
`domain` is required too, but you no longer _write_ it: it is read from `domain` is required too, but you no longer *write* it: it is 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>`. So 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
@ -70,7 +70,7 @@ form qualified labels (`iris@pr1ma.example.com`).
`swarm.name` is purely display — it surfaces in the dashboard chrome `swarm.name` is purely display — it surfaces in the dashboard chrome
header and per-agent system prompts, and federated hives at different header and per-agent system prompts, and federated hives at different
domains can share one. `hiveName` surfaces in the same places but is domains can share one. `hiveName` surfaces in the same places but is
_not_ only display: it is the leftmost label of the hive's domain. That *not* only display: it is the leftmost label of the hive's domain. That
`swarm.name` sits under `swarm` and `hiveName` does not is the whole `swarm.name` sits under `swarm` and `hiveName` does not is the whole
distinction — one names this hive, the other names the group it belongs distinction — one names this hive, the other names the group it belongs
to. to.
@ -120,14 +120,14 @@ services.hyperhive.swarm.hives = {
``` ```
One attrset describing **every** hive in the swarm, **including this One attrset describing **every** hive in the swarm, **including this
one**, keyed by that hive's `hiveName`. It is meant to be _identical on one**, keyed by that hive's `hiveName`. It is meant to be *identical on
every host_ — write it once, share it, and each host reads it correctly every host* — write it once, share it, and each host reads it correctly
because `services.hyperhive.hiveName` says which entry is itself. because `services.hyperhive.hiveName` says which entry is itself.
Empty (the default) means this host isn't in a swarm. Once non-empty it Empty (the default) means this host isn't in a swarm. Once non-empty it
**must** contain an entry for `hiveName`; eval fails naming the missing **must** contain an entry for `hiveName`; eval fails naming the missing
hive. That assertion is load-bearing rather than pedantic — "my peers" hive. That assertion is load-bearing rather than pedantic — "my peers"
is derived as _everything that isn't me_, so a directory that doesn't is derived as *everything that isn't me*, so a directory that doesn't
contain you derives every hive as a peer and you peer with yourself. contain you derives every hive as a peer and you peer with yourself.
`domain` is required per entry and deliberately undefaulted: it is `domain` is required per entry and deliberately undefaulted: it is
@ -138,7 +138,7 @@ evaluates cleanly points at a real machine that isn't the one you meant.
> 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
> does _not_ own — another swarm's, or one keeping its own CA. That is > does *not* own — another swarm's, or one keeping its own CA. That is
> a cross-swarm problem and wants a mechanism designed for it. (An > a cross-swarm problem and wants a mechanism designed for it. (An
> earlier `certFingerprint` field existed for exactly that gap, pinning > earlier `certFingerprint` field existed for exactly that gap, pinning
> a peer's TLS leaf for hive-c0re's own peer HTTPS checks — removed > a peer's TLS leaf for hive-c0re's own peer HTTPS checks — removed
@ -162,7 +162,7 @@ evaluates cleanly points at a real machine that isn't the one you meant.
⚠️ **That container currently trusts no swarm-internal CA**, so a ⚠️ **That container currently trusts no swarm-internal CA**, so a
self-signed gateway certificate does not federate. The swarm root self-signed gateway certificate does not federate. The swarm root
can't simply be listed there: `security.pki.certificateFiles` is can't simply be listed there: `security.pki.certificateFiles` is
read when the system is _built_, and the root is a runtime file (its read when the system is *built*, and the root is a runtime file (its
key must never enter the store), so there is no build-time name for key must never enter the store), so there is no build-time name for
it. Bridging that needs a runtime mechanism and is tracked as its own it. Bridging that needs a runtime mechanism and is tracked as its own
issue. Until then, federation needs CA-issued certs (ACME). See issue. Until then, federation needs CA-issued certs (ACME). See
@ -184,7 +184,7 @@ swarm.hives = { … }; swarm.hives = { … }; # byte-identical
``` ```
That is the point of the shape, and it removes a class of bug rather That is the point of the shape, and it removes a class of bug rather
than saving typing: a per-host peer list let two hosts hold _different_ than saving typing: a per-host peer list let two hosts hold *different*
facts about the same third hive — a stale endpoint, a rotated facts about the same third hive — a stale endpoint, a rotated
fingerprint — with nothing to detect the disagreement. One entry per fingerprint — with nothing to detect the disagreement. One entry per
hive makes it unrepresentable. hive makes it unrepresentable.
@ -277,7 +277,7 @@ port}` tells this hive where the swarm's `btrfs receive` endpoint is, so
`hivectl agent <name> subvol snapshot push` has somewhere to stream to. `hivectl agent <name> subvol snapshot push` has somewhere to stream to.
It is genuinely swarm-scoped rather than per-peer — a swarm has exactly It is genuinely swarm-scoped rather than per-peer — a swarm has exactly
one store, because the receiver keys destinations by _agent_ so a one store, because the receiver keys destinations by *agent* so a
migrating agent keeps one unbroken incremental chain. See migrating agent keeps one unbroken incremental chain. See
[snapshot-store.md](../networking/snapshot-store.md). [snapshot-store.md](../networking/snapshot-store.md).
@ -306,7 +306,7 @@ A hive publishes only once it has been given the three
`never_reported` — it is not broken, it just has nothing to say upward. `never_reported` — it is not broken, it just has nothing to say upward.
| freshness | what to do about it | | freshness | what to do about it |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | |---|---|
| `fresh` | nothing — reported within `staleAfterSeconds` | | `fresh` | nothing — reported within `staleAfterSeconds` |
| `stale` | the hive stopped reporting. Its last payload is still shown, so check `age_seconds` and the payload for what it managed to say | | `stale` | the hive stopped reporting. Its last payload is still shown, so check `age_seconds` and the payload for what it managed to say |
| `never_reported` | this hive has never reported at all — normally a deployment that hasn't happened, not an outage | | `never_reported` | this hive has never reported at all — normally a deployment that hasn't happened, not an outage |
@ -329,7 +329,7 @@ half-configured hive is an eval error rather than one that quietly never
reports: reports:
| option | what to set it to | | option | what to set it to |
| ------------------ | ------------------------------------------------------ | |---|---|
| `natsUrl` | where the swarm queue listens, as this hive reaches it | | `natsUrl` | where the swarm queue listens, as this hive reaches it |
| `tokenEndpoint` | the swarm IdP's `/api/oidc/token` | | `tokenEndpoint` | the swarm IdP's `/api/oidc/token` |
| `clientSecretFile` | path to this hive's client secret, plaintext | | `clientSecretFile` | path to this hive's client secret, plaintext |
@ -364,8 +364,8 @@ on the `agent-configs` org, both under
`https://<swarm.domain>/webhook/forge/`. `https://<swarm.domain>/webhook/forge/`.
The controller **interprets** a delivery and sends hives a specific The controller **interprets** a delivery and sends hives a specific
message — _the knowledge repo changed_, _deploy agent `foo` at rev message — *the knowledge repo changed*, *deploy agent `foo` at rev
`abc123`_ — rather than forwarding forge payloads for each hive to `abc123`* — rather than forwarding forge payloads for each hive to
re-derive. Approval happens once, at the swarm level: a hive receives a re-derive. Approval happens once, at the swarm level: a hive receives a
decision, not an event to adjudicate. decision, not an event to adjudicate.

Some files were not shown because too many files have changed in this diff Show more