From cb504f6863c211319bf182dbf465b03a110a2ac3 Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 20 Sep 2026 00:25:56 +0200 Subject: [PATCH] docs: clear write-good.Passive hits in gateway/subagent/dashboard docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of #4548 — rewriting the 353 hand-written-docs write-good.Passive hits so #4546 can flip the rule to error. This batch: docs/networking/gateway.md (27 hits), docs/tools/subagent.md (19), docs/web-ui/dashboard.md (18). Minimal, meaning-preserving rewrites to active voice only — no restructuring beyond what each flagged sentence needed. Genuine false positives (predicate-adjective state descriptions like "is required", "is broken", "is unchanged", adjectival past participles like "named buckets", and one quoted literal UI string) are left in place; the per-line reasoning is on the tracking issue. Note: docs/tools/forge-cli.md (37 hits, would have been the single biggest file) turned out to be generated from hive-forge's own clap help text (see hive-forge/src/main.rs's `MarkdownDocs` verb) — same category as the already-excluded hivectl-cli.md/swarmctl-cli.md, so it's untouched here. Flagged on #4548. --- docs/networking/gateway.md | 36 +++++++++++++-------------- docs/tools/subagent.md | 50 +++++++++++++++++++------------------- docs/web-ui/dashboard.md | 22 ++++++++--------- 3 files changed, 54 insertions(+), 54 deletions(-) diff --git a/docs/networking/gateway.md b/docs/networking/gateway.md index 5e175e1a..d2035515 100644 --- a/docs/networking/gateway.md +++ b/docs/networking/gateway.md @@ -1,6 +1,6 @@ # hive-gateway -This host's nginx fronts the hyperhive web surfaces running on it — next to hive-c0re, not in its own container: it shares the host netns anyway (see [Vhost map](#vhost-map) below), so containerizing it would buy no network isolation while costing a resolv.conf sync, a machine-bus reload, and three bind mounts. System-config (not meta-flake managed). Configured via `services.hyperhive.gateway.*` + per-subsystem opt-in flags in `services.hyperhive.{forge,matrix,...}`. `gateway.enable` and `gateway.dns.enable` are asserted by the modules that need them, so a host serving a vhost or resolving hive names gets them without an opt-in. +This host's nginx fronts the hyperhive web surfaces running on it — next to hive-c0re, not in its own container: it shares the host netns anyway (see [Vhost map](#vhost-map) below), so containerizing it would buy no network isolation while costing a resolv.conf sync, a machine-bus reload, and three bind mounts. System-config (not meta-flake managed). Configured via `services.hyperhive.gateway.*` + per-subsystem opt-in flags in `services.hyperhive.{forge,matrix,...}`. the modules that need them assert `gateway.enable` and `gateway.dns.enable`, so a host serving a vhost or resolving hive names gets them without an opt-in. ## Vhost map @@ -151,7 +151,7 @@ as same-origin `/agent//…` URLs instead of the legacy direct `http://:/` TCP shape. The signal comes from `StateSnapshot.gateway_enabled`, sourced from the `HIVE_GATEWAY_ENABLED` env the c0re NixOS module now always sets -(`services.hyperhive.gateway.enable` was removed — the gateway runs +(`services.hyperhive.gateway.enable` no longer exists — the gateway runs unconditionally alongside hyperhive), so this is effectively always true; the `false` branch stays as a defensive fallback for the env being unset. Three render sites @@ -166,15 +166,15 @@ frontend-side derivation. ## TLS modes The gateway always terminates TLS — self-signed is the implicit floor when -nothing else is configured, so there is no http-only mode. Three modes, -selected by which (if any) external TLS source is set: +the operator configures nothing else, so there is no http-only mode. Three +modes, selected by which (if any) external TLS source the operator sets: ⚠️ **One vhost class is exempt, so expect it during a TLS audit.** A vhost bound to loopback for a local consumer carries a single plain-HTTP listen and no TLS. `grafana-metrics` (`nix/host-modules/swarm-grafana.nix`) is the only one in the tree today: it listens on `127.0.0.1` alone and serves one `= /metrics` location from grafana's unix socket, for the collector on this -host to scrape. Nothing off-host can reach it, so TLS there protects nothing. Every vhost with a -routable name is covered by the modes below. +host to scrape. Nothing off-host can reach it, so TLS there protects nothing. The modes below +cover every vhost with a routable name. | mode | config | cert source | `.well-known` scheme | |---|---|---|---| @@ -209,7 +209,7 @@ Mutual exclusion: `tls.certDir` set together with `tls.acme.enable = true` fails ### Self-signed TLS (default) -On by default, and listens on `httpsPort` (default 443) on every routable vhost beside the plain-http `port` (default 80). (The loopback-only exception is named under *TLS modes* above.) +On by default, and listens on `httpsPort` (default 443) on every routable vhost beside the plain-http `port` (default 80). (See the loopback-only exception under *TLS modes* above.) The issuer is a **host-held hive CA**, not a bare self-signed leaf. A host service (`hive-tls-ca.service`, from the `hive-tls` module) generates a long-lived CA (`services.hyperhive.deploy.hive-controller.tls.caValidityDays`, default ~20y) under `services.hyperhive.deploy.hive-controller.tls.stateDir` (default `/var/lib/hive-tls`), then signs a gateway **leaf** (`leafValidityDays`, default 30d) with it. `hive-gateway-self-signed-cert` then imports the leaf into nginx's state dir (`/var/lib/hive-gateway/tls/{cert,key}.pem`). @@ -221,17 +221,17 @@ root:root`; nginx's pre-start `nginx -t` runs as the *nginx user*, so a **every cert path the nginx config names exists** — which is what the swarm-services fallback below is for. -**Why a CA, not a bare leaf**: a bare self-signed leaf is its own trust anchor, so every regeneration is a new anchor every consumer must re-trust — and a runtime-generated leaf can't be wired into an agent's build-time trust store at all. With a stable CA, agents and federation peers trust it *once*; leaf rotation never re-breaks them. +**Why a CA, not a bare leaf**: a bare self-signed leaf is its own trust anchor, so every regeneration is a new anchor every consumer must re-trust — and nothing can wire a runtime-generated leaf into an agent's build-time trust store at all. With a stable CA, agents and federation peers trust it *once*; leaf rotation never re-breaks them. **What consumers trust**: `trust-bundle.pem` in the same state dir, not `ca.pem`. The hive CA is itself issued under the swarm root ([`swarm/ca.md`](../swarm/ca.md) has the hierarchy), and an intermediate isn't a chain a verifier can terminate at — so the bundle carries the hive CA plus whatever it's rooted at. `hive-gateway-self-signed-cert` hands nginx the leaf with the hive CA appended for the same reason. Everything that trusts the hive's TLS reads the bundle: agents (via `security.pki.certificateFiles`), the CI and forge containers, and a federating peer. **Why on by default**: matrix-dart-sdk (FluffyChat's SDK) hardcodes `https:///.well-known/matrix/client` for homeserver discovery and refuses to fall back to plain http. Without TLS the browser client can't bootstrap. -**Cert shape**: leaf subject CN = bare hive domain; subjectAltName is `` plus wildcard `*.`, so all current and future sub-domain vhosts validate under the same leaf + the hive CA. A swarm service whose name is *not* under this hive's domain can't be added here — the hive CA is name-constrained to ``, 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 `` plus wildcard `*.`, 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 ``, 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)). **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`. -**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). Agent trust is wired separately (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`). ### Operator-provided cert (`tls.certDir`) @@ -273,8 +273,8 @@ always advertise `https` (see [Discovery flow](#discovery-flow-matrix) above). ## Firewall posture (host-level) -The gateway is unconditional — `services.hyperhive.gateway.enable` was -removed, there is no gateway-off mode. nginx is always the sole +The gateway is unconditional — `services.hyperhive.gateway.enable` no +longer exists, there is no gateway-off mode. nginx is always the sole external entry point and routes to agents over the UDS upstream described above (see [Per-agent unix-socket upstream](#per-agent-unix-socket-upstream)), so the per-agent web-port @@ -297,14 +297,14 @@ 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 gateway. Operators who opt out of the gateway lose external dashboard reach by design — the surface is privileged (approve / -deny / destroy) and must not be exposed without a real reverse +deny / destroy), and operators must not expose it without a real reverse proxy in front. ## `HIVE_FORGE_URL`: agents reach the forge via the gateway by domain Agents poll `HIVE_FORGE_URL` for Forgejo notifications + run all `hive-forge` calls against it. Network isolation is always on (the -shared-netns mode was removed), so agents run in a private netns and +shared-netns mode no longer exists), so agents run in a private netns and can never reach the host's loopback. `nix/host-modules/hive-c0re/environment.nix` sets `HIVE_FORGE_URL` to `http://` (default `forge.` — a swarm runs @@ -355,10 +355,10 @@ only if you already have another process bound to 3000. `sshPort` (default **2222**) is the port Forgejo's built-in SSH server uses for `git clone/push/pull` over SSH (`git@:owner/repo.git` -via `-p 2222`). Port 22 is left alone on the host for openssh. +via `-p 2222`). Port 22 stays alone on the host for openssh. -`openFirewall` (default **false**) controls whether `httpPort` and -`sshPort` are opened in the host firewall. Off by default (secure by +`openFirewall` (default **false**) controls whether the host firewall +opens `httpPort` and `sshPort`. Off by default (secure by default): agents reach Forgejo through the gateway (`forge.` on the bridge), not the raw port, so no firewall hole is needed. Flip to `true` when you need: @@ -529,7 +529,7 @@ is required. nginx reads it at that path directly. Manage users with `hivectl gateway`. `hivectl` sends the request over the host admin socket and the `hive-c0re` daemon performs the write at its -canonical path — no path is exposed to the CLI: +canonical path — the daemon never exposes a path to the CLI: ```sh # Add or update a user (prompted for password): diff --git a/docs/tools/subagent.md b/docs/tools/subagent.md index fb7d9750..a4024b3c 100644 --- a/docs/tools/subagent.md +++ b/docs/tools/subagent.md @@ -37,8 +37,8 @@ that `start` is reachable from. Neither signal tool takes a session name. **The URL is the identity.** At each spawn the daemon mints that run an unguessable token, serves it at `/signal/mcp/`, and writes that one URL into that one subagent's own -`--mcp-config` — a file per session, not a shared one. A request is -resolved to a session before it's dispatched, and the tools read the +`--mcp-config` — a file per session, not a shared one. The daemon resolves +a request to a session before dispatching it, and the tools read the session off the resolution. A subagent therefore has no field in which to name a sibling, and knowing @@ -85,25 +85,25 @@ reaches. Read alongside the last-event age below, it's what separates a subagent that's working from one that's wedged from one that's out of turns — without `ps` and without opening a file. -Four things stop a run, and each is recorded distinctly, reported by -`status`, and appended to the one todo the daemon pushes when the run ends: +Four things stop a run; the daemon records each distinctly, reports it via +`status`, and appends it to the one todo it pushes when the run ends: - **the turn ended and there was no goal** — the single-turn case; - **`goal_reached`**, which the subagent calls itself; - **`need_help`**, likewise; - **the turn cap**, which says so rather than stopping quietly: the todo - states that the harness limit was reached and the goal was never + states that the run hit the harness limit and the goal was never reported reached, so the work stopped where it had got to. A killed or failed turn ends the run too, and keeps the records it already had — see [A killed turn](#a-killed-turn). `interrupt` therefore stops a whole goal run, not just the turn in flight. -When the session was told where its report goes — `start`'s `report_file`, -or the path the subagent names when it signals — the stop reason is -appended to that file as well, so the artifact you were going to read -anyway also says how the run ended. Nothing is inferred: with no path -given, no file is touched. +When the session knows where its report goes — `start`'s `report_file`, +or the path the subagent names when it signals — the daemon appends the +stop reason to that file as well, so the artifact you were going to read +anyway also says how the run ended. The daemon infers nothing: with no +path given, it touches no file. ## `goal_reached` is a label, not a gate @@ -125,8 +125,8 @@ parent that polls `status` sees the block without reading anything else. ## Is it working, or is it wedged? -`status` reporting **running** says a process is tracked, which a wedged -subagent satisfies as fully as a busy one. A running answer therefore +`status` reporting **running** says the daemon tracks a process, which a +wedged subagent satisfies as fully as a busy one. A running answer therefore carries the age of that turn's last event too: seconds means it's working, an age climbing into the minutes means it's stuck. That one number replaces inferring the same thing from `ps` output and CPU-time deltas. @@ -164,7 +164,7 @@ continue error: claude error: no session matched the requested id or title (sear The directory is the part claude's own message never names, and the part that resolves the confusion: pass `dir` to point `continue` at the -directory the session was started in. +directory where the session began. Nothing here is a fixed delay on the way to a successful turn. The wait ends on whichever comes first — the turn's first stream event or its @@ -174,7 +174,7 @@ bounds the one case neither covers: a child that neither speaks nor exits, reported as started, with the end-of-turn todo left to say how it goes. That todo still carries every failure that happens later in the turn, exactly as before; the only one it no longer repeats is the miss -the caller has just been handed to its face. +`continue` has just handed the caller directly. ## A killed turn @@ -185,11 +185,11 @@ than letting it settle back into `idle`: - `status` reports the session **killed**, naming the signal, instead of the `idle` it reports for a turn that ended on its own; -- the end-of-turn todo the daemon pushes without being asked says the - subagent was killed mid-turn, not that it finished; +- the end-of-turn todo the daemon pushes unprompted says something killed + the subagent mid-turn, not that it finished; - `continue` still resumes such a session — often what you want — but - its reply says the previous turn was killed, so nobody carries on from - cut-off work believing it was complete. + its reply says something killed the previous turn, so nobody carries on + from cut-off work believing it was complete. The record is per-name, in memory with the rest of this daemon's state, and the next confirmed spawn under that name clears it. A daemon restart @@ -254,21 +254,21 @@ allowed it, and would diverge from the parent's on the first tool anyone adds to either. Everything else in claude's built-in set is absent, in particular the -tools that let a session act outside the run it was started for: peer and +tools that let a session act outside the run for which it began: peer and operator messaging, nested agents (including the stop verb, which takes an _agent_ id rather than a session), schedule and webhook creation, and -worktree switching. Before this flag was passed, a subagent reached all of -them — `--dangerously-skip-permissions` had removed the only thing that -would have asked, and `--allowedTools` would not have helped: it approves -prompts in advance rather than restricting anything. +worktree switching. Before the harness passed this flag, a subagent +reached all of them — `--dangerously-skip-permissions` had removed the +only thing that would have asked, and `--allowedTools` would not have +helped: it approves prompts in advance rather than restricting anything. One rule worth knowing before editing any of this: **the daemon never emits an empty `--tools` value**, and asserts rather than doing so. Not because of what an empty value does — that's exactly the point. Our own measurement and the installed `claude --help` disagree about whether `--tools ""` means "no tools" or reads as the flag being absent, and the -answer belongs to whichever claude release is installed rather than to -this repo. Refusing the case is correct under either reading, so the code +answer belongs to whichever claude release the system has installed +rather than to this repo. Refusing the case is correct under either reading, so the code never has to know which holds. Nothing here wants a subagent with no built-in tools anyway. diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 81ac6929..ca780d0c 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -58,7 +58,7 @@ surfaces, not tab panes. **C0NTAINERS** — live containers rendered as a depth-first tree using `ContainerView.parent` (populated by -`hive-c0re/src/agent_config/topology.rs` — not to be confused with +`hive-c0re/src/agent_config/topology.rs` — don't confuse it with `hive-c0re/src/dashboard/topology.rs`, which only holds the set-parent endpoints). ASCII tree glyphs (`├─`, `└─`, `│ ` continuation columns) prefix @@ -248,7 +248,7 @@ keeps or drops a whole group, never splitting mid-tree. Default selection is every state **except** `Done`/`Skipped` — a fresh queue view leads with what's still moving or needs attention, not the settled tail; builds.js omits the `states` param entirely (identical request to -before this filter existed) when every state is checked. Server-side: the query +before this filter existed) when the operator has checked every state. Server-side: the query narrows [`Queue::graph_snapshot`]'s already-bounded (`MAX_HISTORY_DAGS`) root set — the history cap and the state filter are independent concerns, so a narrow filter never reaches further back in time to @@ -258,7 +258,7 @@ Below the queue, a **live build-log panel** (`#rebuild-live-log`, `renderRebuildLiveLog`) shows the currently running rebuild's output inline — collapsible, with a live/ok/fail badge and a `↓ raw` download. It's keyed to the first `Running` node (in wire order) -whose `payload.data.build_log_id` is set — read from the `onUpdate` +whose `payload.data.build_log_id` has a value — read from the `onUpdate` callback's node list, same source as the count pill, no separate fetch — and **polls** `GET /api/build-log/{id}` every 2s (`fetchAndRenderLiveLog` / `liveLogPollTimer`); not an `EventSource` @@ -339,8 +339,8 @@ The status dot renders these states: - **grey** — no token (not provisioned). The container-down cross-reference (`/api/state`) takes precedence over -the age check. `as_of_unix` is tooltipped ("live as of N ago") throughout -so freshness is always legible. When `live` is absent (an older hive-c0re +the age check. The dashboard tooltips `as_of_unix` ("live as of N ago") +throughout so freshness is always legible. When `live` is absent (an older hive-c0re without the snapshot) the dot falls back to a token-present rendering. The provision form (account name, homeserver, login method) posts @@ -444,8 +444,8 @@ The current capabilities are: Each row is one agent. Columns are the capability names returned by `GET /api/capabilities` as `caps: Vec`. Checking or unchecking -boxes only stages the change in-browser; nothing is written until the -operator selects the page-level **save all** button (described below). Row +boxes only stages the change in-browser; the browser writes nothing until +the operator selects the page-level **save all** button (described below). Row values follow the `effective`/`assignments` rule described above. **T00L GR0UPS** — per-agent tool-group permissions. Tool groups are @@ -1071,7 +1071,7 @@ renderApprovals`) with three stacked sections: kind-specific drill-in triggers: - `merge_config_pr`: `↳ review PR on forge ↗` deep-links the config PR into `agent-configs//pulls/` (shown - only when `forge_present` and `pr_number` is set). The config diff + only when `forge_present` is true and `pr_number` has a value). The config diff lives on the forge PR itself — no inline diff side-panel. - `spawn`: a one-line "container will be created" note instead. - **decision actions** — `◆ APPR0VE` and `DENY`. Deny pops a @@ -1251,7 +1251,7 @@ below — some endpoints aren't in it yet. an agent's tool-groups and capabilities triggers a single rebuild, not two. **Atomic**: the endpoint validates every change first (agent names via `guard_agent_name`, group + capability names) and on any validation - error nothing is written or enqueued (non-2xx `{ error }`); it skips + error writes or enqueues nothing (non-2xx `{ error }`); it skips rows with both fields omitted rather than erroring on them. Returns `200 "ok"` on success. @@ -1266,8 +1266,8 @@ below — some endpoints aren't in it yet. targets_add?, targets_remove? }`. Missing key = "leave alone"; explicit `null` on `description` / `interval_seconds` clears the field (so a - recurring schedule flips to one-shot when `interval_seconds` - is sent as `null`). `targets_add` is replace-on-conflict: + recurring schedule flips to one-shot when the caller sends + `interval_seconds` as `null`). `targets_add` is replace-on-conflict: re-adding a previously cancelled target drops the tombstone and the target starts fresh (operator intent on re-add = "this target is active again"). `targets_remove` delegates