docs: suppress reviewed write-good.Passive false positives
133 hits across 38 files, all previously classified during #4548's sweep and deliberately left un-rewritten (predicate-adjective state/necessity description, design-intent idiom, structural/type-description idiom, no-single-actor topology claim, parallel-triple exception, vale substring-match artifact — see hyperhive#4548's per-PR bodies for the per-hit reasoning). Wraps each one in a scoped <!-- vale write-good.Passive = NO/YES --> pair (the supported mechanism — TokenIgnores has a known offset-drift bug) rather than a blanket per-file or per-rule silence, so a *new* passive-voice hit anywhere in these files still fails once the rule gates CI (next commit). Table/list false positives (docs/swarm/credentials.md's renewal-table cells) wrap the whole block, not each cell. Part of #4546.
This commit is contained in:
parent
7abb16e6d5
commit
04e27c4fb6
38 changed files with 368 additions and 71 deletions
|
|
@ -1,11 +1,15 @@
|
||||||
# Agent hierarchy & privileges
|
# Agent hierarchy & privileges
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
Every agent has a place in an operator-editable parent/child tree, used
|
Every agent has a place in an operator-editable parent/child tree, used
|
||||||
to scope which agents can manage which others. This doc covers how
|
to scope which agents can manage which others. This doc covers how
|
||||||
hive-c0re stores and edits the tree today, the rules that are meant to run on top
|
hive-c0re stores and edits the tree today, the rules that are meant to run on top
|
||||||
of it once enforcement is finished, and where the manager still gets
|
of it once enforcement is finished, and where the manager still gets
|
||||||
special-cased in the meantime, as a tracked cleanup.
|
special-cased in the meantime, as a tracked cleanup.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
## Where the tree lives
|
## Where the tree lives
|
||||||
|
|
||||||
Topology lives in the hive-c0re-owned **meta repo**, alongside
|
Topology lives in the hive-c0re-owned **meta repo**, alongside
|
||||||
|
|
@ -31,7 +35,7 @@ not its tree position (see _Manager special-casing today_ below).
|
||||||
### Reparenting
|
### Reparenting
|
||||||
|
|
||||||
- CLI: `hivectl agent <child> set-parent --parent <new>` (or `--root`
|
- CLI: `hivectl agent <child> set-parent --parent <new>` (or `--root`
|
||||||
to promote). Exactly one of `--parent` / `--root` is required.
|
to promote). Pass exactly one of `--parent` / `--root`.
|
||||||
- Dashboard: `POST /api/topology/set-parent` (form fields `child`,
|
- Dashboard: `POST /api/topology/set-parent` (form fields `child`,
|
||||||
optional `new_parent` — absent / empty ⇒ promote to root).
|
optional `new_parent` — absent / empty ⇒ promote to root).
|
||||||
- Wire: `HostRequest::SetParent { child, new_parent: Option<String> }`.
|
- Wire: `HostRequest::SetParent { child, new_parent: Option<String> }`.
|
||||||
|
|
@ -104,6 +108,8 @@ Enforcement of the ancestor rules above isn't fully wired yet, so the
|
||||||
**manager (`ruth`) still gets some hard-coded special treatment**
|
**manager (`ruth`) still gets some hard-coded special treatment**
|
||||||
other agents don't:
|
other agents don't:
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
- **Naming/bootstrap** — the manager's broker recipient name, state-dir
|
- **Naming/bootstrap** — the manager's broker recipient name, state-dir
|
||||||
key, and nixos-container name are all `ruth` (container `h-ruth`).
|
key, and nixos-container name are all `ruth` (container `h-ruth`).
|
||||||
`hive-c0re` spawns it directly at boot if missing, with no operator
|
`hive-c0re` spawns it directly at boot if missing, with no operator
|
||||||
|
|
@ -154,6 +160,7 @@ Manager}` switch picks the MCP tool allow-list claude sees. Both are
|
||||||
each. (The harness handles reminder cancellation fully in-agent — see
|
each. (The harness handles reminder cancellation fully in-agent — see
|
||||||
the note on `CancelLooseEndKind::Reminder` in
|
the note on `CancelLooseEndKind::Reminder` in
|
||||||
`hive-c0re/src/socket_server/mod.rs`.)
|
`hive-c0re/src/socket_server/mod.rs`.)
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
None of the above is a stable interface — treat the module doc
|
None of the above is a stable interface — treat the module doc
|
||||||
comments as the source of truth for exactly which checks exist today.
|
comments as the source of truth for exactly which checks exist today.
|
||||||
|
|
@ -204,6 +211,8 @@ direct mentions, reviews, and assignments.
|
||||||
path = [ "/run/wrappers" "/run/current-system/sw" ];
|
path = [ "/run/wrappers" "/run/current-system/sw" ];
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
`/run/wrappers` (not `/run/wrappers/bin`) comes first so setuid
|
`/run/wrappers` (not `/run/wrappers/bin`) comes first so setuid
|
||||||
wrappers — notably `sudo` — resolve before bare nix-store binaries; see
|
wrappers — notably `sudo` — resolve before bare nix-store binaries; see
|
||||||
[`docs/process/gotchas.md`](../process/gotchas.md) ("`systemd.services.*.path` appends
|
[`docs/process/gotchas.md`](../process/gotchas.md) ("`systemd.services.*.path` appends
|
||||||
|
|
@ -214,6 +223,8 @@ the non-setuid nix-store binary and every
|
||||||
`services.hyperhive.agent.user.passwordlessSudo` grant fails with "must be owned by
|
`services.hyperhive.agent.user.passwordlessSudo` grant fails with "must be owned by
|
||||||
uid 0 and have the setuid bit set."
|
uid 0 and have the setuid bit set."
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
### `serviceConfig` highlights
|
### `serviceConfig` highlights
|
||||||
|
|
||||||
- `ExecStart = pkgs.hyperhive/bin/hive-agent` — same binary for every
|
- `ExecStart = pkgs.hyperhive/bin/hive-agent` — same binary for every
|
||||||
|
|
|
||||||
|
|
@ -148,9 +148,10 @@ through the web UI or the forge.
|
||||||
### Approval kinds (wire shapes)
|
### Approval kinds (wire shapes)
|
||||||
|
|
||||||
`ApprovalKind` carries four variants; each maps to a different
|
`ApprovalKind` carries four variants; each maps to a different
|
||||||
`commit_ref` encoding because that field is overloaded as the
|
`commit_ref` encoding because `ApprovalKind` overloads that field as
|
||||||
kind-specific payload carrier.
|
the kind-specific payload carrier.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
- `MergeConfigPr` — the config-change flow. Triggered automatically:
|
- `MergeConfigPr` — the config-change flow. Triggered automatically:
|
||||||
when an agent opens (or force-pushes) a PR on its
|
when an agent opens (or force-pushes) a PR on its
|
||||||
`agent-configs/<agent>` forge repo, hive-c0re's `/webhook/config-pr`
|
`agent-configs/<agent>` forge repo, hive-c0re's `/webhook/config-pr`
|
||||||
|
|
@ -187,6 +188,7 @@ kind-specific payload carrier.
|
||||||
`source = approval:<id>`; the worker fans the body out as
|
`source = approval:<id>`; the worker fans the body out as
|
||||||
inbox messages to each target at the scheduled time, recurring
|
inbox messages to each target at the scheduled time, recurring
|
||||||
when `interval_seconds` is set.
|
when `interval_seconds` is set.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
### Scheduled prompts (submit paths)
|
### Scheduled prompts (submit paths)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,7 @@ One table:
|
||||||
- `events(id, ts, kind, payload_json)` — every `LiveEvent` the
|
- `events(id, ts, kind, payload_json)` — every `LiveEvent` the
|
||||||
harness emits during turn loop execution.
|
harness emits during turn loop execution.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
The harness both writes and vacuums it — this used to be a host-side
|
The harness both writes and vacuums it — this used to be a host-side
|
||||||
sweep, but hive-c0re runs as the unprivileged `hive-core` user under
|
sweep, but hive-c0re runs as the unprivileged `hive-core` user under
|
||||||
privsep and can't delete agent-owned files (host-side deletes hit
|
privsep and can't delete agent-owned files (host-side deletes hit
|
||||||
|
|
@ -128,6 +129,7 @@ host-side sweep was meant to prevent — but a compromised harness is
|
||||||
already inside the container trust boundary
|
already inside the container trust boundary
|
||||||
([`docs/trust-boundary/security.md`](../trust-boundary/security.md)), and these are ephemeral local
|
([`docs/trust-boundary/security.md`](../trust-boundary/security.md)), and these are ephemeral local
|
||||||
artifacts, so cleaning them up where they live is the honest fix.
|
artifacts, so cleaning them up where they live is the honest fix.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Path overridable via `HYPERHIVE_EVENTS_DB` (for dev / no-`/harness`
|
Path overridable via `HYPERHIVE_EVENTS_DB` (for dev / no-`/harness`
|
||||||
setups). On open failure the `Bus` falls back to no-store mode
|
setups). On open failure the `Bus` falls back to no-store mode
|
||||||
|
|
@ -238,6 +240,7 @@ harness keeps serving its web UI and MCP daemons but drives no turns,
|
||||||
and inbox messages queue unacked until it's removed (see
|
and inbox messages queue unacked until it's removed (see
|
||||||
[turn loop](../turn-loop/README.md#the-loop)).
|
[turn loop](../turn-loop/README.md#the-loop)).
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
Unusually, it's read and written from **both** sides of the harness
|
Unusually, it's read and written from **both** sides of the harness
|
||||||
bind-mount, and that's the whole design: the harness stats it
|
bind-mount, and that's the whole design: the harness stats it
|
||||||
in-container via `hive-agent`'s `paths::paused_marker`, while hive-c0re
|
in-container via `hive-agent`'s `paths::paused_marker`, while hive-c0re
|
||||||
|
|
@ -248,6 +251,7 @@ dashboard toggle. Because the file itself is the only shared state
|
||||||
there's no protocol between them, no round-trip into the container, and
|
there's no protocol between them, no round-trip into the container, and
|
||||||
pause keeps working when the harness is wedged or the container is
|
pause keeps working when the harness is wedged or the container is
|
||||||
stopped.
|
stopped.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
It lives in `/harness/` rather than `/state/` deliberately: `/state/`
|
It lives in `/harness/` rather than `/state/` deliberately: `/state/`
|
||||||
is the agent's own space to fill, and this is harness control state.
|
is the agent's own space to fill, and this is harness control state.
|
||||||
|
|
@ -346,19 +350,23 @@ nothing argues for a parent reading it, let alone writing it. hive-c0re
|
||||||
reads a child's harness dir **directly on the host** when it wants
|
reads a child's harness dir **directly on the host** when it wants
|
||||||
those stats, which needs no mount into the parent.
|
those stats, which needs no mount into the parent.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
**`config` is read-only, including for the parent.** A config change is
|
**`config` is read-only, including for the parent.** A config change is
|
||||||
a PR on the child's config repo, made from a clone and merged after
|
a PR on the child's config repo, made from a clone and merged after
|
||||||
review — so the bind-mounted `config` dir is a *copy to read*, never a
|
review — so the bind-mounted `config` dir is a *copy to read*, never a
|
||||||
tree anyone edits in place. Mounting it writable would leave a second
|
tree anyone edits in place. Mounting it writable would leave a second
|
||||||
path to the same file that skips the review entirely, which makes the
|
path to the same file that skips the review entirely, which makes the
|
||||||
boundary a convention rather than a permission.
|
boundary a convention rather than a permission.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
⚠️ Don't confuse it with the config-repo seeding hive-c0re does at
|
⚠️ Don't confuse it with the config-repo seeding hive-c0re does at
|
||||||
spawn (`lifecycle::setup_proposed`): that writes the child's initial
|
spawn (`lifecycle::setup_proposed`): that writes the child's initial
|
||||||
config repo as **hive-c0re, against the host path**, and `read_only` on a bind
|
config repo as **hive-c0re, against the host path**, and `read_only` on a bind
|
||||||
constrains writers *inside* a container only. The two are unrelated —
|
constrains writers *inside* a container only. The two are unrelated —
|
||||||
conflating them can lead you to reason your way into thinking this
|
conflating them can lead you to reason your way into thinking this
|
||||||
mount should be writable when it shouldn't.
|
mount should be writable when it shouldn't.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Per-child isolation still holds: a container only ever has its *own*
|
Per-child isolation still holds: a container only ever has its *own*
|
||||||
dirs plus its direct children's bind-mounted, never a sibling's.
|
dirs plus its direct children's bind-mounted, never a sibling's.
|
||||||
|
|
@ -481,10 +489,12 @@ so it survives restarts and redeploys.
|
||||||
verification until the hook is re-registered with the new value. It's
|
verification until the hook is re-registered with the new value. It's
|
||||||
generated automatically on first start; there's nothing to configure.
|
generated automatically on first start; there's nothing to configure.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
If the file is unreadable at startup the daemon still starts and logs
|
If the file is unreadable at startup the daemon still starts and logs
|
||||||
`webhook secret unavailable`; the webhook endpoint then answers 503
|
`webhook secret unavailable`; the webhook endpoint then answers 503
|
||||||
rather than accepting deliveries it can't verify. Everything else the
|
rather than accepting deliveries it can't verify. Everything else the
|
||||||
controller serves is unaffected.
|
controller serves is unaffected.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
## Run-time dirs
|
## Run-time dirs
|
||||||
|
|
||||||
|
|
@ -587,6 +597,7 @@ librsvg) and forwards its store path as `HIVE_ICON_PNG` on the daemon
|
||||||
unit, gated on `services.hyperhive.agent.icon != null`. No icon configured → the env is
|
unit, gated on `services.hyperhive.agent.icon != null`. No icon configured → the env is
|
||||||
unset → `sync_avatar` returns early and sets no avatar.
|
unset → `sync_avatar` returns early and sets no avatar.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
Idempotency is **per-account**: an `avatar-icon-hash` file in each
|
Idempotency is **per-account**: an `avatar-icon-hash` file in each
|
||||||
account's matrix-sdk `state_dir`. The daemon hashes the PNG bytes and
|
account's matrix-sdk `state_dir`. The daemon hashes the PNG bytes and
|
||||||
skips the upload when unchanged, because every upload mints a fresh
|
skips the upload when unchanged, because every upload mints a fresh
|
||||||
|
|
@ -596,4 +607,5 @@ account gets its avatar when the `systemd.paths.hive-matrix-daemon` token
|
||||||
watcher restarts the daemon (which re-runs the per-account bring-up), so
|
watcher restarts the daemon (which re-runs the per-account bring-up), so
|
||||||
no separate avatar trigger is needed. The daemon swallows avatar failures
|
no separate avatar trigger is needed. The daemon swallows avatar failures
|
||||||
(logged, non-fatal) so they never break account bring-up or sync.
|
(logged, non-fatal) so they never break account bring-up or sync.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,16 @@ sequence. All `hivectl` commands below run as **root on the host** (not
|
||||||
inside an agent container); the `request_*` steps run from ruth's own
|
inside an agent container); the `request_*` steps run from ruth's own
|
||||||
turn via the MCP tools.
|
turn via the MCP tools.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
**Bringing up a hive that doesn't host its own swarm services?** Read
|
**Bringing up a hive that doesn't host its own swarm services?** Read
|
||||||
[`swarm/secrets.md`](../swarm/secrets.md) first. Everything below assumes
|
[`swarm/secrets.md`](../swarm/secrets.md) first. Everything below assumes
|
||||||
each credential is generated where it's read, which is true on an
|
each credential is generated where it's read, which is true on an
|
||||||
all-local deploy and not otherwise — that page says which files an
|
all-local deploy and not otherwise — that page says which files an
|
||||||
operator has to place, and where.
|
operator has to place, and where.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
## Step-by-step
|
## Step-by-step
|
||||||
|
|
||||||
### 1 · Forge
|
### 1 · Forge
|
||||||
|
|
@ -157,7 +161,7 @@ leaves it alone.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
Whether anything more is needed depends on
|
What else you need depends on
|
||||||
`services.hyperhive.deploy.bao.seal`:
|
`services.hyperhive.deploy.bao.seal`:
|
||||||
|
|
||||||
- **`pkcs11`** (the default) — pkcs11 binds the key to the host's TPM, and
|
- **`pkcs11`** (the default) — pkcs11 binds the key to the host's TPM, and
|
||||||
|
|
@ -180,8 +184,6 @@ the store can't hand you, being what opens it. ⚠️ Not the gateway's HTTPS ce
|
||||||
store that took its identity from an authority it will itself distribute could
|
store that took its identity from an authority it will itself distribute could
|
||||||
never come up before that authority.
|
never come up before that authority.
|
||||||
|
|
||||||
Making even the `init` unnecessary is tracked as a follow-up.
|
|
||||||
|
|
||||||
### 4 · Swarm SSO (only when `deploy.authelia`)
|
### 4 · Swarm SSO (only when `deploy.authelia`)
|
||||||
|
|
||||||
⚠️ **Required to finish the install, not optional.** Authelia treats an
|
⚠️ **Required to finish the install, not optional.** Authelia treats an
|
||||||
|
|
@ -287,17 +289,20 @@ See [`tools/hivectl.md`](../tools/hivectl.md) for every `hivectl` verb.
|
||||||
|
|
||||||
## Security notes
|
## Security notes
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
- **No forge admin token is stored in any agent state dir.** Agents
|
- **No forge admin token is stored in any agent state dir.** Agents
|
||||||
hold a regular agent token in their `forge-token` file; sensitive
|
hold a regular agent token in their `forge-token` file; sensitive
|
||||||
creds (the core token, the matrix admin token) live on the host.
|
creds (the core token, the matrix admin token) live on the host.
|
||||||
- All config changes (forge PRs on `agent-configs/<name>`) go through
|
- All config changes (forge PRs on `agent-configs/<name>`) go through
|
||||||
operator approval — agents can't unilaterally rebuild containers, by design.
|
operator approval — agents can't unilaterally rebuild containers, by design.
|
||||||
See [`boundary.md`](../trust-boundary/boundary.md) and [`security.md`](../trust-boundary/security.md).
|
See [`boundary.md`](../trust-boundary/boundary.md) and [`security.md`](../trust-boundary/security.md).
|
||||||
- **Telemetry ingest is authenticated per hive**, and the `hive` label comes
|
- **Each hive authenticates its own telemetry ingest**, and the `hive` label comes
|
||||||
from which hive authenticated rather than from the payload — so no hive can
|
from which hive authenticated rather than from the payload — so no hive can
|
||||||
report metrics as another. A first-run all-local hive gets this with nothing
|
report metrics as another. A first-run all-local hive gets this with nothing
|
||||||
to configure; joining a swarm you don't host needs one secret copied across.
|
to configure; joining a swarm you don't host needs one secret copied across.
|
||||||
See [`observability.md`](../scheduler/observability.md#authenticated-ingest).
|
See [`observability.md`](../scheduler/observability.md#authenticated-ingest).
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Once the hive is running, ruth records anything it needs to remember
|
Once the hive is running, ruth records anything it needs to remember
|
||||||
across restarts in `/agents/ruth/state/notes.md`.
|
across restarts in `/agents/ruth/state/notes.md`.
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,15 @@ injects the token into the agent's state dir out of band.
|
||||||
|
|
||||||
## Enabling
|
## Enabling
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
The integration is **on by default** for every agent (`services.hyperhive.agent.github.enable
|
The integration is **on by default** for every agent (`services.hyperhive.agent.github.enable
|
||||||
= true`), inert until the operator provisions a PAT. No per-agent declaration is
|
= true`), inert until the operator provisions a PAT. No per-agent declaration is
|
||||||
needed — an agent gains GitHub by having a PAT written to its token
|
needed — an agent gains GitHub by having a PAT written to its token
|
||||||
file.
|
file.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
To turn it off for the whole hive, set the host option:
|
To turn it off for the whole hive, set the host option:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ Inside any agent container:
|
||||||
/knowledge/README.md # table of contents (seeded on first use)
|
/knowledge/README.md # table of contents (seeded on first use)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
Agents read documents directly from that path. The mount is
|
Agents read documents directly from that path. The mount is
|
||||||
read-only — agents never write through it. To contribute, use the
|
read-only — agents never write through it. To contribute, use the
|
||||||
`hive-forge` AGit flow (no fork needed — see
|
`hive-forge` AGit flow (no fork needed — see
|
||||||
|
|
@ -21,6 +23,8 @@ read-only — agents never write through it. To contribute, use the
|
||||||
the local clone updates automatically (see
|
the local clone updates automatically (see
|
||||||
[Sync mechanism](#sync-mechanism) below).
|
[Sync mechanism](#sync-mechanism) below).
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
## Repository layout
|
## Repository layout
|
||||||
|
|
||||||
Canonical forge location: `internal/knowledge` (org `internal`,
|
Canonical forge location: `internal/knowledge` (org `internal`,
|
||||||
|
|
@ -53,8 +57,8 @@ pull`, so agents see the new content on their next turn.
|
||||||
URL, so a second registration against the same repo doesn't add a
|
URL, so a second registration against the same repo doesn't add a
|
||||||
recipient — it takes delivery away from whoever registered first.
|
recipient — it takes delivery away from whoever registered first.
|
||||||
Earlier versions had each hive register its own; hive-c0re now
|
Earlier versions had each hive register its own; hive-c0re now
|
||||||
removes its own leftover at startup, so no operator step is needed
|
removes its own leftover at startup, so migrating needs no operator
|
||||||
to migrate.
|
step.
|
||||||
|
|
||||||
2. **Periodic pull** — a background task in `hive-c0re::main`
|
2. **Periodic pull** — a background task in `hive-c0re::main`
|
||||||
pulls on a fixed cadence as a fallback (webhook missed, c0re
|
pulls on a fixed cadence as a fallback (webhook missed, c0re
|
||||||
|
|
@ -86,12 +90,15 @@ before returning.
|
||||||
|
|
||||||
## State
|
## State
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
- **Host clone**: `/var/lib/hyperhive/knowledge` — persists across
|
- **Host clone**: `/var/lib/hyperhive/knowledge` — persists across
|
||||||
hive-c0re restarts and agent destroy/recreate. Deleted only by
|
hive-c0re restarts and agent destroy/recreate. Deleted only by
|
||||||
manual operator action.
|
manual operator action.
|
||||||
- **In-container mount**: `/knowledge` — bind-mounted read-only
|
- **In-container mount**: `/knowledge` — bind-mounted read-only
|
||||||
from the host clone on every container start. Gone when container
|
from the host clone on every container start. Gone when container
|
||||||
is stopped; reappears on next start with the current clone state.
|
is stopped; reappears on next start with the current clone state.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
The mount deliberately **excludes `.git`**: the host clone embeds the `core`
|
The mount deliberately **excludes `.git`**: the host clone embeds the `core`
|
||||||
token in `.git/config` (it rides the clone URL), so hive-priv overlays an empty
|
token in `.git/config` (it rides the clone URL), so hive-priv overlays an empty
|
||||||
|
|
|
||||||
|
|
@ -92,9 +92,11 @@ delegation (the latter lives in `gateway.md::Discovery flow`).
|
||||||
|
|
||||||
## Provisioning flow (appservice)
|
## Provisioning flow (appservice)
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
Registration is closed. The hive's own **appservice** creates accounts:
|
Registration is closed. The hive's own **appservice** creates accounts:
|
||||||
hive-c0re holds the appservice token, agents never see
|
hive-c0re holds the appservice token, agents never see
|
||||||
it, and an agent only ever receives its own `access_token`.
|
it, and an agent only ever receives its own `access_token`.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
The appservice has no URL (`url: null` in its registration), so the
|
The appservice has no URL (`url: null` in its registration), so the
|
||||||
homeserver never calls out to it and there is no service to run. What the
|
homeserver never calls out to it and there is no service to run. What the
|
||||||
|
|
@ -175,6 +177,7 @@ Nothing to do, and nothing to time. The activation script mints the
|
||||||
appservice token and renders the registration before the homeserver
|
appservice token and renders the registration before the homeserver
|
||||||
restarts, so the first boot after the switch already has both halves.
|
restarts, so the first boot after the switch already has both halves.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
- **Existing accounts keep working.** An access token lives on the
|
- **Existing accounts keep working.** An access token lives on the
|
||||||
device that minted it; removing the registration token touches no
|
device that minted it; removing the registration token touches no
|
||||||
device, no account and no session. `login_with_password` stays on, so
|
device, no account and no session. `login_with_password` stays on, so
|
||||||
|
|
@ -198,6 +201,7 @@ restarts, so the first boot after the switch already has both halves.
|
||||||
token only until the first successful read. See
|
token only until the first successful read. See
|
||||||
[`../swarm/secrets.md`](../swarm/secrets.md) for how that mint stays
|
[`../swarm/secrets.md`](../swarm/secrets.md) for how that mint stays
|
||||||
idempotent across runs.
|
idempotent across runs.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|
@ -239,6 +243,7 @@ into it. This gives the operator a single Space in FluffyChat or any
|
||||||
Matrix client that groups all agent-to-agent + operator rooms in one
|
Matrix client that groups all agent-to-agent + operator rooms in one
|
||||||
place.
|
place.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
The sweep also provisions a default **`hive-chat` room** as an
|
The sweep also provisions a default **`hive-chat` room** as an
|
||||||
`m.space.child` of the Space. Joining a Space doesn't autojoin
|
`m.space.child` of the Space. Joining a Space doesn't autojoin
|
||||||
child rooms — the explicit room entry ensures the operator and every
|
child rooms — the explicit room entry ensures the operator and every
|
||||||
|
|
@ -246,6 +251,7 @@ agent can find a common chat room without manual setup. Room join is
|
||||||
restricted (any Space member including the operator can join; agents
|
restricted (any Space member including the operator can join; agents
|
||||||
are explicitly invited). Room version pinned to 10 for the restricted
|
are explicitly invited). Room version pinned to 10 for the restricted
|
||||||
join floor.
|
join floor.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
**State**: hive-c0re persists both room IDs to `/var/lib/hyperhive/matrix/`
|
**State**: hive-c0re persists both room IDs to `/var/lib/hyperhive/matrix/`
|
||||||
(mode `0600`, owned by the hive-c0re service user):
|
(mode `0600`, owned by the hive-c0re service user):
|
||||||
|
|
@ -270,9 +276,9 @@ services.hyperhive.swarm.matrix = {
|
||||||
```
|
```
|
||||||
|
|
||||||
**`trustedServers`** (default `[]`) — list of peer homeserver names
|
**`trustedServers`** (default `[]`) — list of peer homeserver names
|
||||||
whose signing keys tuwunel will fetch and trust. Federation is enabled
|
whose signing keys tuwunel will fetch and trust. tuwunel enables federation
|
||||||
at the protocol level from first boot (`allow_federation = true`) but
|
at the protocol level from first boot (`allow_federation = true`) but
|
||||||
tuwunel trusts no remote homeserver until you list it here. For a closed
|
trusts no remote homeserver until you list it here. For a closed
|
||||||
single-hive deployment the default empty list is correct — add peer
|
single-hive deployment the default empty list is correct — add peer
|
||||||
hive domains here when connecting hives into a swarm (see
|
hive domains here when connecting hives into a swarm (see
|
||||||
[`docs/swarm/`](../swarm/README.md)).
|
[`docs/swarm/`](../swarm/README.md)).
|
||||||
|
|
@ -296,14 +302,14 @@ surprising behaviour:
|
||||||
SSO is unconditional, so the three below are requirements of running a
|
SSO is unconditional, so the three below are requirements of running a
|
||||||
homeserver at all rather than of a setting:
|
homeserver at all rather than of a setting:
|
||||||
|
|
||||||
- **`sso.clientSecretFile` is required** — fails at eval, not at boot:
|
- **Set `sso.clientSecretFile`** — fails at eval, not at boot:
|
||||||
tuwunel reads its identity providers from the config file, so a
|
tuwunel reads its identity providers from the config file, so a
|
||||||
half-configured one can stop the homeserver from starting outright
|
half-configured one can stop the homeserver from starting outright
|
||||||
rather than merely hiding a login button. On a host that also runs
|
rather than merely hiding a login button. On a host that also runs
|
||||||
the swarm's authelia it's wired up for you.
|
the swarm's authelia it's wired up for you.
|
||||||
- **`swarm.authelia.url` is required** — without a provider URL there
|
- **Set `swarm.authelia.url`** — without a provider URL there
|
||||||
is nothing to discover against.
|
is nothing to discover against.
|
||||||
- **`gatewayHost != null` is required** — the SSO callback URL is
|
- **Set `gatewayHost != null`** — the SSO callback URL is
|
||||||
format-locked to `<homeserver>/_matrix/client/unstable/login/sso/callback/<client_id>`,
|
format-locked to `<homeserver>/_matrix/client/unstable/login/sso/callback/<client_id>`,
|
||||||
and the identity provider needs a public name to redirect the
|
and the identity provider needs a public name to redirect the
|
||||||
browser to.
|
browser to.
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ This host's nginx fronts the hyperhive web surfaces running on it — next to hi
|
||||||
|
|
||||||
Only the host that **runs** authelia declares the authelia vhost, not every hive that uses it — a client hive knows the swarm's `authelia.url` but must not answer for a name it doesn't serve. Its server name is exactly `swarm.authelia.domain`: authelia validates `authelia_url ⊂ session cookie domain` at startup, so a near-miss is a container that refuses to boot. It carries no `auth_basic` — the login page must not sit behind the login mechanism it replaces — and sets the four `X-Forwarded-{Proto,Host,Uri,For}` headers, since authelia decides by the *original* request rather than the hop it sees.
|
Only the host that **runs** authelia declares the authelia vhost, not every hive that uses it — a client hive knows the swarm's `authelia.url` but must not answer for a name it doesn't serve. Its server name is exactly `swarm.authelia.domain`: authelia validates `authelia_url ⊂ session cookie domain` at startup, so a near-miss is a container that refuses to boot. It carries no `auth_basic` — the login page must not sit behind the login mechanism it replaces — and sets the four `X-Forwarded-{Proto,Host,Uri,For}` headers, since authelia decides by the *original* request rather than the hop it sees.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
⚠️ **A `502` from this vhost typically means authelia has no users yet, not that the proxy is misconfigured.** Authelia treats an empty user store as a fatal startup error, so an enabled-but-unbootstrapped swarm crash-loops the container while the vhost in front of it works perfectly. Check `journalctl -M swarm-authelia -u authelia-swarm` before suspecting anything here; the bootstrap step is in [`swarm/sso.md`](../swarm/sso.md).
|
⚠️ **A `502` from this vhost typically means authelia has no users yet, not that the proxy is misconfigured.** Authelia treats an empty user store as a fatal startup error, so an enabled-but-unbootstrapped swarm crash-loops the container while the vhost in front of it works perfectly. Check `journalctl -M swarm-authelia -u authelia-swarm` before suspecting anything here; the bootstrap step is in [`swarm/sso.md`](../swarm/sso.md).
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Per-agent UIs stay sub-path, forge and matrix get sub-domains — see
|
Per-agent UIs stay sub-path, forge and matrix get sub-domains — see
|
||||||
[Sub-domain shape (rationale)](#sub-domain-shape-rationale) below for why.
|
[Sub-domain shape (rationale)](#sub-domain-shape-rationale) below for why.
|
||||||
|
|
@ -34,7 +36,9 @@ Operator points client at `<hive>`. Sequence:
|
||||||
|
|
||||||
matrix-dart-sdk (FluffyChat etc.) hardcodes `https` for the well-known fetch regardless of input scheme, so the discovery endpoint MUST be https — see "Self-signed TLS" below for the cert generation that backs the default-on path.
|
matrix-dart-sdk (FluffyChat etc.) hardcodes `https` for the well-known fetch regardless of input scheme, so the discovery endpoint MUST be https — see "Self-signed TLS" below for the cert generation that backs the default-on path.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
Federation peers fetch `.well-known/matrix/server` → `{"m.server":"chat.<swarm>:<httpsPort>"}` (the federation delegation always carries an explicit port, even the HTTPS default 443 — the https-implies-443 elision only applies to the client base_url above). Gateway only listens on configured `port` (+ `httpsPort` when TLS on); cross-hive federation needs either an SRV record (`_matrix._tcp.chat.<swarm>` → port 80 / 443) OR `matrix.openFirewall = true` so peers reach tuwunel's federation port directly. Hyperhive is closed/internal in most deployments, so this rarely bites.
|
Federation peers fetch `.well-known/matrix/server` → `{"m.server":"chat.<swarm>:<httpsPort>"}` (the federation delegation always carries an explicit port, even the HTTPS default 443 — the https-implies-443 elision only applies to the client base_url above). Gateway only listens on configured `port` (+ `httpsPort` when TLS on); cross-hive federation needs either an SRV record (`_matrix._tcp.chat.<swarm>` → port 80 / 443) OR `matrix.openFirewall = true` so peers reach tuwunel's federation port directly. Hyperhive is closed/internal in most deployments, so this rarely bites.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
## SPA fallback (Accept-header pattern)
|
## SPA fallback (Accept-header pattern)
|
||||||
|
|
||||||
|
|
@ -134,12 +138,14 @@ now set unconditionally for every agent. The mechanism:
|
||||||
retry and report on (`RELOAD_PENDING`), not two units racing on an
|
retry and report on (`RELOAD_PENDING`), not two units racing on an
|
||||||
inotify event.
|
inotify event.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
c0re regenerates `agents.conf` (and triggers a reload) on two
|
c0re regenerates `agents.conf` (and triggers a reload) on two
|
||||||
triggers: every topology change (new/removed agents) and every 10s
|
triggers: every topology change (new/removed agents) and every 10s
|
||||||
marker poll tick (`agent_sockets::spawn_poll`). `write()` is
|
marker poll tick (`agent_sockets::spawn_poll`). `write()` is
|
||||||
idempotent — skips the rename when content is unchanged.
|
idempotent — skips the rename when content is unchanged.
|
||||||
`gateway_nginx::reload_if_pending` automatically retries failed reloads
|
`gateway_nginx::reload_if_pending` automatically retries failed reloads
|
||||||
on subsequent poll ticks.
|
on subsequent poll ticks.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
`agents.conf` uses atomic `<path>.tmp` + `rename()` writes so a crashing
|
`agents.conf` uses atomic `<path>.tmp` + `rename()` writes so a crashing
|
||||||
c0re process never leaves a partial or unparseable file behind.
|
c0re process never leaves a partial or unparseable file behind.
|
||||||
|
|
@ -229,7 +235,9 @@ swarm-services fallback below is for.
|
||||||
|
|
||||||
**Cert shape**: leaf subject CN = bare hive domain; subjectAltName is `<hive>` plus wildcard `*.<hive>`, so all current and future sub-domain vhosts validate under the same leaf + the hive CA. You can't add a swarm service whose name is *not* under this hive's domain here — the hive CA is name-constrained to `<hive>`, and a violating SAN invalidates the whole leaf, not just that name. Those names get the swarm-services leaf instead ([`swarm/ca.md`](../swarm/ca.md)).
|
**Cert shape**: leaf subject CN = bare hive domain; subjectAltName is `<hive>` plus wildcard `*.<hive>`, so all current and future sub-domain vhosts validate under the same leaf + the hive CA. You can't add a swarm service whose name is *not* under this hive's domain here — the hive CA is name-constrained to `<hive>`, and a violating SAN invalidates the whole leaf, not just that name. Those names get the swarm-services leaf instead ([`swarm/ca.md`](../swarm/ca.md)).
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
**Rotation**: `hive-tls-ca.service` is idempotent — it re-signs the leaf when it's missing or within 30 days of expiry, always under the same CA (so consumer trust is undisturbed). It regenerates the CA itself only if missing or already expired. To force a leaf rotation, delete `gateway.pem` under the state dir and restart the unit, then reload `nginx`.
|
**Rotation**: `hive-tls-ca.service` is idempotent — it re-signs the leaf when it's missing or within 30 days of expiry, always under the same CA (so consumer trust is undisturbed). It regenerates the CA itself only if missing or already expired. To force a leaf rotation, delete `gateway.pem` under the state dir and restart the unit, then reload `nginx`.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
**Cert prompts**: browsers still warn once per host until the operator adds the hive's `trust-bundle.pem` to the browser/OS trust store (an anchor, not the leaf, is the thing to trust). A separate mechanism wires agent trust (see the agent-trust work for `/run/hive-ca`).
|
**Cert prompts**: browsers still warn once per host until the operator adds the hive's `trust-bundle.pem` to the browser/OS trust store (an anchor, not the leaf, is the thing to trust). A separate mechanism wires agent trust (see the agent-trust work for `/run/hive-ca`).
|
||||||
|
|
||||||
|
|
@ -292,6 +300,7 @@ TLS (see [TLS modes](#tls-modes) above).
|
||||||
Every agent hashes into the same port range (no special case), so
|
Every agent hashes into the same port range (no special case), so
|
||||||
one range opening covers every container.
|
one range opening covers every container.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
The dashboard port (`services.hyperhive.c0re.dashboardPort`, default 7000) is *not*
|
The dashboard port (`services.hyperhive.c0re.dashboardPort`, default 7000) is *not*
|
||||||
listed in either case — it binds `127.0.0.1` only, so a firewall
|
listed in either case — it binds `127.0.0.1` only, so a firewall
|
||||||
hole would be a no-op. Remote dashboard access flows through the
|
hole would be a no-op. Remote dashboard access flows through the
|
||||||
|
|
@ -299,6 +308,7 @@ gateway. Operators who opt out of the gateway lose external
|
||||||
dashboard reach by design — the surface is privileged (approve /
|
dashboard reach by design — the surface is privileged (approve /
|
||||||
deny / destroy), and operators must not expose it without a real reverse
|
deny / destroy), and operators must not expose it without a real reverse
|
||||||
proxy in front.
|
proxy in front.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
## `HIVE_FORGE_URL`: agents reach the forge via the gateway by domain
|
## `HIVE_FORGE_URL`: agents reach the forge via the gateway by domain
|
||||||
|
|
||||||
|
|
@ -308,7 +318,7 @@ shared-netns mode no longer exists), so agents run in a private netns and
|
||||||
can never reach the host's loopback.
|
can never reach the host's loopback.
|
||||||
`nix/host-modules/hive-c0re/environment.nix` sets `HIVE_FORGE_URL` to
|
`nix/host-modules/hive-c0re/environment.nix` sets `HIVE_FORGE_URL` to
|
||||||
`http://<forge.domain>` (default `forge.<swarm-domain>` — a swarm runs
|
`http://<forge.domain>` (default `forge.<swarm-domain>` — a swarm runs
|
||||||
one forge; `services.hyperhive.domain` is required). Agents
|
one forge; you must set `services.hyperhive.domain`). Agents
|
||||||
get the bridge dnsmasq as their resolver, resolve the hostname →
|
get the bridge dnsmasq as their resolver, resolve the hostname →
|
||||||
bridge IP, then reach nginx on port 80 (the bridge firewall opens
|
bridge IP, then reach nginx on port 80 (the bridge firewall opens
|
||||||
80+443). nginx proxies to forgejo — the same path an operator browser
|
80+443). nginx proxies to forgejo — the same path an operator browser
|
||||||
|
|
@ -357,6 +367,7 @@ only if you already have another process bound to 3000.
|
||||||
uses for `git clone/push/pull` over SSH (`git@<domain>:owner/repo.git`
|
uses for `git clone/push/pull` over SSH (`git@<domain>:owner/repo.git`
|
||||||
via `-p 2222`). Port 22 stays alone on the host for openssh.
|
via `-p 2222`). Port 22 stays alone on the host for openssh.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
`openFirewall` (default **false**) controls whether the host firewall
|
`openFirewall` (default **false**) controls whether the host firewall
|
||||||
opens `httpPort` and `sshPort`. Off by default (secure by
|
opens `httpPort` and `sshPort`. Off by default (secure by
|
||||||
default): agents reach Forgejo through the gateway (`forge.<swarm-domain>` on
|
default): agents reach Forgejo through the gateway (`forge.<swarm-domain>` on
|
||||||
|
|
@ -365,6 +376,7 @@ the bridge), not the raw port, so no firewall hole is needed. Flip to
|
||||||
- The operator's browser to reach `http://<host>:<httpPort>/` directly
|
- The operator's browser to reach `http://<host>:<httpPort>/` directly
|
||||||
(not behind the gateway).
|
(not behind the gateway).
|
||||||
- External git clients that push/pull via SSH directly to the host.
|
- External git clients that push/pull via SSH directly to the host.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Forgejo served through the gateway (`deploy.forgejo.behindGateway = true`) does
|
Forgejo served through the gateway (`deploy.forgejo.behindGateway = true`) does
|
||||||
not need `openFirewall` — the gateway's own `openFirewall` option covers
|
not need `openFirewall` — the gateway's own `openFirewall` option covers
|
||||||
|
|
@ -495,12 +507,14 @@ palette (`#1e1e2e` bg, `#cdd6f4` text, `#cba6f7` heading). No
|
||||||
dependencies on the frontend dist — these pages render even when
|
dependencies on the frontend dist — these pages render even when
|
||||||
hive-c0re itself is down.
|
hive-c0re itself is down.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
Scope is intentionally narrow: a route earns a custom page when the
|
Scope is intentionally narrow: a route earns a custom page when the
|
||||||
default status code would point at the wrong component. The per-agent
|
default status code would point at the wrong component. The per-agent
|
||||||
routes qualify (a 502 there means the harness is restarting, not that
|
routes qualify (a 502 there means the harness is restarting, not that
|
||||||
the gateway is broken), and so does `auth.<swarm>` — a dead authelia
|
the gateway is broken), and so does `auth.<swarm>` — a dead authelia
|
||||||
upstream almost always means the user store was never bootstrapped, and
|
upstream almost always means the user store was never bootstrapped, and
|
||||||
a bare 502 blames the proxy, which is the one part that's working.
|
a bare 502 blames the proxy, which is the one part that's working.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Forge / matrix / fluffychat still get nginx defaults: their upstreams
|
Forge / matrix / fluffychat still get nginx defaults: their upstreams
|
||||||
being down means what the status code says, so a themed page would add
|
being down means what the status code says, so a themed page would add
|
||||||
|
|
@ -508,10 +522,12 @@ styling and no information.
|
||||||
|
|
||||||
## HTTP Basic auth
|
## HTTP Basic auth
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
`services.hyperhive.gateway.auth.enable = true` gates every request to
|
`services.hyperhive.gateway.auth.enable = true` gates every request to
|
||||||
the main vhost (`_`) behind HTTP Basic auth. nginx's built-in `auth_basic`
|
the main vhost (`_`) behind HTTP Basic auth. nginx's built-in `auth_basic`
|
||||||
module validates credentials; no extra service or host-side daemon is
|
module validates credentials; no extra service or host-side daemon is
|
||||||
required.
|
required.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
**Setup:**
|
**Setup:**
|
||||||
|
|
||||||
|
|
@ -522,10 +538,12 @@ services.hyperhive.gateway.auth = {
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
The credential store lives at the fixed path
|
The credential store lives at the fixed path
|
||||||
`/var/lib/hive-gateway/conf/gateway.htpasswd` on the host. A tmpfiles
|
`/var/lib/hive-gateway/conf/gateway.htpasswd` on the host. A tmpfiles
|
||||||
rule pre-creates the file on first boot; no manual path configuration
|
rule pre-creates the file on first boot; no manual path configuration
|
||||||
is required. nginx reads it at that path directly.
|
is required. nginx reads it at that path directly.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Manage users with `hivectl gateway`. `hivectl` sends the request over the
|
Manage users with `hivectl gateway`. `hivectl` sends the request over the
|
||||||
host admin socket and the `hive-c0re` daemon performs the write at its
|
host admin socket and the `hive-c0re` daemon performs the write at its
|
||||||
|
|
@ -545,9 +563,11 @@ hivectl gateway delete-user bob
|
||||||
hivectl gateway list-users
|
hivectl gateway list-users
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
The daemon hashes passwords with BCrypt (cost 12) and writes
|
The daemon hashes passwords with BCrypt (cost 12) and writes
|
||||||
`$2y$`-prefixed hashes that nginx accepts natively. No external
|
`$2y$`-prefixed hashes that nginx accepts natively. No external
|
||||||
`htpasswd` binary is required.
|
`htpasswd` binary is required.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
**What's not gated:** per-agent UI routes emitted into `agents.conf`
|
**What's not gated:** per-agent UI routes emitted into `agents.conf`
|
||||||
(served under `/agent/<name>/`) inherit no auth from `/` — nginx
|
(served under `/agent/<name>/`) inherit no auth from `/` — nginx
|
||||||
|
|
@ -612,6 +632,7 @@ TLS-less mode that could violate it.
|
||||||
|
|
||||||
## Dialing another vhost by name (`verifiedProxyTo`)
|
## Dialing another vhost by name (`verifiedProxyTo`)
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
`vhost-lib.nix`'s `verifiedProxyTo` builds the `proxy_ssl_*` /
|
`vhost-lib.nix`'s `verifiedProxyTo` builds the `proxy_ssl_*` /
|
||||||
`proxy_set_header` block a module uses to dial another service on this
|
`proxy_set_header` block a module uses to dial another service on this
|
||||||
same gateway BY NAME over https, verified. One definition rather than a
|
same gateway BY NAME over https, verified. One definition rather than a
|
||||||
|
|
@ -619,6 +640,7 @@ copy per module: nginx verifies nothing by default
|
||||||
(`proxy_ssl_verify` is off), so a `proxy_pass https://…` without these
|
(`proxy_ssl_verify` is off), so a `proxy_pass https://…` without these
|
||||||
lines is encrypted and unauthenticated. That failure is invisible — it
|
lines is encrypted and unauthenticated. That failure is invisible — it
|
||||||
works, and keeps working, against any certificate at all.
|
works, and keeps working, against any certificate at all.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Every line earns its place, each confirmed against a real nginx with
|
Every line earns its place, each confirmed against a real nginx with
|
||||||
the opposite arm run as a control:
|
the opposite arm run as a control:
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ Configured via `services.hyperhive.network.*`.
|
||||||
One picture of the whole hive — two planes: **infra
|
One picture of the whole hive — two planes: **infra
|
||||||
containers share the host netns** and bind host ports directly;
|
containers share the host netns** and bind host ports directly;
|
||||||
**compute containers (agents + CI) each get a private netns** behind
|
**compute containers (agents + CI) each get a private netns** behind
|
||||||
the bridge. The unix-socket control plane rides the VFS and is
|
the bridge. The unix-socket control plane rides the VFS, and none of
|
||||||
untouched by any of it.
|
that touches it.
|
||||||
|
|
||||||
```
|
```
|
||||||
internet
|
internet
|
||||||
|
|
@ -192,12 +192,16 @@ listed TCP port `P` on the bridge-interface `allowedTCPPorts`, so an
|
||||||
agent can connect to `<bridgeIp>:P` (point the collector endpoint at
|
agent can connect to `<bridgeIp>:P` (point the collector endpoint at
|
||||||
`http://<bridgeIp>:4318`, default `http://10.42.0.1:4318`).
|
`http://<bridgeIp>:4318`, default `http://10.42.0.1:4318`).
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
This is **firewall-only**: the host service must bind an address
|
This is **firewall-only**: the host service must bind an address
|
||||||
reachable from the bridge — `0.0.0.0` or the bridge IP — not loopback
|
reachable from the bridge — `0.0.0.0` or the bridge IP — not loopback
|
||||||
only. The bridge→`127.0.0.0/8` DROP rule (below) is unchanged, so a
|
only. The bridge→`127.0.0.0/8` DROP rule (below) is unchanged, so a
|
||||||
service bound to `127.0.0.1` only stays unreachable; rebind it to
|
service bound to `127.0.0.1` only stays unreachable; rebind it to
|
||||||
`0.0.0.0`.
|
`0.0.0.0`.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
The port is reachable by **every** agent on the bridge subnet (like
|
The port is reachable by **every** agent on the bridge subnet (like
|
||||||
DNS/gateway), so only expose services safe for any agent to reach.
|
DNS/gateway), so only expose services safe for any agent to reach.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -188,25 +188,31 @@ of `state/`, `claude/` and `harness/` (see
|
||||||
section](../agent-lifecycle/persistence.md#btrfs-subvolumes-for-varlibhyperhiveagentsname)
|
section](../agent-lifecycle/persistence.md#btrfs-subvolumes-for-varlibhyperhiveagentsname)
|
||||||
for how and when hive-c0re creates that subvolume). Consequences:
|
for how and when hive-c0re creates that subvolume). Consequences:
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
- The Claude session (`claude/`) travels, so a restored agent keeps its
|
- The Claude session (`claude/`) travels, so a restored agent keeps its
|
||||||
live `--continue` session rather than needing to log in again.
|
live `--continue` session rather than needing to log in again.
|
||||||
- `harness/` travels too, including `harness/bash-tasks/`. Task output
|
- `harness/` travels too, including `harness/bash-tasks/`. Task output
|
||||||
is part of an agent's working continuity, so this is wanted --- but it
|
is part of an agent's working continuity, so this is wanted --- but it
|
||||||
means anything that has ever leaked into a task's captured output is
|
means anything that has ever leaked into a task's captured output is
|
||||||
in the retained snapshots as well.
|
in the retained snapshots as well.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
It does **not** cover the agent's applied config (`/applied/<name>/`) or
|
It does **not** cover the agent's applied config (`/applied/<name>/`) or
|
||||||
its topology entry, both of which live outside the subvolume. A restore
|
its topology entry, both of which live outside the subvolume. A restore
|
||||||
therefore yields an agent's memory without its definition; closing that
|
therefore yields an agent's memory without its definition.
|
||||||
gap is tracked separately.
|
|
||||||
|
|
||||||
### Retention
|
### Retention
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Per-agent or per-hive `btrfs qgroup` quotas aren't configured yet.
|
Per-agent or per-hive `btrfs qgroup` quotas aren't configured yet.
|
||||||
Without them one runaway hive can fill the store and take out every
|
Without them one runaway hive can fill the store and take out every
|
||||||
other hive's snapshots.
|
other hive's snapshots.
|
||||||
|
|
|
||||||
|
|
@ -132,12 +132,14 @@ always passes `wait_seconds: None`, an immediate peek.
|
||||||
|
|
||||||
Per-row bookkeeping inside the broker:
|
Per-row bookkeeping inside the broker:
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
- `delivered_at = NOW` set on every popped row.
|
- `delivered_at = NOW` set on every popped row.
|
||||||
- Each recipient has an in-memory `unacked_ids` list of every row
|
- Each recipient has an in-memory `unacked_ids` list of every row
|
||||||
delivered since the last `AckTurn`.
|
delivered since the last `AckTurn`.
|
||||||
- `redelivered = true` on a row if `RequeueInflight` resurfaced it
|
- `redelivered = true` on a row if `RequeueInflight` resurfaced it
|
||||||
(the harness prepends a "may already be handled" hint when this
|
(the harness prepends a "may already be handled" hint when this
|
||||||
flag is set so the per-message warning is visible).
|
flag is set so the per-message warning is visible).
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
`AgentRequest::AckTurn` closes out the in-memory list — the harness
|
`AgentRequest::AckTurn` closes out the in-memory list — the harness
|
||||||
fires it after `TurnOutcome::Ok`, marking every message popped since
|
fires it after `TurnOutcome::Ok`, marking every message popped since
|
||||||
|
|
@ -269,7 +271,7 @@ changes, and deserialization leniently accepts both the string form
|
||||||
and the legacy bare integer (rolling-deploy skew, persisted blobs).
|
and the legacy bare integer (rolling-deploy skew, persisted blobs).
|
||||||
**Input-direction** fields agents compute as epoch (`first_fire_at_unix`,
|
**Input-direction** fields agents compute as epoch (`first_fire_at_unix`,
|
||||||
schedule-edit `next_fire_at_unix`, `Wakeup::At`) stay integers. The
|
schedule-edit `next_fire_at_unix`, `Wakeup::At`) stay integers. The
|
||||||
`*_unix` field *names* are kept for now — renaming is the wire-types
|
`*_unix` field *names* stay for now — renaming is the wire-types
|
||||||
refactor's concern. The dashboard frontend parses via
|
refactor's concern. The dashboard frontend parses via
|
||||||
`util.js::epochSec` wherever it needs arithmetic and feeds the string
|
`util.js::epochSec` wherever it needs arithmetic and feeds the string
|
||||||
straight to `new Date(s)` for display.
|
straight to `new Date(s)` for display.
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,8 @@ Two things to get right when a daemon needs outbound TCP:
|
||||||
enumerate local addresses before it returns any, so name resolution
|
enumerate local addresses before it returns any, so name resolution
|
||||||
fails without it even when the unit allows `AF_INET`.
|
fails without it even when the unit allows `AF_INET`.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
**The directive is a claim about what the program does, and nothing
|
**The directive is a claim about what the program does, and nothing
|
||||||
re-checks it when the program changes.** A unit that only served a unix
|
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
|
||||||
|
|
@ -101,6 +103,8 @@ 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.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
### `register_agent` is idempotent
|
### `register_agent` is idempotent
|
||||||
|
|
||||||
Drops any prior socket task before rebinding. Required so a
|
Drops any prior socket task before rebinding. Required so a
|
||||||
|
|
@ -114,7 +118,7 @@ socket without needing a clean reinstall.
|
||||||
`claude-code` comes from the flake's main `nixpkgs` (nixos-26.05).
|
`claude-code` comes from the flake's main `nixpkgs` (nixos-26.05).
|
||||||
It's unfree, so the agent modules set `config.allowUnfreePredicate`
|
It's unfree, so the agent modules set `config.allowUnfreePredicate`
|
||||||
at the container level to allowlist `claude-code` specifically —
|
at the container level to allowlist `claude-code` specifically —
|
||||||
scoped, only this one package. This is needed because each per-agent
|
scoped, only this one package, because each per-agent
|
||||||
`nixosConfiguration` evaluates its own nixpkgs instance and the
|
`nixosConfiguration` evaluates its own nixpkgs instance and the
|
||||||
operator's host-level `allowUnfree` does **not** propagate in.
|
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.
|
||||||
|
|
@ -138,6 +142,8 @@ reference-less `-source` (so the runtime closure never arrives), and
|
||||||
pure evaluation rejects. `services.hyperhive.agent.docs.source` gets away with being
|
pure evaluation rejects. `services.hyperhive.agent.docs.source` gets away with being
|
||||||
an input only because a docs tree has no runtime dependencies.
|
an input only because a docs tree has no runtime dependencies.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
The `storePath` trap is worth spelling out, because it's not confined
|
The `storePath` trap is worth spelling out, because it's 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.
|
||||||
|
|
@ -149,6 +155,10 @@ container as text in a real derivation — a symlink farm built from
|
||||||
the interpolated string — before it can go anywhere a package is
|
the interpolated string — before it can go anywhere a package is
|
||||||
expected.
|
expected.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
The catch is that a path written into a generated flake is text, not a
|
The catch is that a path written into a generated flake is text, not a
|
||||||
reference — the container's closure doesn't keep the binary alive.
|
reference — the container's closure doesn't keep the binary alive.
|
||||||
The **host** does: it interpolates the package into
|
The **host** does: it interpolates the package into
|
||||||
|
|
@ -160,6 +170,8 @@ the hive's `claude` out from under it. The price of the root is that
|
||||||
`nix-collect-garbage` can't reclaim an old `claude-code` until every
|
`nix-collect-garbage` can't reclaim an old `claude-code` until every
|
||||||
agent has rebuilt past it and the old generations are gone.
|
agent has rebuilt past it and the old generations are gone.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
### Claude credentials are per-agent
|
### Claude credentials are per-agent
|
||||||
|
|
||||||
`/var/lib/hyperhive/agents/<name>/claude/` bind-mounts to
|
`/var/lib/hyperhive/agents/<name>/claude/` bind-mounts to
|
||||||
|
|
@ -315,6 +327,8 @@ CI on drift).
|
||||||
|
|
||||||
### Split asset derivations away from the rust workspace
|
### Split asset derivations away from the rust workspace
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
`nix/packages/assets.nix` builds the branding SVG/PNG family + claude
|
`nix/packages/assets.nix` builds the branding SVG/PNG family + claude
|
||||||
system-prompt template + claude-settings JSON as its own derivation,
|
system-prompt template + claude-settings JSON as its own derivation,
|
||||||
separate from the hive-ag3nt / hive-c0re crates. Reason: when the
|
separate from the hive-ag3nt / hive-c0re crates. Reason: when the
|
||||||
|
|
@ -327,6 +341,8 @@ its own — the split breaks the coupling at the derivation boundary.
|
||||||
build time; librsvg dependency lives here, not in the rust
|
build time; librsvg dependency lives here, not in the rust
|
||||||
derivation's `nativeBuildInputs`.
|
derivation's `nativeBuildInputs`.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
### `nix fmt` fails in a git worktree with "object not found"
|
### `nix fmt` fails in a git worktree with "object not found"
|
||||||
|
|
||||||
`nix fmt` (and any `nix` command that fetches a `git+file://` flake
|
`nix fmt` (and any `nix` command that fetches a `git+file://` flake
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ independently.
|
||||||
|
|
||||||
## What arming automerge actually means
|
## What arming automerge actually means
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
Automerge isn't "no human ever looked at this." Whoever arms it has
|
Automerge 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,
|
||||||
|
|
@ -46,6 +48,8 @@ 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.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
The practical consequence for a reviewer: on a repo where someone with
|
The practical consequence for a reviewer: on a repo where someone with
|
||||||
write access may already have armed automerge before your review
|
write access may already have armed automerge before your review
|
||||||
lands, a plain `APPROVED` can
|
lands, a plain `APPROVED` can
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ writeup.
|
||||||
|
|
||||||
## CI checks
|
## CI checks
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
Several jobs run on every PR (and on `workflow_dispatch` for manual
|
Several jobs run on every PR (and on `workflow_dispatch` for manual
|
||||||
re-triggers), defined in
|
re-triggers), defined in
|
||||||
[`.forgejo/workflows/ci.yml`](../../.forgejo/workflows/ci.yml). The first four
|
[`.forgejo/workflows/ci.yml`](../../.forgejo/workflows/ci.yml). The first four
|
||||||
|
|
@ -41,6 +43,8 @@ gating on the full job's standing warning/suggestion backlog, but flipping
|
||||||
that switch on the forge is a separate operator action, not something this
|
that switch on the forge is a separate operator action, not something this
|
||||||
table can assert ahead of it happening.
|
table can assert ahead of it happening.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
| Job | What it runs | Required? |
|
| Job | What it runs | Required? |
|
||||||
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
|
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
|
||||||
| **nix flake check** | treefmt formatting, `cargo clippy -D warnings`, `cargo test`, module evaluation | yes |
|
| **nix flake check** | treefmt formatting, `cargo clippy -D warnings`, `cargo test`, module evaluation | yes |
|
||||||
|
|
@ -54,6 +58,8 @@ table can assert ahead of it happening.
|
||||||
`hive-forge ci-rerun --pr N` dispatches a `workflow_dispatch` retrigger
|
`hive-forge ci-rerun --pr N` dispatches a `workflow_dispatch` retrigger
|
||||||
without an empty commit.
|
without an empty commit.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
**`ci-rerun --pr` verifies the code but doesn't reliably move the PR's own
|
**`ci-rerun --pr` verifies the code but doesn't reliably move the PR's own
|
||||||
status checks.** Measured directly (raw `GET /repos/.../actions/tasks`
|
status checks.** Measured directly (raw `GET /repos/.../actions/tasks`
|
||||||
JSON): a genuine PR-triggered run carries `event: pull_request` with the
|
JSON): a genuine PR-triggered run carries `event: pull_request` with the
|
||||||
|
|
@ -70,6 +76,8 @@ ci-log --run <n> --job 0` shows the code green (verdict is the
|
||||||
last line) but the status is stuck, ask the operator to select "rerun" in
|
last line) but the status is stuck, ask the operator to select "rerun" in
|
||||||
the forge web UI — it's CSRF-gated, so only they can do it.
|
the forge web UI — it's CSRF-gated, so only they can do it.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
### Running checks locally
|
### Running checks locally
|
||||||
|
|
||||||
Don't run `nix flake check` directly — it dispatches to the shared build farm and
|
Don't run `nix flake check` directly — it dispatches to the shared build farm and
|
||||||
|
|
@ -144,12 +152,16 @@ Gated on `HYPERHIVE_FORGE_CI_ENABLED` (the nix module sets it on `hive-c0re.serv
|
||||||
|
|
||||||
## Actions checkout mirror
|
## Actions checkout mirror
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
When `deploy.forgejo.ci.enable` is set, hive-c0re autoseeds an
|
When `deploy.forgejo.ci.enable` is set, hive-c0re autoseeds an
|
||||||
`actions/checkout` pull-mirror on the local forge and sets Forgejo's
|
`actions/checkout` pull-mirror on the local forge and sets Forgejo's
|
||||||
`DEFAULT_ACTIONS_URL` to point at the local instance. This means CI
|
`DEFAULT_ACTIONS_URL` to point at the local instance. This means CI
|
||||||
`uses: actions/checkout@vN` steps resolve entirely on loopback — no
|
`uses: actions/checkout@vN` steps resolve entirely on loopback — no
|
||||||
external DNS on the CI critical path.
|
external DNS on the CI critical path.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
**hive-c0re** itself seeds the mirror during its forge
|
**hive-c0re** itself seeds the mirror during its forge
|
||||||
provisioning sweep (`forge/repos.rs::ensure_mirrors`). The nix module
|
provisioning sweep (`forge/repos.rs::ensure_mirrors`). The nix module
|
||||||
forwards the effective mirror list as `HYPERHIVE_FORGE_MIRRORS` in the
|
forwards the effective mirror list as `HYPERHIVE_FORGE_MIRRORS` in the
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,8 @@ Nix-heavy — hold one of the `buildSlots` permits for the node's duration:
|
||||||
|
|
||||||
Cheap — no build slot:
|
Cheap — no build slot:
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
| 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 |
|
||||||
|
|
@ -85,6 +87,8 @@ Cheap — no build slot:
|
||||||
| `KnowledgePull` | one-shot boot-time `/knowledge` pull (`knowledge::pull`), reconciling commits that landed while `hive-c0re` was down. Same rationale as `MatrixSweep`: the periodic hourly re-pull stays a background loop |
|
| `KnowledgePull` | one-shot boot-time `/knowledge` pull (`knowledge::pull`), reconciling commits that landed while `hive-c0re` was down. Same rationale as `MatrixSweep`: the periodic hourly re-pull stays a background loop |
|
||||||
| `WantedPull` | one-shot boot-time pull of the agent set the swarm controller declares for this hive (`wanted::pull`), converging the agents it names. No background loop behind this one — boot is the whole cadence; the deploy event (`swarm_status`) is the fast path, this repairs a missed one. Agentless |
|
| `WantedPull` | one-shot boot-time pull of the agent set the swarm controller declares for this hive (`wanted::pull`), converging the agents it names. No background loop behind this one — boot is the whole cadence; the deploy event (`swarm_status`) is the fast path, this repairs a missed one. Agentless |
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Deliberately, **no `GitCommit` node** exists: `meta.rs` fuses each mutation
|
Deliberately, **no `GitCommit` node** exists: `meta.rs` fuses each mutation
|
||||||
with its commit under its internal `META_LOCK` mutex, so a standalone commit
|
with its commit under its internal `META_LOCK` mutex, so a standalone commit
|
||||||
node would open a dirty-working-tree window between nodes.
|
node would open a dirty-working-tree window between nodes.
|
||||||
|
|
@ -206,9 +210,13 @@ they surface under that DAG's id in the same loop — no separate child DAGs.
|
||||||
|
|
||||||
### Scheduler semantics
|
### Scheduler semantics
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
A node is **ready** when it's `Queued`, every dep is satisfied, and its
|
A node is **ready** when it's `Queued`, every dep is satisfied, and its
|
||||||
resources are free. Resources:
|
resources are free. Resources:
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
1. **Build slots** — `services.hyperhive.c0re.buildSlots` permits (default 1),
|
1. **Build slots** — `services.hyperhive.c0re.buildSlots` permits (default 1),
|
||||||
held by nix-heavy nodes for the node's duration.
|
held by nix-heavy nodes for the node's duration.
|
||||||
2. **Per-agent lifecycle lease** — keyed on the **node's** agent (agent is
|
2. **Per-agent lifecycle lease** — keyed on the **node's** agent (agent is
|
||||||
|
|
@ -251,6 +259,8 @@ node, with the omission below it documented on the brace itself.
|
||||||
|
|
||||||
Two consequences worth knowing:
|
Two consequences worth knowing:
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
- **Flattening a chain under a brace is safe.** The stop chain used to nest
|
- **Flattening a chain under a brace is safe.** The stop chain used to nest
|
||||||
`Signal` over `Drain` over `StopForUpdate` specifically so the lease stayed
|
`Signal` over `Drain` over `StopForUpdate` specifically so the lease stayed
|
||||||
continuous — as independent siblings each would acquire it separately and
|
continuous — as independent siblings each would acquire it separately and
|
||||||
|
|
@ -263,6 +273,7 @@ Two consequences worth knowing:
|
||||||
but doesn't stop anything, and claiming otherwise would widen crash
|
but doesn't stop anything, and claiming otherwise would widen crash
|
||||||
suppression across the build and tail, where a vanished container is still a
|
suppression across the build and tail, where a vanished container is still a
|
||||||
real crash.
|
real crash.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Among simultaneously ready nodes competing for a resource, DAG-submit order
|
Among simultaneously ready nodes competing for a resource, DAG-submit order
|
||||||
wins (FIFO) so bulk operations drain predictably. The scheduler also owns the
|
wins (FIFO) so bulk operations drain predictably. The scheduler also owns the
|
||||||
|
|
@ -276,8 +287,7 @@ reconcile_), so there is no durable-recovery machinery to go wrong.
|
||||||
### Cancel, history
|
### Cancel, history
|
||||||
|
|
||||||
The agent-per-node move removed submit-time dedup (a multi-agent DAG
|
The agent-per-node move removed submit-time dedup (a multi-agent DAG
|
||||||
has no single agent to key a dedup on), so every submit enqueues a fresh DAG;
|
has no single agent to key a dedup on), so every submit enqueues a fresh DAG.
|
||||||
whether any dedup needs reintroducing is tracked as a follow-up.
|
|
||||||
|
|
||||||
Cancel only applies to DAGs that are still fully queued (an in-flight nix build isn't
|
Cancel only applies to DAGs that are still fully queued (an in-flight nix build isn't
|
||||||
interruptible) — each op is one DAG now, so there are no child DAGs to cascade to.
|
interruptible) — each op is one DAG now, so there are no child DAGs to cascade to.
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ Each **row** you see in a queue view (the **BU1LDS** page's R3BU1LD QU3U3
|
||||||
nested under it are that job's steps, in order (occasionally a couple run
|
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:
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
| Glyph | Meaning |
|
| Glyph | Meaning |
|
||||||
| ----- | ------------------------------------------------------- |
|
| ----- | ------------------------------------------------------- |
|
||||||
| `⏸` | queued, waiting its turn |
|
| `⏸` | queued, waiting its turn |
|
||||||
|
|
@ -47,6 +49,8 @@ side by side). A step shows one of:
|
||||||
| `⊘` | cancelled |
|
| `⊘` | cancelled |
|
||||||
| `·` | skipped (not needed for this run) |
|
| `·` | skipped (not needed for this run) |
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
A step that isn't needed for a given run stays in the graph as `·` rather
|
A step that isn't needed for a given run stays in the graph as `·` rather
|
||||||
than being absent from it, so the same kind of operation keeps a
|
than being absent from it, so the same kind of operation keeps a
|
||||||
recognizable shape run to run, whichever steps it actually needed.
|
recognizable shape run to run, whichever steps it actually needed.
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,8 @@ doesn't change that.** Treat it as a convenience for grouping dashboards, never
|
||||||
as evidence of which container produced a sample: any agent that can reach this
|
as evidence of which container produced a sample: any agent that can reach this
|
||||||
hive's collector can label its data as any other agent.
|
hive's collector can label its data as any other agent.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
**Logs ride the same hop, and a journal carries more than a counter does.** Each
|
**Logs ride the same hop, and a journal carries more than a counter does.** Each
|
||||||
agent container forwards its own journal through this port — every unit in it at
|
agent container forwards its own journal through this port — every unit in it at
|
||||||
`info` and above, not an allowlist. That's the harness, the MCP daemons and
|
`info` and above, not an allowlist. That's the harness, the MCP daemons and
|
||||||
|
|
@ -76,6 +78,8 @@ container where before only counts did. The trust boundary is unchanged (same
|
||||||
destination, same credential, and an agent could already send arbitrary OTLP);
|
destination, same credential, and an agent could already send arbitrary OTLP);
|
||||||
what changes is how much detail leaves by default.
|
what changes is how much detail leaves by default.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Worth spelling out, because two different hops are in play and only one of them
|
Worth spelling out, because two different hops are in play and only one of them
|
||||||
carries a credential:
|
carries a credential:
|
||||||
|
|
||||||
|
|
@ -190,9 +194,13 @@ 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.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
⚠️ **None of this is needed for hyperhive's own telemetry** — `otel.enable`
|
⚠️ **None of this is needed for hyperhive's own telemetry** — `otel.enable`
|
||||||
contributes the collector's port and derives the agent-facing endpoint itself.
|
contributes the collector's port and derives the agent-facing endpoint itself.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
## Built-in resource labels
|
## Built-in resource labels
|
||||||
|
|
||||||
The harness sets the OTLP variables (`OTEL_EXPORTER_OTLP_ENDPOINT`, `_PROTOCOL`,
|
The harness sets the OTLP variables (`OTEL_EXPORTER_OTLP_ENDPOINT`, `_PROTOCOL`,
|
||||||
|
|
@ -250,12 +258,16 @@ while lines that never had one signal nothing of the sort.
|
||||||
|
|
||||||
## Host-emitted container-resource metrics (hive-c0re)
|
## Host-emitted container-resource metrics (hive-c0re)
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
When OTEL is enabled, **hive-c0re itself** also exports each agent
|
When OTEL is enabled, **hive-c0re itself** also exports each agent
|
||||||
container's resource load — the same cgroup gauges shown on the dashboard
|
container's resource load — the same cgroup gauges shown on the dashboard
|
||||||
LOAD tab — to this hive's own collector, exactly like an agent does and with
|
LOAD tab — to this hive's own collector, exactly like an agent does and with
|
||||||
no separate toggle. These come from the host, not the in-container Claude SDK,
|
no separate toggle. These come from the host, not the in-container Claude SDK,
|
||||||
so they cover containers even when their agent is idle.
|
so they cover containers even when their agent is idle.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Emitted via the OpenTelemetry Rust SDK, using the
|
Emitted via the OpenTelemetry Rust SDK, using the
|
||||||
[semconv `container.*`](https://opentelemetry.io/docs/specs/semconv/system/container-metrics/)
|
[semconv `container.*`](https://opentelemetry.io/docs/specs/semconv/system/container-metrics/)
|
||||||
metric names + the standard `container.name` attribute where a spec metric
|
metric names + the standard `container.name` attribute where a spec metric
|
||||||
|
|
@ -278,13 +290,19 @@ alongside the spec `container.cpu.time` counter for convenience). Hive
|
||||||
labels (`hive`, `swarm`, …) ride on the resource via
|
labels (`hive`, `swarm`, …) ride on the resource via
|
||||||
`extraResourceAttributes`.
|
`extraResourceAttributes`.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
Cadence follows `metricIntervalMs` (default 60s). Transport is OTLP/HTTP
|
Cadence follows `metricIntervalMs` (default 60s). Transport is OTLP/HTTP
|
||||||
(JSON) to the hive collector's bridge address, with no auth header — that
|
(JSON) to the hive collector's bridge address, with no auth header — that
|
||||||
first hop is unauthenticated for every producer on this host, and the upstream
|
first hop is unauthenticated for every producer on this host, and the upstream
|
||||||
credential stays on the swarm tier.
|
credential stays on the swarm tier.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
## Agent-emitted per-turn metrics (`hive-agent`)
|
## Agent-emitted per-turn metrics (`hive-agent`)
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
When OTEL is enabled, the harness itself (`hive-agent`) exports one small set
|
When OTEL is enabled, the harness itself (`hive-agent`) exports one small set
|
||||||
of metrics per claude turn, recorded the moment the turn ends (not polled).
|
of metrics per claude turn, recorded the moment the turn ends (not polled).
|
||||||
These are deliberately the fields Claude Code's own built-in export (see
|
These are deliberately the fields Claude Code's own built-in export (see
|
||||||
|
|
@ -293,6 +311,8 @@ turn, its own outcome classification, the loose-ends backlog, and session
|
||||||
boundaries. Token usage, cost, and tool-call counts are **not** duplicated
|
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.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
| 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` |
|
||||||
|
|
@ -312,6 +332,8 @@ records them (every turn, always).
|
||||||
|
|
||||||
## Hive-scoped metrics (hive-c0re)
|
## Hive-scoped metrics (hive-c0re)
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
hive-c0re measures everything above **per agent**, tagged with the hive it
|
hive-c0re measures everything above **per agent**, tagged with the hive it
|
||||||
runs in. It measures these three per **hive** instead, and they carry no
|
runs in. It measures these three per **hive** instead, and they carry no
|
||||||
`agent` label — so a hive
|
`agent` label — so a hive
|
||||||
|
|
@ -319,6 +341,8 @@ that hosts no agents still reports, and "this hive is quiet" is
|
||||||
distinguishable from "this hive is gone." Select them with
|
distinguishable from "this hive is gone." Select them with
|
||||||
`{hive!="",agent=""}`.
|
`{hive!="",agent=""}`.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
| 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 |
|
||||||
|
|
@ -420,6 +444,8 @@ overriding Claude Code's default of DELTA. This avoids silent metric drops in
|
||||||
Prometheus-family backends (including Grafana LGTM / Mimir) that don't ship a
|
Prometheus-family backends (including Grafana LGTM / Mimir) that don't ship a
|
||||||
delta-to-cumulative processor.
|
delta-to-cumulative processor.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
**`hive-metric` counters are the one exception**, reporting delta by default
|
**`hive-metric` counters are the one exception**, reporting delta by default
|
||||||
(see above) — programmatically set on the exporter, which overrides this
|
(see above) — programmatically set on the exporter, which overrides this
|
||||||
container-wide env var for that tool specifically. `--type gauge` is
|
container-wide env var for that tool specifically. `--type gauge` is
|
||||||
|
|
@ -428,3 +454,5 @@ runs a `deltatocumulative` processor ahead of export, so a delta
|
||||||
`hive-metric` counter still lands in VictoriaMetrics as a cumulative
|
`hive-metric` counter still lands in VictoriaMetrics as a cumulative
|
||||||
series — the standard `rate()`/`increase()` idioms work on it exactly like
|
series — the standard `rate()`/`increase()` idioms work on it exactly like
|
||||||
any other counter in this system, no special query needed.
|
any other counter in this system, no special query needed.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,8 @@ services.hyperhive = {
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
`swarm.domain` and `hiveName` are **required** whenever hyperhive is
|
`swarm.domain` and `hiveName` are **required** whenever hyperhive is
|
||||||
enabled; eval fails with a hint naming each. Neither defaults,
|
enabled; eval fails with a hint naming each. Neither defaults,
|
||||||
because a guessed value here is a wrong hostname that evaluates cleanly
|
because a guessed value here is a wrong hostname that evaluates cleanly
|
||||||
|
|
@ -47,7 +49,9 @@ 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's read from
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
|
You must still set `domain` too, but you no longer _write_ it: it's read from
|
||||||
this hive's own entry in the directory, whose `domain` defaults to
|
this hive's own entry in the directory, whose `domain` defaults to
|
||||||
`<name>.<swarm.domain>`. A conventional swarm states no addresses at
|
`<name>.<swarm.domain>`. A conventional swarm states no addresses at
|
||||||
all, and a hive addressed by something else states it in the one place
|
all, and a hive addressed by something else states it in the one place
|
||||||
|
|
@ -276,8 +280,8 @@ If one host is behind NAT and can't accept incoming connections, only
|
||||||
that host needs a null `wireguardEndpoint` on the peer config — the
|
that host needs a null `wireguardEndpoint` on the peer config — the
|
||||||
other side initiates. With keepalive on, the NAT hole stays open.
|
other side initiates. With keepalive on, the NAT hole stays open.
|
||||||
|
|
||||||
If both hosts are behind NAT, a STUN relay or a third host (exit node)
|
If both hosts are behind NAT, you need a STUN relay or a third host
|
||||||
is required. Out of scope for v0.
|
(exit node). Out of scope for v0.
|
||||||
|
|
||||||
## Snapshot store
|
## Snapshot store
|
||||||
|
|
||||||
|
|
@ -458,12 +462,16 @@ leftover at startup. A webhook has exactly one target URL, so per-hive
|
||||||
registration never added a recipient — it took delivery away from
|
registration never added a recipient — it took delivery away from
|
||||||
whichever hive registered before it.
|
whichever hive registered before it.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
**The `agent-configs` org isn't yet.** Each hive still registers its own
|
**The `agent-configs` org isn't yet.** Each hive still registers its own
|
||||||
`pull_request` hook there, so that repo has two — the hive's and the
|
`pull_request` hook there, so that repo has two — the hive's and the
|
||||||
controller's — and **both are expected; don't delete either.** Removing
|
controller's — and **both are expected; don't delete either.** Removing
|
||||||
a hive's stops it acting on config PRs; removing the controller's just
|
a hive's stops it acting on config PRs; removing the controller's just
|
||||||
gets recreated on its next start.
|
gets recreated on its next start.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Nothing to configure. The controller registers the hooks only when this
|
Nothing to configure. The controller registers the hooks only when this
|
||||||
host also serves the swarm UI vhost — that's what publishes the
|
host also serves the swarm UI vhost — that's what publishes the
|
||||||
endpoint, and a hook the forge can't reach would collect failed
|
endpoint, and a hook the forge can't reach would collect failed
|
||||||
|
|
|
||||||
|
|
@ -71,9 +71,13 @@ its own subdomain — enforced by every verifier rather than by
|
||||||
convention. The constraint excludes both IP families as well, since a
|
convention. The constraint excludes both IP families as well, since a
|
||||||
permitted-DNS-only constraint says nothing about IP SANs.
|
permitted-DNS-only constraint says nothing about IP SANs.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
The root is issued with `pathlen:1`: it may sign hive CAs, and those may
|
The root is issued with `pathlen:1`: it may sign hive CAs, and those may
|
||||||
sign leaves, and the chain stops there.
|
sign leaves, and the chain stops there.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
## What to hand a peer
|
## What to hand a peer
|
||||||
|
|
||||||
`hivectl peer-config` prints the `services.hyperhive.swarm.hives."<name>"`
|
`hivectl peer-config` prints the `services.hyperhive.swarm.hives."<name>"`
|
||||||
|
|
@ -103,6 +107,8 @@ per-hive file.
|
||||||
|
|
||||||
## Adopting the hierarchy on an existing hive
|
## Adopting the hierarchy on an existing hive
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
A hive that predates the swarm root carries a self-signed `ca.pem`, and
|
A hive that predates the swarm root carries a self-signed `ca.pem`, and
|
||||||
adopting the hierarchy means replacing it. That invalidates an anchor
|
adopting the hierarchy means replacing it. That invalidates an anchor
|
||||||
consumers already trust, and they refresh on their own schedule — agents
|
consumers already trust, and they refresh on their own schedule — agents
|
||||||
|
|
@ -110,6 +116,8 @@ only pick up new trust when their container restarts, peers only on
|
||||||
their own rebuild. **Who is allowed to decide that's what splits the
|
their own rebuild. **Who is allowed to decide that's what splits the
|
||||||
two cases.**
|
two cases.**
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
### Where this host owns the root (`autoConfigure`)
|
### Where this host owns the root (`autoConfigure`)
|
||||||
|
|
||||||
Adoption happens by itself, once. `hive-tls-ca.service` notices that
|
Adoption happens by itself, once. `hive-tls-ca.service` notices that
|
||||||
|
|
@ -141,10 +149,14 @@ rm <deploy.hive-controller.tls.stateDir>/ca.pem <deploy.hive-controller.tls.stat
|
||||||
systemctl restart hive-tls-ca.service
|
systemctl restart hive-tls-ca.service
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
Failing rather than warning is deliberate: a hive whose CA doesn't
|
Failing rather than warning is deliberate: a hive whose CA doesn't
|
||||||
chain to the root it received is misconfigured, and a warning in a
|
chain to the root it received is misconfigured, and a warning in a
|
||||||
build log isn't something anyone reads twice.
|
build log isn't something anyone reads twice.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
To keep the current CA on purpose — a hive that deliberately stays
|
To keep the current CA on purpose — a hive that deliberately stays
|
||||||
outside the hierarchy, or one mid-migration — `touch` the marker file
|
outside the hierarchy, or one mid-migration — `touch` the marker file
|
||||||
named in the message. That's a decision, and it's recorded as one.
|
named in the message. That's a decision, and it's recorded as one.
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
# Credentials: the target shape
|
# Credentials: the target shape
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
The swarm's credential store is bao. This page describes the shape every
|
The swarm's credential store is bao. This page describes the shape every
|
||||||
credential is meant to have — who mints it, who reads it, and how it renews
|
credential is meant to have — who mints it, who reads it, and how it renews
|
||||||
— not what's on disk today. [`secrets.md`](secrets.md) remains the map of
|
— not what's on disk today. [`secrets.md`](secrets.md) remains the map of
|
||||||
the files that exist right now; this page replaces it, and `secrets.md` gets
|
the files that exist right now; this page replaces it, and `secrets.md` gets
|
||||||
deleted, once the swarm's credential path matches what's described below.
|
deleted, once the swarm's credential path matches what's described below.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
**Public material is a value.** The store hands a certificate or a public
|
**Public material is a value.** The store hands a certificate or a public
|
||||||
nkey to every client that connects, so it's a fine place for that material.
|
nkey to every client that connects, so it's a fine place for that material.
|
||||||
Nothing below is about those.
|
Nothing below is about those.
|
||||||
|
|
@ -48,6 +52,8 @@ path at runtime — not a path on disk, and not a unit whose job is to turn a
|
||||||
store value into a file. A renewal cell may never read `NONE`: state the
|
store value into a file. A renewal cell may never read `NONE`: state the
|
||||||
strategy for every credential, including the mTLS leaf.
|
strategy for every credential, including the mTLS leaf.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
| store path | minter | reader — pulls at runtime, holds in memory | renewal |
|
| store path | minter | reader — pulls at runtime, holds in memory | renewal |
|
||||||
| -------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- | -------------- |
|
| -------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- | -------------- |
|
||||||
| `swarm/agents/<agent>/matrix/<account>` | `swarm-controller` | the agent container itself, under the certificate its hive passed in | must be stated |
|
| `swarm/agents/<agent>/matrix/<account>` | `swarm-controller` | the agent container itself, under the certificate its hive passed in | must be stated |
|
||||||
|
|
@ -57,6 +63,8 @@ strategy for every credential, including the mTLS leaf.
|
||||||
| `swarm/services/<clientId>/oidc/client` | authelia | the service process that presents the client secret, under the certificate of the host it runs on | must be stated |
|
| `swarm/services/<clientId>/oidc/client` | authelia | the service process that presents the client secret, under the certificate of the host it runs on | must be stated |
|
||||||
| _(not in the store)_ a hive's mTLS leaf | the store's own PKI, or an operator placing it by hand | its own client, off disk — the exception above, because it's what makes every other row's pull possible | must be stated |
|
| _(not in the store)_ a hive's mTLS leaf | the store's own PKI, or an operator placing it by hand | its own client, off disk — the exception above, because it's what makes every other row's pull possible | must be stated |
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
**An agent's mTLS leaf is in the store; a hive's isn't, and the difference
|
**An agent's mTLS leaf is in the store; a hive's isn't, and the difference
|
||||||
isn't an inconsistency.** The rule the exception protects is that nothing
|
isn't an inconsistency.** The rule the exception protects is that nothing
|
||||||
can fetch from the store the credential it would need in order to fetch. A
|
can fetch from the store the credential it would need in order to fetch. A
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ eval, naming the option it wants. That's deliberate: a queue that started
|
||||||
without them would accept `CONNECT {"user":"auth"}` from anyone sharing the
|
without them would accept `CONNECT {"user":"auth"}` from anyone sharing the
|
||||||
host's network namespace, and nothing would look wrong until somebody connected.
|
host's network namespace, and nothing would look wrong until somebody connected.
|
||||||
|
|
||||||
**All four or none** — the seed paths are required too, not just the public
|
**All four or none** — you must supply the seed paths too, not just the public
|
||||||
keys. They're two halves of the same pair: the server verifies with the public
|
keys. They're two halves of the same pair: the server verifies with the public
|
||||||
half, the responder signs with the private one. Supplying only the public keys
|
half, the responder signs with the private one. Supplying only the public keys
|
||||||
used to pass eval and leave the queue with an auth-callout nobody answers, which
|
used to pass eval and leave the queue with an auth-callout nobody answers, which
|
||||||
|
|
@ -195,7 +195,7 @@ the client id it authenticates, because that's how a queue client reads them —
|
||||||
the secret by path, the id by value. No local fallback exists, and none is
|
the secret by path, the id by value. No local fallback exists, and none is
|
||||||
possible, so absent files mean this hive's agents don't connect, which is what
|
possible, so absent files mean this hive's agents don't connect, which is what
|
||||||
a swarm looks like before the publisher on the authelia host has run. The
|
a swarm looks like before the publisher on the authelia host has run. The
|
||||||
reader runs before `hive-c0re.service` and is wanted (not required) by it, so
|
reader runs before `hive-c0re.service`, which wants it (doesn't require it), so
|
||||||
an agent container never renders ahead of the credential; an unreachable store
|
an agent container never renders ahead of the credential; an unreachable store
|
||||||
delays the daemon's start rather than failing it.
|
delays the daemon's start rather than failing it.
|
||||||
|
|
||||||
|
|
@ -231,9 +231,9 @@ exists so a host holds **one** out-of-band secret, its client certificate, and
|
||||||
reads everything else with it.
|
reads everything else with it.
|
||||||
|
|
||||||
Two things follow, and `swarm-grafana.nix` asserts both rather than degrading:
|
Two things follow, and `swarm-grafana.nix` asserts both rather than degrading:
|
||||||
running Grafana requires `swarm.authelia.url` (its local login form is disabled
|
running Grafana requires `swarm.authelia.url` (this module disables its local
|
||||||
unconditionally, so SSO isn't a feature of some topologies — it's the only way
|
login form unconditionally, so SSO isn't a feature of some topologies — it's the
|
||||||
in), and it requires this host's `deploy.bao.clientCertFile` /
|
only way in), and it requires this host's `deploy.bao.clientCertFile` /
|
||||||
`clientKeyFile`. Each refusal names the option to set. Both used to be silent:
|
`clientKeyFile`. Each refusal names the option to set. Both used to be silent:
|
||||||
a null URL dropped the OIDC block, a missing leaf produced a warning, and
|
a null URL dropped the OIDC block, a missing leaf produced a warning, and
|
||||||
either one left a Grafana with no SSO and no password box — nothing failed, so
|
either one left a Grafana with no SSO and no password box — nothing failed, so
|
||||||
|
|
|
||||||
|
|
@ -118,12 +118,16 @@ See `retentionPeriod` below before leaving it at its default.
|
||||||
| `swarm.grafana.oidc.role` | Default `Admin` for everyone who logs in. Lower to `Viewer`/`Editor` if the swarm grows operators who shouldn't be able to reconfigure Grafana. |
|
| `swarm.grafana.oidc.role` | Default `Admin` for everyone who logs in. Lower to `Viewer`/`Editor` if the swarm grows operators who shouldn't be able to reconfigure Grafana. |
|
||||||
| `deploy.grafana.datasourceUrl` | Only if you front VictoriaMetrics with something else. It defaults to the store on this host, which is the only thing it can reach. |
|
| `deploy.grafana.datasourceUrl` | Only if you front VictoriaMetrics with something else. It defaults to the store on this host, which is the only thing it can reach. |
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
**Logging in.** Grafana is behind swarm SSO, so the accounts are the
|
**Logging in.** Grafana is behind swarm SSO, so the accounts are the
|
||||||
authelia ones — there is no separate Grafana password, and this module
|
authelia ones — there is no separate Grafana password, and this module
|
||||||
switches off the local login form whenever SSO is configured. If you enable
|
switches off the local login form whenever SSO is configured. If you enable
|
||||||
Grafana on a host with no authelia, the form stays on and Grafana's
|
Grafana on a host with no authelia, the form stays on and Grafana's
|
||||||
default `admin`/`admin` applies; change it before exposing that host.
|
default `admin`/`admin` applies; change it before exposing that host.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
**Where the data comes from.** The swarm's OTEL collector, below.
|
**Where the data comes from.** The swarm's OTEL collector, below.
|
||||||
|
|
||||||
Neither container is reachable except through the gateway: both bind
|
Neither container is reachable except through the gateway: both bind
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,8 @@ read that same value, so the redirect URI authelia accepts and the one
|
||||||
the service actually sends can't drift apart. A mismatch there is a
|
the service actually sends can't drift apart. A mismatch there is a
|
||||||
rejected login with no error text worth reading.
|
rejected login with no error text worth reading.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
⚠️ The delivery is a copy, not a `bindMounts` entry, and deliberately so:
|
⚠️ The delivery is a copy, not a `bindMounts` entry, and deliberately so:
|
||||||
nixos-container refuses to start a container whose bind source is
|
nixos-container refuses to start a container whose bind source is
|
||||||
missing, and this secret doesn't exist until authelia's first boot has
|
missing, and this secret doesn't exist until authelia's first boot has
|
||||||
|
|
@ -128,6 +130,8 @@ run. Binding it would make the service wait on a file that waits on a
|
||||||
container that starts after it — on a fresh hive, a permanent stall
|
container that starts after it — on a fresh hive, a permanent stall
|
||||||
presenting as "the forge is broken," many layers from its cause.
|
presenting as "the forge is broken," many layers from its cause.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
### 2. Swarm-managed services
|
### 2. Swarm-managed services
|
||||||
|
|
||||||
The controller side owns provisioning: `swarmctl` writes both halves, the
|
The controller side owns provisioning: `swarmctl` writes both halves, the
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ fine and still gets bounced.
|
||||||
swarmctl user add <you> --group admins
|
swarmctl user add <you> --group admins
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
`admins` deliberately, not a new word: [`../getting-started/setup.md`](../getting-started/setup.md) has
|
`admins` deliberately, not a new word: [`../getting-started/setup.md`](../getting-started/setup.md) has
|
||||||
told every operator to create exactly that group since the bootstrap step
|
told every operator to create exactly that group since the bootstrap step
|
||||||
existed, so an account made by following the guide already passes. This
|
existed, so an account made by following the guide already passes. This
|
||||||
|
|
@ -45,6 +47,8 @@ is the first rule that _consumes_ a group name — inventing a second one
|
||||||
would have meant those accounts silently failing a check they were
|
would have meant those accounts silently failing a check they were
|
||||||
supposed to pass.
|
supposed to pass.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
An account created without any group needs re-adding with the flag —
|
An account created without any group needs re-adding with the flag —
|
||||||
`swarmctl` reads the existing entry out of `users.yml,` so the group is
|
`swarmctl` reads the existing entry out of `users.yml,` so the group is
|
||||||
what changes.
|
what changes.
|
||||||
|
|
@ -73,6 +77,8 @@ a container":
|
||||||
| vhost + `gateway.localNames` | the service's own module (for example `nix/host-modules/swarm-ui.nix`) |
|
| vhost + `gateway.localNames` | the service's own module (for example `nix/host-modules/swarm-ui.nix`) |
|
||||||
| **certificate name** | `nix/host-modules/swarm.nix` (`serviceDomains`) |
|
| **certificate name** | `nix/host-modules/swarm.nix` (`serviceDomains`) |
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
The DNS record and the local-dev `/etc/hosts` entry need no separate
|
The DNS record and the local-dev `/etc/hosts` entry need no separate
|
||||||
edit: both derive from `services.hyperhive.gateway.localNames`,
|
edit: both derive from `services.hyperhive.gateway.localNames`,
|
||||||
which a service's own module already has to push its domain into to be
|
which a service's own module already has to push its domain into to be
|
||||||
|
|
@ -82,6 +88,8 @@ the surface the hive's own domain serves (dashboard, per-agent routing,
|
||||||
matrix discovery); a swarm service declares its own vhost next to its
|
matrix discovery); a swarm service declares its own vhost next to its
|
||||||
own options, the way `swarm-ui.nix` and `swarm-authelia.nix` do.
|
own options, the way `swarm-ui.nix` and `swarm-authelia.nix` do.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
⚠️ The certificate one is the hardest to predict and the most visible when
|
⚠️ The certificate one is the hardest to predict and the most visible when
|
||||||
missed. `serviceDomains` is _both_ the services sub-CA's
|
missed. `serviceDomains` is _both_ the services sub-CA's
|
||||||
`nameConstraints` set and the leaf's SAN list, and the apex is a
|
`nameConstraints` set and the leaf's SAN list, and the apex is a
|
||||||
|
|
|
||||||
|
|
@ -142,10 +142,12 @@ field; `pr status` when the only question is "can I merge it."
|
||||||
|
|
||||||
### Contributing to a read-only repo (`clone` + `pr create --agit`)
|
### Contributing to a read-only repo (`clone` + `pr create --agit`)
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
Agents are read-only collaborators on some repos (e.g.
|
Agents are read-only collaborators on some repos (e.g.
|
||||||
`internal/knowledge`) and so can't push branches. Forgejo's AGit flow
|
`internal/knowledge`) and so can't push branches. Forgejo's AGit flow
|
||||||
lets a read-only user open a PR by pushing the current `HEAD` to the
|
lets a read-only user open a PR by pushing the current `HEAD` to the
|
||||||
magic ref `refs/for/<base>/<topic>`. Two verbs cover the workflow:
|
magic ref `refs/for/<base>/<topic>`. Two verbs cover the workflow:
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
```
|
```
|
||||||
hive-forge -r internal/knowledge clone # clone, auth handled for you
|
hive-forge -r internal/knowledge clone # clone, auth handled for you
|
||||||
|
|
@ -158,6 +160,7 @@ hive-forge -r internal/knowledge pr create --agit \
|
||||||
[--body "details"] # PR description (also accepts --body-file)
|
[--body "details"] # PR description (also accepts --body-file)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
`clone` derives the dest dir from the repo basename (override with a
|
`clone` derives the dest dir from the repo basename (override with a
|
||||||
positional arg); it passes `--branch` / `--depth` through. The clone
|
positional arg); it passes `--branch` / `--depth` through. The clone
|
||||||
URL and the `origin` remote it leaves behind carry no credentials —
|
URL and the `origin` remote it leaves behind carry no credentials —
|
||||||
|
|
@ -166,6 +169,7 @@ URL and the `origin` remote it leaves behind carry no credentials —
|
||||||
hand), which git calls fresh on every fetch/push. That's what lets
|
hand), which git calls fresh on every fetch/push. That's what lets
|
||||||
`pr create --agit` (default remote `origin`) push without re-auth,
|
`pr create --agit` (default remote `origin`) push without re-auth,
|
||||||
without a durable token sitting in the checkout's `.git/config`.
|
without a durable token sitting in the checkout's `.git/config`.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
`pr create --agit` prints the PR URL. Re-running with the same
|
`pr create --agit` prints the PR URL. Re-running with the same
|
||||||
`--topic` force-updates the existing open PR (the AGit ref is
|
`--topic` force-updates the existing open PR (the AGit ref is
|
||||||
|
|
@ -244,6 +248,7 @@ to discover valid label names before triaging or to audit the label set.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
- **Read-before-comment guard:** `comment` refuses to post when forge still
|
- **Read-before-comment guard:** `comment` refuses to post when forge still
|
||||||
has an unread notification for the thread (that is, someone commented since
|
has an unread notification for the thread (that is, someone commented since
|
||||||
you last read it). This prevents accidentally replying to old activity without
|
you last read it). This prevents accidentally replying to old activity without
|
||||||
|
|
@ -341,3 +346,4 @@ to discover valid label names before triaging or to audit the label set.
|
||||||
response's `X-Total-Count` (`… 50 of 187 shown … 137 more`), so trust
|
response's `X-Total-Count` (`… 50 of 187 shown … 137 more`), so trust
|
||||||
the trailer, not the row count, when deciding whether you've seen
|
the trailer, not the row count, when deciding whether you've seen
|
||||||
everything.
|
everything.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,8 @@ hivectl matrix invite @mara:server --room '#hive-chat:server' # ...or to a spec
|
||||||
|
|
||||||
## GitHub
|
## GitHub
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
Write an operator-supplied GitHub personal access token (PAT) into an
|
Write an operator-supplied GitHub personal access token (PAT) into an
|
||||||
agent's token file so its `gh` wrapper + git credential helper can act as
|
agent's token file so its `gh` wrapper + git credential helper can act as
|
||||||
the bot account. Unlike forge/matrix there is no account creation — the PAT
|
the bot account. Unlike forge/matrix there is no account creation — the PAT
|
||||||
|
|
@ -99,6 +101,8 @@ is for an existing GitHub account. A CLI alternative to the dashboard
|
||||||
credentials tab; the [GitHub integration](../integrations/github.md) is on by default
|
credentials tab; the [GitHub integration](../integrations/github.md) is on by default
|
||||||
(`services.hyperhive.agent.github.enable`), so no per-agent config is needed.
|
(`services.hyperhive.agent.github.enable`), so no per-agent config is needed.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
hivectl github set-token damocles --token-stdin # paste the PAT on stdin (preferred)
|
hivectl github set-token damocles --token-stdin # paste the PAT on stdin (preferred)
|
||||||
hivectl github set-token damocles --token <pat> # inline (visible in shell history)
|
hivectl github set-token damocles --token <pat> # inline (visible in shell history)
|
||||||
|
|
@ -242,8 +246,8 @@ session id — the flag name deliberately matches the claude flag it maps
|
||||||
to. (choom never uses claude's `--continue`: that's a bare flag that
|
to. (choom never uses claude's `--continue`: that's a bare flag that
|
||||||
takes no argument and resumes the cwd's _latest_ session, that is, the
|
takes no argument and resumes the cwd's _latest_ session, that is, the
|
||||||
harness's; claude would consume a value after it as the first prompt,
|
harness's; claude would consume a value after it as the first prompt,
|
||||||
silently poking the live harness session.) A value is required when you
|
silently poking the live harness session.) Passing the flag requires a
|
||||||
pass the flag. Either way choom never collides with the harness's live
|
value. Either way choom never collides with the harness's live
|
||||||
session in the same project dir: the harness pins its own id via
|
session in the same project dir: the harness pins its own id via
|
||||||
`--resume`, so a blank choom session is invisible to it. The container
|
`--resume`, so a blank choom session is invisible to it. The container
|
||||||
must be running.
|
must be running.
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ room you haven't read yet.
|
||||||
|
|
||||||
### Room membership
|
### Room membership
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
- `invite_user(room, user_id)` — invite `@user:server` into a room
|
- `invite_user(room, user_id)` — invite `@user:server` into a room
|
||||||
you're already in; you must have a high enough power level.
|
you're already in; you must have a high enough power level.
|
||||||
The invitee sees a pending invite and resolves it via
|
The invitee sees a pending invite and resolves it via
|
||||||
|
|
@ -66,6 +67,7 @@ room you haven't read yet.
|
||||||
invites (it can reject too).
|
invites (it can reject too).
|
||||||
- `list_invites()` — rooms this agent has been invited to but not
|
- `list_invites()` — rooms this agent has been invited to but not
|
||||||
yet joined (`{ id, canonical_alias, name }` per room).
|
yet joined (`{ id, canonical_alias, name }` per room).
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
### Receipts
|
### Receipts
|
||||||
|
|
||||||
|
|
@ -73,6 +75,7 @@ room you haven't read yet.
|
||||||
|
|
||||||
## Multiple accounts
|
## Multiple accounts
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
`services.hyperhive.agent.matrixAccounts` (declared in `agent.nix`) is
|
`services.hyperhive.agent.matrixAccounts` (declared in `agent.nix`) is
|
||||||
the agent's full set of matrix identities — for example an
|
the agent's full set of matrix identities — for example an
|
||||||
external-facing account alongside the internal one — **and, being
|
external-facing account alongside the internal one — **and, being
|
||||||
|
|
@ -89,6 +92,7 @@ here are the *further* accounts. hive-c0re pins its `tokenFile` to
|
||||||
`<state>/matrix-token` and provisions it there, and the
|
`<state>/matrix-token` and provisions it there, and the
|
||||||
dashboard's link-account route refuses to create an account by that
|
dashboard's link-account route refuses to create an account by that
|
||||||
name.
|
name.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
`main` is present exactly when that URL is non-null, which is the whole
|
`main` is present exactly when that URL is non-null, which is the whole
|
||||||
mechanism behind "accounts are the enable signal": an agent with no
|
mechanism behind "accounts are the enable signal": an agent with no
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,15 @@ cancel.
|
||||||
|
|
||||||
### `request_schedule_prompt(targets, body, first_fire_at_unix, interval_seconds?, description?)`
|
### `request_schedule_prompt(targets, body, first_fire_at_unix, interval_seconds?, description?)`
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
Queue an operator-approval for a scheduled prompt. On approve,
|
Queue an operator-approval for a scheduled prompt. On approve,
|
||||||
hive-c0re fans `body` out to each agent in `targets` at
|
hive-c0re fans `body` out to each agent in `targets` at
|
||||||
`first_fire_at_unix` (Unix timestamp). Recurring when `interval_seconds`
|
`first_fire_at_unix` (Unix timestamp). Recurring when `interval_seconds`
|
||||||
is set, one-shot otherwise.
|
is set, one-shot otherwise.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Catch-up clamp: if hive-c0re is down across multiple intervals, only
|
Catch-up clamp: if hive-c0re is down across multiple intervals, only
|
||||||
ONE delayed fire happens on resume (per recurring schedule). The
|
ONE delayed fire happens on resume (per recurring schedule). The
|
||||||
skipped-cycle count surfaces in the per-target `last_result` for
|
skipped-cycle count surfaces in the per-target `last_result` for
|
||||||
|
|
@ -41,10 +45,14 @@ rows (terminal state).
|
||||||
|
|
||||||
### `cancel_schedule(id, targets?)`
|
### `cancel_schedule(id, targets?)`
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
Cancel a schedule. Omit `targets` / pass empty to cancel the whole
|
Cancel a schedule. Omit `targets` / pass empty to cancel the whole
|
||||||
schedule; pass a list to cancel just those recipients (the schedule
|
schedule; pass a list to cancel just those recipients (the schedule
|
||||||
autocancels once every target is gone).
|
autocancels once every target is gone).
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
### `fire_schedule_now(id)`
|
### `fire_schedule_now(id)`
|
||||||
|
|
||||||
Fire a scheduled prompt out of band immediately. Recurring schedules
|
Fire a scheduled prompt out of band immediately. Recurring schedules
|
||||||
|
|
|
||||||
|
|
@ -144,12 +144,16 @@ no progress left to describe.
|
||||||
|
|
||||||
## A `continue` that finds no session
|
## A `continue` that finds no session
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
`continue` doesn't check for the session before spawning. claude's own
|
`continue` doesn't check for the session before spawning. claude's own
|
||||||
`--resume` is the authority, and it exits non-zero rather than quietly
|
`--resume` is the authority, and it exits non-zero rather than quietly
|
||||||
starting a fresh session, so the check could only duplicate the lookup
|
starting a fresh session, so the check could only duplicate the lookup
|
||||||
the driver was about to do — while answering as though the session were
|
the driver was about to do — while answering as though the session were
|
||||||
gone. The usual truth is that the session exists somewhere else.
|
gone. The usual truth is that the session exists somewhere else.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
`continue` waits for that answer instead. Where `start` returns the
|
`continue` waits for that answer instead. Where `start` returns the
|
||||||
instant the process exists — it creates its session, so the spawn
|
instant the process exists — it creates its session, so the spawn
|
||||||
succeeding is the whole story — a resumed turn can fail a moment _after_
|
succeeding is the whole story — a resumed turn can fail a moment _after_
|
||||||
|
|
@ -198,6 +202,8 @@ to report it, which is what `OOMPolicy=continue` on the unit is for.
|
||||||
|
|
||||||
## Compaction trade-off
|
## Compaction trade-off
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
Built on `hive_claude::Claude::spawn` + `RunningClaude::wait` directly
|
Built on `hive_claude::Claude::spawn` + `RunningClaude::wait` directly
|
||||||
rather than `InfiniteSession::run`, since only the low-level driver
|
rather than `InfiniteSession::run`, since only the low-level driver
|
||||||
exposes a cancel handle to stop a turn mid-flight — that's what makes
|
exposes a cancel handle to stop a turn mid-flight — that's what makes
|
||||||
|
|
@ -208,6 +214,8 @@ Subagents are meant to be bounded, single-batch work, not sessions
|
||||||
long-lived enough to need in-place compaction — a real follow-up if that
|
long-lived enough to need in-place compaction — a real follow-up if that
|
||||||
assumption stops holding.
|
assumption stops holding.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
`services.hyperhive.agent.mcp.subagentHttpPort` — the daemon's streamable-http listen
|
`services.hyperhive.agent.mcp.subagentHttpPort` — the daemon's streamable-http listen
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,9 @@ Run a LogsQL query and print the matched log lines
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
<small><i>
|
<small><i>
|
||||||
This document was generated automatically by
|
This document was generated automatically by
|
||||||
<a href="https://crates.io/crates/clap-markdown"><code>clap-markdown</code></a>.
|
<a href="https://crates.io/crates/clap-markdown"><code>clap-markdown</code></a>.
|
||||||
</i></small>
|
</i></small>
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
# The operator/agent boundary
|
# The operator/agent boundary
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
Design rationale for hyperhive's two-principal trust model. The
|
Design rationale for hyperhive's two-principal trust model. The
|
||||||
_implementation_ work — container network isolation, the unifying
|
_implementation_ work — container network isolation, the unifying
|
||||||
gateway, core-daemon privsep — is tracked as `area:ops` issues on
|
gateway, core-daemon privsep — is tracked as `area:ops` issues on
|
||||||
the forge.
|
the forge.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
The operator/agent boundary is technically enforced, not just a
|
The operator/agent boundary is technically enforced, not just a
|
||||||
convention: containers run in private netns (network isolation is
|
convention: containers run in private netns (network isolation is
|
||||||
always on), the gateway proxies all operator-facing traffic, and
|
always on), the gateway proxies all operator-facing traffic, and
|
||||||
|
|
@ -109,6 +113,8 @@ The last two land in "other," so the dir is **`0751`, owned by the
|
||||||
agent's container uid/gid** — `o=--x` is traverse without listing, and
|
agent's container uid/gid** — `o=--x` is traverse without listing, and
|
||||||
both sockets are `0666`, which is all a dialer needs.
|
both sockets are `0666`, which is all a dialer needs.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
**Ownership is declared, not repaired.** The tmpfiles.d entry written by
|
**Ownership is declared, not repaired.** The tmpfiles.d entry written by
|
||||||
`SyncAgentTmpfiles` names the uid/gid directly. Don't add a chown
|
`SyncAgentTmpfiles` names the uid/gid directly. Don't add a chown
|
||||||
alongside it: `d` re-applies on every boot _and_ every agent
|
alongside it: `d` re-applies on every boot _and_ every agent
|
||||||
|
|
@ -116,6 +122,8 @@ spawn/destroy, and reverts any ownership set afterwards the next time
|
||||||
any agent changes — which is exactly how this dir spent a long time at
|
any agent changes — which is exactly how this dir spent a long time at
|
||||||
`0777 root root` while a privileged chown appeared to be fixing it.
|
`0777 root root` while a privileged chown appeared to be fixing it.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
The mode is load-bearing, not cosmetic. Write permission on a
|
The mode is load-bearing, not cosmetic. Write permission on a
|
||||||
_directory_ is what confers the right to unlink its entries, whoever owns
|
_directory_ is what confers the right to unlink its entries, whoever owns
|
||||||
them, and the sticky bit is the only thing that would restrain that (it
|
them, and the sticky bit is the only thing that would restrain that (it
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ agent capability X?."
|
||||||
|
|
||||||
### The trust boundary is the container, not credential storage
|
### The trust boundary is the container, not credential storage
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
An agent is **trusted code running inside its own nspawn container**. The
|
An agent is **trusted code running inside its own nspawn container**. The
|
||||||
boundary that matters is the container: a sub-agent can't see the host
|
boundary that matters is the container: a sub-agent can't see the host
|
||||||
netns, another agent's container, or another agent's state dir. Within its
|
netns, another agent's container, or another agent's state dir. Within its
|
||||||
|
|
@ -22,10 +24,16 @@ below stops a build's nixbld user reading the agent's own forge token, and the
|
||||||
state-file endpoint stops one agent proxying another's files. Those harden the
|
state-file endpoint stops one agent proxying another's files. Those harden the
|
||||||
boundary; they don't sandbox the agent from itself.)
|
boundary; they don't sandbox the agent from itself.)
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
The corollary: **don't reason about security as "can the agent be stopped from
|
The corollary: **don't reason about security as "can the agent be stopped from
|
||||||
touching its credentials." Reason about it as "what's the blast radius if this
|
touching its credentials." Reason about it as "what's the blast radius if this
|
||||||
agent does the worst possible thing with everything it can reach."**
|
agent does the worst possible thing with everything it can reach."**
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
### Scoped tokens bound the blast radius
|
### Scoped tokens bound the blast radius
|
||||||
|
|
||||||
Each agent gets its own scoped credentials, never shared:
|
Each agent gets its own scoped credentials, never shared:
|
||||||
|
|
@ -34,10 +42,14 @@ Each agent gets its own scoped credentials, never shared:
|
||||||
collaborator grants; can't act as another agent or as `core`).
|
collaborator grants; can't act as another agent or as `core`).
|
||||||
- **matrix token** → that agent's matrix account only.
|
- **matrix token** → that agent's matrix account only.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
_Its own_ account's scope bounds a compromised/confused agent's reach on the
|
_Its own_ account's scope bounds a compromised/confused agent's reach on the
|
||||||
forge or matrix, not the swarm's. This is the main thing standing
|
forge or matrix, not the swarm's. This is the main thing standing
|
||||||
between "one agent does something dumb" and "the whole hive is affected."
|
between "one agent does something dumb" and "the whole hive is affected."
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
**Identity vs. secret (matrix).** The scoping is on the _secret_, not the
|
**Identity vs. secret (matrix).** The scoping is on the _secret_, not the
|
||||||
_identity_: an agent's matrix **token** is private to its own account, but its
|
_identity_: an agent's matrix **token** is private to its own account, but its
|
||||||
matrix **identities** — the public handles (`name`, `user_id` `@user:server`,
|
matrix **identities** — the public handles (`name`, `user_id` `@user:server`,
|
||||||
|
|
@ -222,7 +234,7 @@ token policy bounds file reads; network isolation bounds network reach.
|
||||||
The proper fix is to enable user namespaces inside nspawn containers
|
The proper fix is to enable user namespaces inside nspawn containers
|
||||||
(`--private-users=inherit` in `EXTRA_NSPAWN_FLAGS`) so nix can set up its real
|
(`--private-users=inherit` in `EXTRA_NSPAWN_FLAGS`) so nix can set up its real
|
||||||
sandbox and `sandbox-fallback` becomes a true last resort. This requires verifying
|
sandbox and `sandbox-fallback` becomes a true last resort. This requires verifying
|
||||||
bind-mount compatibility with user namespace UID mapping and is tracked as a TODO.
|
bind-mount compatibility with user namespace UID mapping first.
|
||||||
|
|
||||||
## hive-c0re privilege separation
|
## hive-c0re privilege separation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,8 @@ hyperhive owns compaction as policy: the `hive_claude::InfiniteSession`
|
||||||
keeps the session alive across the context window with two triggers baked
|
keeps the session alive across the context window with two triggers baked
|
||||||
into its `run`:
|
into its `run`:
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
- **Reactive** — claude-code prints `Prompt is too long`. The session is
|
- **Reactive** — claude-code prints `Prompt is too long`. The session is
|
||||||
_already_ past the window, so no turn can run on it — the session
|
_already_ past the window, so no turn can run on it — the session
|
||||||
`/compact`s straight away and retries the same wake-up prompt once. No
|
`/compact`s straight away and retries the same wake-up prompt once. No
|
||||||
|
|
@ -128,6 +130,7 @@ into its `run`:
|
||||||
"context is filling up, flush durable state into `/state` now") and
|
"context is filling up, flush durable state into `/state` now") and
|
||||||
_then_ `/compact`s, so the agent can persist in-flight state before the
|
_then_ `/compact`s, so the agent can persist in-flight state before the
|
||||||
detail collapses into a summary.
|
detail collapses into a summary.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
**hive-agent checks both triggers above between its own turns** —
|
**hive-agent checks both triggers above between its own turns** —
|
||||||
`PercentPolicy::should_compact` reads the percent watermark from the
|
`PercentPolicy::should_compact` reads the percent watermark from the
|
||||||
|
|
@ -236,8 +239,8 @@ needs to `chown` a bind mount), once at startup:
|
||||||
- `claude-system-prompt.md` — rendered from
|
- `claude-system-prompt.md` — rendered from
|
||||||
`hive-agent/prompts/system.md` by `hive-agent`'s `prompt::render`:
|
`hive-agent/prompts/system.md` by `hive-agent`'s `prompt::render`:
|
||||||
HTML-comment markers (`<!-- role:agent -->...<!-- /role:agent -->`,
|
HTML-comment markers (`<!-- role:agent -->...<!-- /role:agent -->`,
|
||||||
same for `role:manager`) gate the role-specific blocks; everything
|
same for `role:manager`) gate the role-specific blocks; both flavors
|
||||||
else is shared. Five placeholders are then
|
share everything else. Five placeholders are then
|
||||||
substituted: `{label}` (short agent name), `{qualified_label}`
|
substituted: `{label}` (short agent name), `{qualified_label}`
|
||||||
(hive-qualified `name@domain` form), `{operator_pronouns}`,
|
(hive-qualified `name@domain` form), `{operator_pronouns}`,
|
||||||
`{hive_identity}` (for example `` on hive `pr1ma` ``; empty when
|
`{hive_identity}` (for example `` on hive `pr1ma` ``; empty when
|
||||||
|
|
@ -245,7 +248,7 @@ needs to `chown` a bind mount), once at startup:
|
||||||
shape for the swarm). Pronouns come from `HIVE_OPERATOR_PRONOUNS`
|
shape for the swarm). Pronouns come from `HIVE_OPERATOR_PRONOUNS`
|
||||||
env (set by the meta flake from
|
env (set by the meta flake from
|
||||||
`services.hyperhive.c0re.operatorPronouns`, default `she/her`).
|
`services.hyperhive.c0re.operatorPronouns`, default `she/her`).
|
||||||
When `services.hyperhive.agent.docs.enable` is set, `HIVE_DOCS_DIR` is present
|
When you enable `services.hyperhive.agent.docs.enable`, `HIVE_DOCS_DIR` is present
|
||||||
in the environment and `render()` appends a one-sentence pointer
|
in the environment and `render()` appends a one-sentence pointer
|
||||||
telling the agent where the docs live.
|
telling the agent where the docs live.
|
||||||
Passed via `--system-prompt-file`.
|
Passed via `--system-prompt-file`.
|
||||||
|
|
|
||||||
|
|
@ -288,6 +288,8 @@ The file lives in the agent's bind-mounted **harness** dir, not `state/`
|
||||||
(see `docs/agent-lifecycle/persistence.md`'s "Parent access to child state"), so an API
|
(see `docs/agent-lifecycle/persistence.md`'s "Parent access to child state"), so an API
|
||||||
key placed here is reachable by nothing but this agent and the host.
|
key placed here is reachable by nothing but this agent and the host.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
⚠️ Verified end-to-end against OpenRouter hasn't happened as of this
|
⚠️ Verified end-to-end against OpenRouter hasn't happened as of this
|
||||||
writing — `ANTHROPIC_BASE_URL` support in the shipped Claude CLI is
|
writing — `ANTHROPIC_BASE_URL` support in the shipped Claude CLI is
|
||||||
documented behavior, not something this hive has run a live turn
|
documented behavior, not something this hive has run a live turn
|
||||||
|
|
@ -295,6 +297,8 @@ against yet. Tool use, streaming, and MCP all need to keep working
|
||||||
through a non-Anthropic base URL; treat the first real agent on this
|
through a non-Anthropic base URL; treat the first real agent on this
|
||||||
path as the actual verification, not this doc.
|
path as the actual verification, not this doc.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Switching an already-provisioned OAuth agent to `useApiKey` leaves
|
Switching an already-provisioned OAuth agent to `useApiKey` leaves
|
||||||
`~/.claude/credentials.json` in place but unused — harmless, not
|
`~/.claude/credentials.json` in place but unused — harmless, not
|
||||||
cleaned up automatically. Cost shape also changes: subscription pricing
|
cleaned up automatically. Cost shape also changes: subscription pricing
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ moment any row of badges/pills wrapped onto an extra line at some
|
||||||
viewport width; measuring instead of guessing closes that bug class
|
viewport width; measuring instead of guessing closes that bug class
|
||||||
structurally rather than for one specific trigger. Two columns:
|
structurally rather than for one specific trigger. Two columns:
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
- **Agent icon** (`<img class="agent-icon">`): fixed-size square
|
- **Agent icon** (`<img class="agent-icon">`): fixed-size square
|
||||||
identity anchor — `width: 5em; height: 5em` with explicit pixel
|
identity anchor — `width: 5em; height: 5em` with explicit pixel
|
||||||
sizing so the `<img>`'s intrinsic (large) dimensions don't push
|
sizing so the `<img>`'s intrinsic (large) dimensions don't push
|
||||||
|
|
@ -100,6 +101,7 @@ structurally rather than for one specific trigger. Two columns:
|
||||||
- No header cancel-turn button any more — `/cancel` (slash command,
|
- No header cancel-turn button any more — `/cancel` (slash command,
|
||||||
below) is the only path; the turn-loop state badge already shows
|
below) is the only path; the turn-loop state badge already shows
|
||||||
`thinking` as the discoverability cue.
|
`thinking` as the discoverability cue.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Values throughout come from `GET /api/state`'s cold-load snapshot,
|
Values throughout come from `GET /api/state`'s cold-load snapshot,
|
||||||
kept in sync afterwards by the SSE stream (see Live view below) —
|
kept in sync afterwards by the SSE stream (see Live view below) —
|
||||||
|
|
@ -113,6 +115,7 @@ so two tabs from different hives are distinguishable; the header's own
|
||||||
**Main content** (`<main class="agent-main">`): fills the viewport
|
**Main content** (`<main class="agent-main">`): fills the viewport
|
||||||
and scrolls behind the fixed header + footer.
|
and scrolls behind the fixed header + footer.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
- `#status` overlay: empty when online; shows the login form / OAuth
|
- `#status` overlay: empty when online; shows the login form / OAuth
|
||||||
URL when `status` is `needs_login_*`. The OAuth code input is
|
URL when `status` is `needs_login_*`. The OAuth code input is
|
||||||
`type="password"` with a `👁 reveal` toggle that flips it back to
|
`type="password"` with a `👁 reveal` toggle that flips it back to
|
||||||
|
|
@ -135,6 +138,7 @@ and scrolls behind the fixed header + footer.
|
||||||
composites above the composer. Geometry is unchanged —
|
composites above the composer. Geometry is unchanged —
|
||||||
`.agent-main` and `.terminal-wrap` both `inset: 0` fill the same
|
`.agent-main` and `.terminal-wrap` both `inset: 0` fill the same
|
||||||
area.
|
area.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
## Footer / composer
|
## Footer / composer
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -310,9 +310,11 @@ Three sub-tabs:
|
||||||
|
|
||||||
### MATRIX tab
|
### MATRIX tab
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
Provision / log in a per-agent **external** matrix account and store its
|
Provision / log in a per-agent **external** matrix account and store its
|
||||||
access token (this half is unchanged from the old `/matrix-accounts.html`
|
access token (this half is unchanged from the old `/matrix-accounts.html`
|
||||||
page it replaces — only the URL and surrounding chrome moved).
|
page it replaces — only the URL and surrounding chrome moved).
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
An agent picker (populated from `state.containers`, the live roster) drives a list of that
|
An agent picker (populated from `state.containers`, the live roster) drives a list of that
|
||||||
agent's accounts — name, homeserver, user id, and a status dot —
|
agent's accounts — name, homeserver, user id, and a status dot —
|
||||||
|
|
@ -447,6 +449,7 @@ boxes only stages the change in-browser; the browser writes nothing until
|
||||||
the operator selects the page-level **save all** button (described below). Row
|
the operator selects the page-level **save all** button (described below). Row
|
||||||
values follow the `effective`/`assignments` rule described above.
|
values follow the `effective`/`assignments` rule described above.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
**T00L GR0UPS** — per-agent tool-group permissions. Tool groups are
|
**T00L GR0UPS** — per-agent tool-group permissions. Tool groups are
|
||||||
named buckets of MCP tools; each agent starts with a role default
|
named buckets of MCP tools; each agent starts with a role default
|
||||||
(sub-agents: `messaging`, `meta`, `inbox`, `execution` —
|
(sub-agents: `messaging`, `meta`, `inbox`, `execution` —
|
||||||
|
|
@ -458,6 +461,7 @@ unchecking stages which groups are active for the agent; the
|
||||||
page-level **save all** button (below) commits it. Columns come from
|
page-level **save all** button (below) commits it. Columns come from
|
||||||
`GET /api/tool-groups`. hive-c0re queues a rebuild so `HIVE_TOOL_GROUPS`
|
`GET /api/tool-groups`. hive-c0re queues a rebuild so `HIVE_TOOL_GROUPS`
|
||||||
takes effect.
|
takes effect.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
The current tool groups are: `messaging`, `meta`, `inbox`, `lifecycle`,
|
The current tool groups are: `messaging`, `meta`, `inbox`, `lifecycle`,
|
||||||
`approvals`, `scheduling`, `forge`, `execution`,
|
`approvals`, `scheduling`, `forge`, `execution`,
|
||||||
|
|
@ -746,6 +750,7 @@ falls back to the dimmed hyperhive mark (`/favicon.svg`). The card
|
||||||
body sits to the right with three stacked lines
|
body sits to the right with three stacked lines
|
||||||
(`assets/swarm.js::renderContainers`).
|
(`assets/swarm.js::renderContainers`).
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
**Icon layout + load strategy:** the `<img>` is absolutely
|
**Icon layout + load strategy:** the `<img>` is absolutely
|
||||||
positioned (`inset: 0`) inside the `.container-icon` wrapper —
|
positioned (`inset: 0`) inside the `.container-icon` wrapper —
|
||||||
the wrapper is the flex child and sizes itself via `width: 5em` +
|
the wrapper is the flex child and sizes itself via `width: 5em` +
|
||||||
|
|
@ -762,7 +767,9 @@ the dashboard itself, always reachable) and adds the
|
||||||
already knows the container is stopped (`ContainerView.running = false`)
|
already knows the container is stopped (`ContainerView.running = false`)
|
||||||
the fallback fires immediately, skipping the doomed `<url>/icon`
|
the fallback fires immediately, skipping the doomed `<url>/icon`
|
||||||
fetch entirely.
|
fetch entirely.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
**Line 1** — agent name (link → new tab), m1nd/ag3nt chip, an
|
**Line 1** — agent name (link → new tab), m1nd/ag3nt chip, an
|
||||||
**icon-only nav strip** plus live agent-owned state, all populated
|
**icon-only nav strip** plus live agent-owned state, all populated
|
||||||
async from a single `GET /api/dashboard-state` call to the agent's
|
async from a single `GET /api/dashboard-state` call to the agent's
|
||||||
|
|
@ -793,7 +800,9 @@ that's unset — never guessed from `<host>:3000`. External links are
|
||||||
already absolute. The same base URL drives the primary agent-name
|
already absolute. The same base URL drives the primary agent-name
|
||||||
link + favicon fetch, so the whole row routes through the gateway
|
link + favicon fetch, so the whole row routes through the gateway
|
||||||
as a unit.
|
as a unit.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
**When the container is stopped** (`ContainerView.running = false`),
|
**When the container is stopped** (`ContainerView.running = false`),
|
||||||
the dashboard skips the async `dashboard-state` fetch entirely (the agent
|
the dashboard skips the async `dashboard-state` fetch entirely (the agent
|
||||||
web server is down), so a single badge replaces the badge chain,
|
web server is down), so a single badge replaces the badge chain,
|
||||||
|
|
@ -803,6 +812,7 @@ badges disappear. The agent icon goes straight to the dimmed
|
||||||
container's URL. Static fields — `needs_update`, `deployed_sha`,
|
container's URL. Static fields — `needs_update`, `deployed_sha`,
|
||||||
`pending_reminders`, `parent`, `config` link — remain visible
|
`pending_reminders`, `parent`, `config` link — remain visible
|
||||||
regardless of run state.
|
regardless of run state.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
**Line 2** — status badges only (no per-card action buttons — actions
|
**Line 2** — status badges only (no per-card action buttons — actions
|
||||||
moved to the **selection bar** or the **per-agent `⋮` menu**, see
|
moved to the **selection bar** or the **per-agent `⋮` menu**, see
|
||||||
|
|
@ -1010,6 +1020,7 @@ When the operator selects one or more agents (via the icon), a sticky
|
||||||
frosted-mauve bar slides up from the bottom of the viewport
|
frosted-mauve bar slides up from the bottom of the viewport
|
||||||
(`#selection-bar`, `position: fixed; bottom: 0`). It shows:
|
(`#selection-bar`, `position: fixed; bottom: 0`). It shows:
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
- **Count + names** — "N agents selected · name1, name2, …"
|
- **Count + names** — "N agents selected · name1, name2, …"
|
||||||
- **Bulk action buttons** — only enabled when ALL selected agents
|
- **Bulk action buttons** — only enabled when ALL selected agents
|
||||||
support the action; disabled with a tooltip naming the blockers
|
support the action; disabled with a tooltip naming the blockers
|
||||||
|
|
@ -1037,6 +1048,7 @@ frosted-mauve bar slides up from the bottom of the viewport
|
||||||
- **multiple agents** → `POST /api/topology/set-parent-bulk`
|
- **multiple agents** → `POST /api/topology/set-parent-bulk`
|
||||||
(JSON `[{ child, new_parent }]`; all moves land in a single
|
(JSON `[{ child, new_parent }]`; all moves land in a single
|
||||||
`topology.json` commit instead of one per agent)
|
`topology.json` commit instead of one per agent)
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Both write `topology.json` and re-emit a container snapshot so
|
Both write `topology.json` and re-emit a container snapshot so
|
||||||
the tree repaints without a page reload.
|
the tree repaints without a page reload.
|
||||||
|
|
@ -1061,6 +1073,7 @@ renderApprovals`) with three stacked sections:
|
||||||
| `schedule_prompt` | `⏱` | `schedule` | — |
|
| `schedule_prompt` | `⏱` | `schedule` | — |
|
||||||
| `spawn` | `⊕` | `spawn` | — |
|
| `spawn` | `⊕` | `spawn` | — |
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
The chip ticks live every second via a `data-requested-at`
|
The chip ticks live every second via a `data-requested-at`
|
||||||
attribute + client-side interval (no re-render). Turns amber once
|
attribute + client-side interval (no re-render). Turns amber once
|
||||||
the request has been pending ≥ 1h so a stale approval stands out;
|
the request has been pending ≥ 1h so a stale approval stands out;
|
||||||
|
|
@ -1076,6 +1089,7 @@ renderApprovals`) with three stacked sections:
|
||||||
- **decision actions** — `◆ APPR0VE` and `DENY`. Deny pops a
|
- **decision actions** — `◆ APPR0VE` and `DENY`. Deny pops a
|
||||||
`prompt()` for an optional reason carried to the submitting agent as
|
`prompt()` for an optional reason carried to the submitting agent as
|
||||||
`HelperEvent::ApprovalResolved.note`.
|
`HelperEvent::ApprovalResolved.note`.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
A `pending · N` / `history · N` tab pair switches the section
|
A `pending · N` / `history · N` tab pair switches the section
|
||||||
between the live queue and the last 30 resolved approvals.
|
between the live queue and the last 30 resolved approvals.
|
||||||
|
|
@ -1091,6 +1105,7 @@ The toggle controls live in the Y3R C4LL tab's ◆ PR3F3R3NC3S ◆
|
||||||
section; see that section above for the user-facing shape. Dispatch
|
section; see that section above for the user-facing shape. Dispatch
|
||||||
logic lives in `common.js::NOTIF`.
|
logic lives in `common.js::NOTIF`.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
First `/api/state` after page load seeds "seen" sets without
|
First `/api/state` after page load seeds "seen" sets without
|
||||||
firing — only items that arrive while the page is open count.
|
firing — only items that arrive while the page is open count.
|
||||||
Per-event tags (`hyperhive:approval:<id>`,
|
Per-event tags (`hyperhive:approval:<id>`,
|
||||||
|
|
@ -1105,6 +1120,7 @@ permission. Requires a secure context (HTTPS or localhost); on
|
||||||
other origins the controls hide themselves. Browsers typically
|
other origins the controls hide themselves. Browsers typically
|
||||||
suppress notifications while the originating tab is focused —
|
suppress notifications while the originating tab is focused —
|
||||||
that's a browser-level decision, not the dashboard's.
|
that's a browser-level decision, not the dashboard's.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
## Dashboard endpoints
|
## Dashboard endpoints
|
||||||
|
|
||||||
|
|
@ -1183,6 +1199,7 @@ below — some endpoints aren't in it yet.
|
||||||
bodies at 1 MiB. Selection-time backing for the inline path-link
|
bodies at 1 MiB. Selection-time backing for the inline path-link
|
||||||
preview.
|
preview.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
The broker forwarder detects which tokens *are* path links
|
The broker forwarder detects which tokens *are* path links
|
||||||
**server-side at broker-message ingest**, not client-side:
|
**server-side at broker-message ingest**, not client-side:
|
||||||
it calls `scan_validated_paths(body)` —
|
it calls `scan_validated_paths(body)` —
|
||||||
|
|
@ -1298,6 +1315,7 @@ below — some endpoints aren't in it yet.
|
||||||
- `GET /api/dashboard/history` — last ~200 broker messages
|
- `GET /api/dashboard/history` — last ~200 broker messages
|
||||||
(wrapped as `{ seq, events }`) for the message-flow
|
(wrapped as `{ seq, events }`) for the message-flow
|
||||||
terminal's backfill on page load.
|
terminal's backfill on page load.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
## Dashboard event channel
|
## Dashboard event channel
|
||||||
|
|
||||||
|
|
@ -1378,6 +1396,7 @@ payload):
|
||||||
`nix flake update` ripple starts or completes. BU1LDS M3T4 1NPUTS
|
`nix flake update` ripple starts or completes. BU1LDS M3T4 1NPUTS
|
||||||
tab uses this to show/hide the "⏳ meta-update running" banner.
|
tab uses this to show/hide the "⏳ meta-update running" banner.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
`/api/state` is **only fetched on cold-load and on the few
|
`/api/state` is **only fetched on cold-load and on the few
|
||||||
forms that mutate non-event-derived state** (PURG3 +
|
forms that mutate non-event-derived state** (PURG3 +
|
||||||
meta-update, since tombstones + meta_inputs aren't event-
|
meta-update, since tombstones + meta_inputs aren't event-
|
||||||
|
|
@ -1386,6 +1405,7 @@ transients, containers, operator inbox, message flow —
|
||||||
derives from `/api/dashboard/stream` after the initial snapshot,
|
derives from `/api/dashboard/stream` after the initial snapshot,
|
||||||
maintaining its own client-side store and applying events on
|
maintaining its own client-side store and applying events on
|
||||||
top. The 5s periodic poll is gone.
|
top. The 5s periodic poll is gone.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Generalised form helpers: `form[data-confirm="…"]` pops
|
Generalised form helpers: `form[data-confirm="…"]` pops
|
||||||
`confirm()` before submit; `form[data-prompt="…"]` pops
|
`confirm()` before submit; `form[data-prompt="…"]` pops
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,8 @@ home.js`, is the reference example — currently in the dashboard
|
||||||
|
|
||||||
## Motion
|
## Motion
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
**General rule, not case-by-case: every non-essential CSS animation
|
**General rule, not case-by-case: every non-essential CSS animation
|
||||||
gates on `prefers-reduced-motion` and pauses when its tab/section is
|
gates on `prefers-reduced-motion` and pauses when its tab/section is
|
||||||
hidden.** The dashboard's matrix-rain background (`packages/dashboard/
|
hidden.** The dashboard's matrix-rain background (`packages/dashboard/
|
||||||
|
|
@ -54,6 +56,8 @@ though it lives in the dashboard package rather than swarm-ui; every
|
||||||
future swarm-ui animation follows the same pattern, not just whimsy
|
future swarm-ui animation follows the same pattern, not just whimsy
|
||||||
pieces.
|
pieces.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Prefer CSS-driven animation over JS-driven where possible, and avoid
|
Prefer CSS-driven animation over JS-driven where possible, and avoid
|
||||||
jarring content swaps (layout shift, hard cuts) where a transition can
|
jarring content swaps (layout shift, hard cuts) where a transition can
|
||||||
smooth them instead.
|
smooth them instead.
|
||||||
|
|
@ -88,6 +92,8 @@ The governing question for anything that shows time-sensitive data:
|
||||||
needs a refresh story; a value that's silently gone stale with no way to
|
needs a refresh story; a value that's silently gone stale with no way to
|
||||||
tell is worse than one that's visibly stale.
|
tell is worse than one that's visibly stale.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
- **Relative-time labels must actually tick.** A label like "fresh (5s
|
- **Relative-time labels must actually tick.** A label like "fresh (5s
|
||||||
ago)" derives from a stored timestamp and re-renders itself on an
|
ago)" derives from a stored timestamp and re-renders itself on an
|
||||||
interval — never a value frozen at fetch time that quietly goes stale
|
interval — never a value frozen at fetch time that quietly goes stale
|
||||||
|
|
@ -100,6 +106,7 @@ tell is worse than one that's visibly stale.
|
||||||
Any polling component's contract needs to make this the caller's
|
Any polling component's contract needs to make this the caller's
|
||||||
problem to opt out of correctly, not something the next adopter
|
problem to opt out of correctly, not something the next adopter
|
||||||
discovers by shipping a bug.
|
discovers by shipping a bug.
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,8 @@ shapes the renderers call:
|
||||||
- `api.fromHistory` — `true` while backfill replay is running; renderers
|
- `api.fromHistory` — `true` while backfill replay is running; renderers
|
||||||
use it to suppress live-only side effects.
|
use it to suppress live-only side effects.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
**`create(opts)` options:** `logEl` (log container element), `streamUrl`
|
**`create(opts)` options:** `logEl` (log container element), `streamUrl`
|
||||||
(SSE endpoint), `historyUrl?` (optional replay endpoint — skipped if absent
|
(SSE endpoint), `historyUrl?` (optional replay endpoint — skipped if absent
|
||||||
or if the fetch fails), `renderers` (kind→fn map; unknown kinds fall through
|
or if the fetch fails), `renderers` (kind→fn map; unknown kinds fall through
|
||||||
|
|
@ -126,6 +128,8 @@ than captured once — lets a page default otherwise-collapsed panels open
|
||||||
per a live browser-local preference; renderers that force a row open
|
per a live browser-local preference; renderers that force a row open
|
||||||
regardless, for example message-bearing tool_use, are unaffected either way).
|
regardless, for example message-bearing tool_use, are unaffected either way).
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
**Sticky-bottom + snap animation.** `stickToBottom` is the
|
**Sticky-bottom + snap animation.** `stickToBottom` is the
|
||||||
operator's intent: true means "keep snapping to bottom on every
|
operator's intent: true means "keep snapping to bottom on every
|
||||||
mutation," false means "I scrolled up, leave me alone." The flag
|
mutation," false means "I scrolled up, leave me alone." The flag
|
||||||
|
|
@ -238,6 +242,8 @@ checkboxes use a lighter snapshot-then-restore pattern (snapshot
|
||||||
before `replaceChildren`, restore after) since they're single
|
before `replaceChildren`, restore after) since they're single
|
||||||
values rather than per-row caches.
|
values rather than per-row caches.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
**`<details>` open-state preservation:** any collapsible element
|
**`<details>` open-state preservation:** any collapsible element
|
||||||
tagged with `data-restore-key="<stable-key>"` survives the
|
tagged with `data-restore-key="<stable-key>"` survives the
|
||||||
refresh. `snapshotOpenDetails()` walks managed sections before
|
refresh. `snapshotOpenDetails()` walks managed sections before
|
||||||
|
|
@ -250,6 +256,8 @@ dashboard's own answered-questions history list was the only
|
||||||
nothing to restore, kept in place for the next collapsible section
|
nothing to restore, kept in place for the next collapsible section
|
||||||
that needs it rather than ripped out along with their one caller.
|
that needs it rather than ripped out along with their one caller.
|
||||||
|
|
||||||
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
**Side panel (dashboard):** long content opens in a drawer that
|
**Side panel (dashboard):** long content opens in a drawer that
|
||||||
swipes in from the right — a singleton `#side-panel` with a
|
swipes in from the right — a singleton `#side-panel` with a
|
||||||
titled header, a close button, and a scrollable body. Closes on
|
titled header, a close button, and a scrollable body. Closes on
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue