Commit graph hyperhive/docs
Author SHA1 Message Date
atlas
fe9417ae52 swarm: give agent containers their own queue principal
Agents have authelia *users*; they had no machine identity at all, so an
agent could not authenticate to the swarm queue as anything. This mints
one `agent-<hive>` OIDC client per hive beside the existing
`hive-<hive>` one, teaches the auth-callout responder an agent arm, and
opens the queue's client port on the bridge so a container can reach it.

One client per HIVE, not per agent: agents are created at runtime, and a
per-agent client would make creating one a config change plus an
authelia reload. The cost is that agents on a hive are indistinguishable
to the broker, which is deliberate and tracked separately.

The agent grant is deny-by-default twice over. An agent id matches no
hive rule, so it gets a hive's status-key grant from neither; and with
no agent subject configured the responder returns no grant at all rather
than an empty publish list, which would be a denial wearing a grant's
shape. What an agent may publish is a deployment's decision, taken
through `--agent-publish-subject` the same way `--hive-publish-subject`
already works.

`Policy::new` now refuses two prefixes where one contains the other. The
arms are tried in order, so that overlap does not error at match time -
it silently hands one principal the other's grant.

Not shipped here, and neither is reachable without it: no subject is
configured for agents anywhere in nix, and nothing yet delivers
`agent-<hive>.secret` into an agent container. Both belong to the stream
that will be the first consumer.
2026-09-12 10:33:06 +02:00
damocles
01c0a6dcba job_queue: fix dangling rustdoc intra-doc link left by strum conversion 2026-09-12 00:06:31 +02:00
damocles
1fb9e068ee docs: drop the operator-schedule-target aside from approvals.md 2026-09-11 23:32:45 +02:00
damocles
53507bf59c docs: fix vale contraction lint in approvals.md 2026-09-11 23:32:45 +02:00
damocles
b4dc09ff93 remove operator as target for scheduled prompts 2026-09-11 23:32:45 +02:00
atlas
8251223144 docs/setup: use the emphasis form treefmt's prettier normalises to
The three cross-references this branch rewrote used `*Swarm SSO*`; prettier
emits `_Swarm SSO_`, so checks.formatting went red on an otherwise
content-only change.

Verified: nix fmt is now idempotent on this tree (0 changed); the three
scripts/check-*.sh lints exit 0 with the change staged.
2026-09-11 19:30:17 +02:00
atlas
108f7e17ea docs/setup: name the section a cross-reference points at, not its number
`c1b7be11` inserted `### 3 · Secret store` and renumbered every heading below
it — SSO 3→4, UI 4→5, Matrix 5→6, Spawn 6→7, Host commands 7→8 — without
touching a single cross-reference. Five were left pointing one section short:

  line  25   "see step 6"  -> §6 Matrix,        meant §7 Spawn sub-agents
  line  30   "in step 6"   -> §6 Matrix,        meant §7 Spawn sub-agents
  line  35   "see step 3"  -> §3 Secret store,  meant §4 Swarm SSO
  line 217   "(step 3)"    -> §3 Secret store,  meant §4 Swarm SSO
  line 246   "see step 3"  -> §3 Secret store,  meant §4 Swarm SSO

Both `step 3` targets are account-creation instructions and §3 is the secret
store, which is `only when deploy.bao` — so an operator without a store follows
the pointer into a section that does not apply and finds no `swarmctl` in it.

Renumbering to 4/7 would rot on the next insertion. These name the section
instead, the form `approvals.md:87` and `dashboard.md:906` already use. The
Matrix block's `# 5a.`–`# 5d.` comments lose their prefixes for the same
reason: they numbered themselves against that section's old position, and the
page's other code blocks carry unnumbered comments anyway.

Closes #4212.
2026-09-11 19:30:17 +02:00
damocles
a2d40dc1bd docs: regenerate forge-cli.md for the --limit/--tail opt-in swap
fixes hive-forge-docs-fresh check on #4207
2026-09-11 19:13:30 +02:00
atlas
db0eb8829b hivectl, docs: choom is not root-only, and set-limits takes no agent name
Two unrelated changes landed correctly and left their prose behind, both
on operator-facing surfaces.

`hivectl agent <name> choom` gained a `hive-admin` path when hive-c0re
started shipping a polkit rule for `machinectl shell`. `choom.rs`
implements exactly that — `euid != 0 && !in_hive_admin_group()` — but
the `Choom` variant's clap doc comment still said "Requires root and a
running container", eight weeks on. That string is what
`choom --help` prints and what renders into the generated CLI
reference, so it is the sentence an operator actually reads, and it
tells a `hive-admin` member the command is not available to them.
`docs/turn-loop/mcp.md` carried the same staleness inside a
trust-boundary argument ("an operator (root) action"), where naming a
narrower reachable set than the real one is the wrong way to be wrong.

Dating it rather than asserting it: the acceptance landed 2026-09-07,
the doc string 2026-07-16.

Separately, `docs/tools/hivectl.md` states the agent-name hoist itself
("the name is hoisted onto the parent command, so none of the verbs
below repeat it") and its own example block obeys it, then twice writes
`set-limits sock --memory-max 8G` — the pre-hoist order. The generated
reference is unambiguous: `hivectl agent <NAME> <COMMAND>` versus
`hivectl agent set-limits [OPTIONS]`, with `quota set <SIZE>` nearby
showing that positionals do render when they exist. The costly one is a
complete command in backticks inside the paragraph explaining the
footgun it illustrates.

`docs/tools/hivectl-cli.md` is regenerated rather than hand-edited; the
diff against the committed copy is exactly one line.

Closes #4236.
2026-09-11 19:11:13 +02:00
damocles
f80947f4cc hive-sh4re, docs: fix the two docs argus flagged on ToolGroup::Execution
addresses review on #4245
2026-09-11 19:01:31 +02:00
atlas
6d7565a30d scheduling.md: drop the list_schedules prose, keep the approval-scope fix
mara's call on this PR was "list schedules not being scoped at all is a
bug - dont document it, file the bug and fix it". The bug is fixed in
damocles's separate PR, which also rewrites this page's
`list_schedules()` section.

So both of my paragraphs about scoping go: the "not scoped at all"
sentence in the intro (documenting the bug, which is what she
objected to) and the follow-up in the `list_schedules()` section. That
section is now byte-identical to main again, leaving it entirely to the
PR that changes the behaviour — the two PRs no longer touch a common
hunk in this file.

What stays is the claim this PR was actually filed for: the page said
"All scheduling ops go through the operator approval queue", and only
creating one does. The intro now splits creating from the other four
verbs and states the one authorization rule that covers all of them,
which the scoping fix makes true.
2026-09-11 18:43:10 +02:00
atlas
c28da210b4 scheduling: clamp get_logs host-side, and fix two authorization claims
`docs/tools/scheduling.md` said three things about who may do what. Two
were wrong prose; the third was the code.

"All scheduling ops go through the operator approval queue" — one of the
five does. Cancel, edit, list and fire are `require_group("scheduling")`
and nothing else (socket_server/mod.rs:594-643), which is what the MCP
tool descriptions already tell an agent. The page pushed in the cautious
direction: someone watching a runaway recurring schedule would wait for
an operator rather than cancel it themselves.

The authorization sentence covered "read/cancel/edit". Cancel, edit and
fire really do check `cancel_authorized` (self, operator, or subtree) from
three call sites. `handle_list_schedules` takes no requester at all and
returns every row — now stated, along with the part that matters: the
snapshot includes other agents' schedule bodies.

`lines` was documented as "host-capped at 500" and the 500 was in the
agent's own MCP layer, not the host; `handle_get_logs` passed any u32
straight into JournalQuery. A limit in the caller is not a limit, so the
host clamps instead of the sentence changing. That also makes args.rs's
arg doc and the tool description agents read correct, untouched. The
sibling `get_host_journal` already clamps host-side at 100, which is both
the precedent and the control that the missing clamp here was real.

Closes #4230.
2026-09-11 18:43:10 +02:00
atlas
bafda6e3d5 docs/sso: name Grafana as the exception, and say it is unconditional
sso.md's "What this doesn't do" list opened with "It doesn't disable
local login" without qualification, which was wrong for Grafana before
the previous commit and is wrong in a different way after it: Grafana now
disables the form for every deployment, not just where authelia happens to
be on the same host.

Names the exception, gives the reason a reader can act on (a default
`admin`/`admin` account on a gateway-published vhost), and states the
consequence plainly — SSO is the only door, so a dead provider locks
everyone out, which is why the OIDC role defaults to Admin.
2026-09-11 18:23:51 +02:00
atlas
e3864fe787 docs/matrix: name the [acct:<name>] prefix a multi-account agent receives
`matrix.md` documents the exact text of every inbound matrix signal —
three wake-body shapes and the invite loose-end — and none of them
mention that the daemon prefixes `[acct:<name>] ` when it serves more
than one account.

`wake::tag_account` is live on both documented paths (`timeline.rs:75`
for unread wakes, `:139` for invite todos), so an agent with an extra
account receives `[acct:ccc] [matrix] @a:s in #x: hi` where the page
promises a body starting `[matrix]`. The example is not hypothetical:
the matrix module uses `matrix-token-ccc` on dmatrix as its worked
example of an extra account.

It stayed invisible because the `None` arm returns the body unchanged,
so every single-account agent sees the documented format exactly. The
page is right for almost every reader and wrong for precisely the
readers its "Multiple accounts" section is written for.

Two placements rather than one. The prefix itself goes next to the wake
formats it corrects, with the worked example and the reason a leading
`[matrix]` match works until a second account exists. A forward pointer
goes in "Multiple accounts", because that is the section someone
configuring extra accounts actually lands on, and it previously covered
only the outbound `account` parameter — the half you pass, not the half
you parse.

Closes #4243.
2026-09-11 18:19:22 +02:00
atlas
78a53cc9ac docs/observability: hive→swarm ingest auth shipped, stop calling it planned
The security section said hive→swarm ingest auth was "planned" and that the
`hive` label "becomes" unforgeable. Sixty lines later, `### Authenticated
ingest` in the same file describes it as shipped and mandatory — "No
unauthenticated mode exists" — and the code agrees: swarm-otel.nix:157 derives
one receiver port per entry in `swarm.hives`, :1387 stamps `key = "hive"` from
the receiver that accepted the sample, :751 asserts `swarm.hives != {}`, and
otel.nix:659 makes a hive without an identity a build error.

Not drift. Ordered by position in main rather than by author date, the "planned"
wording is the NEWEST of the three commits: `9dc60061` documented authenticated
ingest and `9bd2b9e9` dropped the unauthenticated mode (both 2026-08-19), while
`5fcd2a93` — authored 00:59 that morning, merged on 08-30 — added the sentence
eleven days later into a tree where the feature already existed. A long-lived
branch's prose is a snapshot of the tree it was written against, and nothing
re-reads it at merge time.

Matters because §"what the agent→collector hop is and isn't" is the page a
reader goes to for "can a hive forge a label". It answered "auth is planned",
which reads as "`hive=` is forgeable today". The truth is the opposite and
stronger.

Closes #4216.
2026-09-11 18:11:19 +02:00
damocles
79c43a15d8 hive-c0re: scope list_schedules to what the requester can actually act on
handle_list_schedules took no requester and returned every schedule
unfiltered, unlike cancel_schedule/edit_schedule/fire_schedule_now
which all gate on the shared ownership predicate (self, operator, or
subtree via topology::is_descendant_of). list_schedules now filters
through the same predicate, renamed cancel_authorized ->
schedule_authorized since it backs all four verbs now, not just
cancel.

Fixed five stale 'every schedule' / 'unfiltered' claims found while in
here: filter_ghost_schedule_targets's doc comment, the list_schedules
MCP tool description, docs/tools/scheduling.md's per-verb section
(already self-contradicting its own top-of-file subtree-scoping claim
before this fix), and hive-core-agent-sock's ListSchedules/Schedules
wire-type doc comments (including a stale '(privileged)' marker from
the pre-topology-subtree model).

Credit to atlas: independently found the same fix while finishing
PR #4233 (which documents this bug per mara's 'fix it, don't document
it' ruling) and caught two stale doc spots I'd missed
(hive-core-agent-sock's comments) plus proposed the
schedule_authorized rename. Compared diffs directly before either of
us pushed; he dropped his scheduling.rs changes so we didn't collide.

fixes #4237
2026-09-11 18:05:13 +02:00
atlas
2ec5c9433f docs/security: name all 34 PrivRequest variants, not 17
`security.md` introduces its hive-priv table as "Narrow interface —
`PrivRequest` variants map 1:1 to specific known operations". The table
named 17 of 34, so the claim it was making was only half-checkable by
the reader it was written for.

Two whole subsystems were absent rather than stragglers: btrfs
subvolume + snapshot management (9 verbs) and per-agent external forge
accounts (2), plus `SendAgentSnapshotToFd`, which passes a file
descriptor across the boundary via SCM_RIGHTS — the kind of verb
someone reads a security page to find.

The table had already been resynced by hand once, in June, and drifted
again within three months. mara's call was to keep it exhaustive rather
than summarise by subsystem: the swarm-level operations are expected to
go away, so the row count is at its peak now and shrinks from here.

Rows for the 17 missing variants, each naming what the helper actually
runs. Three of them carry the constraint that makes them safe rather
than just the command, because that is what the surrounding section is
for: `ControlInfraContainer`'s allowlist is the `InfraContainer` enum
(serde rejects unknown names at the wire boundary, and `hive-c0re` has
no variant); `WriteAgentExtraForgeAccount`'s `label` reaches a filename
and is validated as a plain identifier first; `SendAgentSnapshotToFd`
requires exactly one descriptor and refuses one arriving alongside any
other operation.

Prose is active voice throughout the new rows — "hive-priv validates
`label`" rather than "`label` is validated" — since the question a
reader brings to this table is which component enforces what.

Verified with a variant-vs-page diff carrying its own controls
(extractor count, page-readable, a present variant resolves, an
invented one does not): 34 named, 0 absent. vale is unchanged from
main's baseline for this file, 0 errors and 13 warnings on both sides.

Closes #4222.
2026-09-11 17:41:25 +02:00
damocles
2f792a868c docs: use pr status's positional form in the two remaining --pr examples
#4184 gave 'pr status' a positional PR-number arg (--pr <n> still works, just
no longer the recommended form). docs/tools/forge.md's examples and the
prose section still showed the old --pr form in four places; same one-line
hint baked into every agent's own system prompt (hive-agent/prompts/
system.md). Neither file was touched by #4184 itself -- leftover from my own
#4182 branch that lost the collision to atlas's #4184 (docs/tools/forge.md
and hive-agent/prompts/system.md were the two genuinely non-overlapping bits
of that branch, tracked as a follow-up rather than dropped).

docs/tools/forge-cli.md is clap-generated and doesn't need regenerating --
prose-only doc changes don't touch the clap tree it's derived from.

refs #4182
2026-09-11 17:27:49 +02:00
atlas
5af1f6a8e5 docs, mcp.nix: an overridable default is not unconditional, and there are four subagent tools
`docs/tools/subagent.md` and `docs/tools/bash.md` both described their MCP
server as injected "unconditionally". Both entries are `lib.mkDefault`, and
the module says why one line above each: "so an agent.nix can still
override/disable the entry", "so the operator's own agent.nix can override
the entry".

The word matters for the subagent one in particular. The same comment block
records the framing that it is default-on for now and should become a real
capability gate later, so "can I turn this off today?" is a question an
operator has — and "unconditionally" answers it as "patch nix/" when the
answer is one override in agent.nix.

Both pages now say default, and say what the default yields to.

The other direction on the same page: `subagentHttpPort`'s option
description and the unit comment beside it both listed three tools,
`start`/`continue`/`interrupt`. The daemon serves four. #4101, which
introduced it, is titled with the three-verb phrasing, so `status` landed
afterwards and never reached either description — while `subagent.md` had
the full set all along. The option description renders into the generated
options doc, so it is the one an operator reads.

Closes #4231.
2026-09-11 16:58:12 +02:00
atlas
0a80f21003 prompts+docs: the lifecycle tools reach the whole subtree, not just direct children
`require_descendant` (`socket_server/mod.rs:666`) authorises
kill/start/restart/update/get_logs with `topology::is_descendant_of` — the
caller's whole subtree, itself included. That has been true since `53b4e752`
(#1865), whose message says "a parent owns its whole subtree; the root covers
every agent as a consequence, no positional privilege", and two tests pin it
(`is_descendant_of_in_grandchild`, `is_descendant_of_in_self_is_true`).

The prose never followed. The four lifecycle tool descriptions, their
`// IMPORTANT:` comments, `docs/tools/lifecycle.md`, the tools README,
hive-agent-mcp's README and the system prompt every agent is rendered from all
still said "direct children only" — while `list_containers`, four tools away in
the same file, said "direct children + their subtrees".

`lifecycle.md` also taught the model #1865 deleted: "Privileged agents (for
example ruth) may operate on any sub-agent — the topology scope applies to all
others." There is no privileged class to belong to; ruth reaches every agent
because the check is transitive and everything sits under it.

Same drift on the state-query side: `resolve_agent_state_target` is
subtree-scoped by the same commit, so `get_loose_ends`' argument doc, the
`QueryAgentState` capability doc and `docs/turn-loop/mcp.md` were all telling a
parent it needs a capability to read a grandchild's threads.

Two smaller corrections found on the way:

* `list_containers` returns the caller itself. `is_descendant_of` is true for
  `candidate == ancestor` and `handle_list_descendants` filters the topology
  with it; called from a leaf agent it answers one row, that agent.
* `request_init_config` accepts any unused name — the requester becomes its
  parent — or an existing agent already in the caller's subtree, not "a direct
  child". The editing surface is narrower than the guard, though: only direct
  children's config repos are bind-mounted, so re-seeding deeper in the subtree
  leaves no local copy to edit. `lifecycle.md` now says so.

The prompt's other stale claim, the dead `request_apply_commit`, is #4226 and
was fixed independently by damocles in #4227 while this was being gated. This
branch keeps only the scope wording on that line.

Closes #4225.
2026-09-11 15:23:50 +02:00
atlas
0d25c0c9e6 docs/swarm: a hive entry's domain defaults, as the module says and the page already said once
README.md described `swarm.hives.<name>.domain` two ways, 100 lines apart. §
*Hive identity config* says it defaults to `<name>.<swarm.domain>` and shows a
names-only directory; § *The swarm's hive directory* called it "required per
entry and deliberately undefaulted" and wrote a domain into every entry.

The module sides with the first (`nix/host-modules/swarm.nix:125`):

    default = if swarmCfg.domain == null
              then "${name}.invalid"
              else "${name}.${swarmCfg.domain}";

and `:110` answers the stale paragraph's objection directly — the default is "a
derivation from two values an operator had to state explicitly (both are
required), not a guess".

Ordinary drift, two hours wide: `433b2940` wrote "deliberately undefaulted" at
20:44 on 08-05, `3b6576fa` added the default at 22:43 the same evening under the
message "a hive's domain comes out of the swarm directory". Both mine.

The example now shows what the convention actually looks like — names only, plus
one off-convention hive carrying the override, which is the case the option
exists for.

Closes #4220.
2026-09-11 14:40:01 +02:00
atlas
0d31560e38 docs/jobq: the queue view hides done and skipped steps by default
jobq.md's "Watching it happen" section ended by promising that a step which
isn't needed "shows as `·` rather than dropping out of the tree entirely, so
the same kind of operation keeps a recognizable shape run to run".

In that view it does drop out. `JobqGraph.tsx:94` sets
`DEFAULT_HIDDEN_STATES = {Done, Skipped}` under the comment 'Product call:
"default selection filters out skipped and done"', `:299` seeds the selection as
everything except those, and `:271` puts the selection in the query string — so
the hidden states are never fetched, not merely styled out. That component is
what renders both surfaces the page names, and neither passes an override.

The claim is true of the data and false of the screen, in a section about what
is on the screen. Keeping both: the node stays in the graph, and the default
filter is named, along with the part that would otherwise surprise someone
debugging it — the selection is a request parameter.

Closes #4223.
2026-09-11 14:38:20 +02:00
atlas
c234a32b36 docs/services: the store export is unconditional, and there is no eval refusal
`5478e0bf` (`fix(#3554)`) made both store exporters unconditional and removed
the "somewhere to send" assertion, and touched no documentation — its diffstat
is four `nix/` files. `services.md` § *Telemetry collector (OTEL)* still
described the old shape in two places.

① "it writes the store above and exports to `otel.endpoint`, doing both when
both are configured" — only the upstream is conditional. `exporterNames`
(swarm-otel.nix:231) always carries the store exporter, with a comment saying
why: `deploy.victoriametrics.enable` means "this host RUNS the store", and a
swarm has one either way.

② "With neither `otel.endpoint` nor the store enabled, this module refuses the
collector at eval" — that assertion is gone. swarm-otel.nix:693 records the
removal at the head of the assertions list, and `5478e0bf`'s message states it
was a ruling rather than an oversight: a collector on a host of its own is a
supported shape, so refusing to build it would have made the fix illegal
exactly where the bug bit hardest. `git grep` finds no surviving assertion of
that shape.

The replacement paragraph states the invariant an operator can act on — the
exporters have no gate, the stores are addressed by swarm name — and keeps the
failure it prevents, since "an absent exporter is not an error" is the part that
made the old bug silent.

The stale claim dates to `80c9118f` (2026-08-18), 13 days before the behaviour
changed under it.

Closes #4214.
2026-09-11 14:35:01 +02:00
atlas
78699ebd5a docs/secrets: the collector's secret copy has one condition, not two
secrets.md said the swarm collector's OIDC secret is copied "when authelia is
enabled on this host and something published is being scraped; otherwise no
secret is needed and none is placed". The second condition left the tree in
5478e0bf (#3554): `swarm-otel-oidc-secret` and the client registration it
delivers for are both `lib.mkIf deployCfg.authelia.enable`, and
`publishedScrapeTargets` appears in neither guard. swarm-otel.nix:589 says why
in as many words — authelia refuses a bearer-authz client with no audience,
which is what the old guard was for, and the push audiences are unconditional,
so there is now always one.

The sentence survived because I rewrote the paragraph around it a day later
(8cba57e0) for the collector-elsewhere case and carried the clause through.

What a reader loses: with authelia on and nothing published, the page promises
the delivery is inert. It is not — the unit runs, waits a bounded 120s for
authelia's mint, and fails the collector's container start if the secret never
appears. So the case documented as quiet is one that can fail a boot, and the
operator debugging it has a page saying this path was not taken. The
replacement states the single real condition and that failure mode.

Closes #4210.
2026-09-11 14:34:34 +02:00
atlas
98376fd846 docs/security: the claude dir is 0755 by design, not 0700
security.md's "What's NOT exposed" list said `/home/<name>/.claude/` is mode
`0700`. Measured from inside an agent container — the vantage the claim is
about, since the nixbld users in this threat model run there — it is `0755`.

`ensure_claude_dir` (hive-c0re/src/lifecycle/setup.rs) chmods it to 0755
deliberately, and its comment says why: hive-core is a different user and needs
read+execute to list the directory so `claude_has_session` can detect a valid
session. So the code is right and the doc named a mechanism that was never the
real one.

The conclusion the doc drew still holds — nixbld users can't read anything that
matters. Every sensitive entry is protected on its own: `.credentials.json`,
`history.jsonl` and `settings.json` at 0600, `projects/` and `sessions/` at
0700, and the five `backups/*.backup.*` at 0600. 32 nixbld users exist on this
host (uid 30001+) and none of them can read any of it.

What changes is the invariant a reader should rely on. The directory mode was
the doc's whole stated guarantee and it isn't there; the protection is per-file,
which fails differently — anything landing in `.claude/` at a default mode is
world-readable while the doc says it cannot be. `plugins/` (596 files, 0755) and
`.last-cleanup` (0644) already demonstrate that files do land loose there. Both
are harmless; the point is that nothing stops the next one from being.

Closes #4204.
2026-09-11 14:34:07 +02:00
atlas
3f878408f0 docs: name options by the path an operator can set, not by cfg.*
`cfg` is whatever the reading module bound it to. It does not exist in a
NixOS configuration, so a sentence naming an option as `cfg.<name>` is
correct about behaviour and unusable as an instruction — the reader has
to go find the real path.

The four sites in docs/networking/gateway.md this was filed for:

  cfg.sshPort       -> services.hyperhive.swarm.forge.sshPort
  cfg.dashboardPort -> services.hyperhive.c0re.dashboardPort
  cfg.frontend (x2) -> services.hyperhive.c0re.frontend

Sweeping docs/ for the pattern rather than the ticket's line numbers
found five more, in four other files:

  approvals.md    cfg.hyperhiveFlake        -> services.hyperhive.c0re.hyperhiveFlake
  matrix.md       cfg.registrationTokenFile -> services.hyperhive.deploy.matrix.registrationTokenFile
  matrix.md       cfg.gatewayHost           -> services.hyperhive.swarm.matrix.gatewayHost
  conventions.md  cfg.dashboardPort         -> services.hyperhive.c0re.dashboardPort
  gotchas.md      cfg.dashboardPort         -> services.hyperhive.c0re.dashboardPort

matrix.md is the clearest case for doing this at all: its two `cfg.`
references resolve to *different* option trees — `deploy.matrix` and
`swarm.matrix` — so the shorthand is ambiguous even within one file.

Each path is read off the `mkOption` that declares it plus the
`options.services.hyperhive.*` root it sits under, with the indentation
checked so a nested block cannot have been missed. `cfg.frontend` is
declared in hive-c0re, not the gateway: the gateway module binds
`cfg = config.services.hyperhive.gateway`, which has no `frontend`.

Deliberately unchanged: docs/networking/snapshot-store.md:136, where
`cfg.port` sits inside a ```nix block quoting module source. `cfg` is
correct there, and rewriting it would make the snippet wrong.

Closes #4193.
2026-09-11 13:32:24 +02:00
atlas
4846c3b953 docs/gateway: name the loopback-only vhost class the TLS claims exclude
The TLS section read as universal — "the gateway always terminates TLS",
"listens on httpsPort on every vhost". Measured against the rendered
config of an all-local host: ten of eleven vhosts carry two listens and
addSSL/forceSSL, and `grafana-metrics` carries one plain-HTTP listen on
127.0.0.1 with a single `= /metrics` location proxying grafana's unix
socket for the local collector.

The exception is right; a doc with no room for it is not. Someone asking
"is anything here serving plain HTTP" was told no, when the useful answer
is "no, except an internal class that is loopback-bound and deliberately
exempt" — which tells them what to look for on the next host.

One sentence where the modes are introduced rather than a patch to each
sentence that inherits the assumption, plus "every vhost" -> "every
routable vhost" where the self-signed default repeats it.

Two neighbouring claims were checked at the same time and hold: /_matrix/
proxies to 127.0.0.1:8008, and .well-known/matrix/client returns the
homeserver base_url with no port suffix on 443.

Closes #4191
2026-09-11 09:05:12 +02:00
atlas
23850e633b docs: regenerate forge-cli.md for pr status's positional
`docs/tools/forge-cli.md` is generated from the clap tree, so changing an
argument or its doc comment changes that file too. The freshness check in
`checks.hive-forge-docs` is what says so, and it is what failed here —
after 8m44s, on a PR that was already approved.

Regenerated, no hand edits. Two hunks, both the `pr status` section, and
`nix fmt` leaves the result byte-identical so the formatter and the
generator are not fighting over it.

Part of #4182.
2026-09-11 09:04:20 +02:00
atlas
ccde8a2445 docs: the agent telemetry hop carries logs now, not only stats
observability.md scoped the whole feature to Claude Code statistics, so a
reader asking what leaves an agent container would have concluded
counters and nothing else.

That page also documents the trust properties of the agent->collector
hop, and those are what needs restating for a richer payload: same
destination, same credential, same already-accepted ability to push
arbitrary OTLP. What changes is how much detail leaves by default.

docs/swarm/services.md needs no change. It already said each hive ships
the journals of "the harness units inside every agent container" — which
was not true until now.

Part of #3940.
2026-09-11 09:03:49 +02:00
atlas
b0edb5b2cc docs/setup: contract "cannot" in the KV-mount troubleshooting block
The `prose lint (vale, errors)` CI gate fails on Microsoft.Contractions
at `setup.md:147`, in prose this branch adds.

Verified against the repo's own styles rather than assumed: with
`XDG_DATA_HOME=$PWD/.vale-data vale sync` (the ini's own note — sync
ignores `StylesPath` and writes under `$XDG_DATA_HOME`), the file
reports 0 errors, and a copy of it with the word put back reports that
line and nothing else. Without the sync, vale exits 2 on a missing
styles dir, which is a config error rather than a pass.
2026-09-11 00:55:03 +02:00
atlas
48e6a0b88f swarm-bao: create the KV mount the controller writes credentials through
The bootstrap unit writes a policy granting `secret/data/swarm/agents/*` and
nothing creates that engine. A fresh OpenBao has no `secret/` — only a dev-mode
one does — so `swarm-controller`'s first credential write answers `no handler
for route "secret/data/swarm/agents/<agent>/matrix/<name>". route entry not
found.` Measured on the live host at 21:27:27Z; #4171.

`git grep` for `secrets enable`, `kv-v2`, `kv_v2` and `sys/mounts` returned zero
across the whole tree. Control, so the zero means something: `auth enable` in
this same file returns 2 — the same defect was already found and fixed once, for
the cert auth mount, with a comment that states the principle. This is the other
half of it.

The mount name is now bound once and interpolated into both the policy text and
the new step, because a grant and a mount that disagree is exactly the failure
being fixed.

Placed outside the client-CA block: the controller writes *through* this mount
regardless of whether anything can log in by certificate. `module-eval` asserts
that, since one indentation level decides it.

Grants, measured against a real openbao 2.6.2 rather than derived:
`-output-policy` asks for `sys/mounts/secret` create+update, and a token holding
exactly `sys/mounts` read + `sys/mounts/<path>` create/update enabled the engine
— **no `sudo`**, unlike `sys/auth/cert`. Negative control: the same token on an
ungranted path got 403, so the grant is what made it work. `setup.md`'s
documented policy gains those two.

Also from that session, each deciding how this is written: re-enabling an
existing path errors (exit 2), so this asks first like the auth mount does;
`secrets list -format=json` keys look like `"secret/"`, so the `case` idiom
ports over; and `kv put -mount=<p>` reports `<p>/data/...`, confirming v2 — the
prefix the policy grants and the client writes.

setup.md also drops a check that cannot work: it told the operator to confirm
with `bao read auth/cert/…`, which 403s because the host wrapper carries no
token. `systemctl status swarm-bao-controller-policy` needs no credential and
names the three success lines. The first-attempt-after-rebuild race is now
written down too — the store is still coming up, and the 30s retry is what
lands.

Refs #4171.
2026-09-11 00:16:46 +02:00
atlas
bf54436937 docs/setup: the granting unit runs on the host now
The bootstrap block still described a one-shot unit "inside the store's
container". That stopped being true when the unit moved, and a merged
change that leaves the docs asserting the old topology is worse than one
that never moved it.

Also retires the "only run once, didn't go all the way through" note. It
has run twice against a live store now and failed both times — but for
its own reasons, not the grants': it could not reach the store from where
it ran, so neither attempt exercised a single one of them. The grants are
still derived from `-output-policy` rather than proven, and the warning
now says that directly instead of implying a partial test.
2026-09-10 22:19:48 +02:00
atlas
a1b2541d62 docs/setup: run bao operator init on the host, and say when to delete the token
The host now has a wrapped bao (#4154, deployed) carrying the store's
address, CA and client cert, so the init step is a bare command there
instead of a root-login hop with an SNI override. The container route
stays as one sentence explaining why it needs the two extra pieces.

The delete step said "once that has run" without saying what -- the
rebuild, or the unit succeeding. It now names the check that settles it,
because deleting the token before the role exists leaves the unit
skipping forever and looks exactly like a store nobody bootstrapped.

Closes #4162
2026-09-10 18:34:59 +02:00
atlas
6c37411781 regenerate the hive-forge CLI reference
The doc-comment sweep changed --help text, so the generated reference went
stale and hive-forge-docs-fresh caught it. Regenerated with the command the
check itself prints; the diff is five lines, all propagations of the sweep.

Kept as its own commit rather than an amend: argus approved d090a9fb, and a
force-push clears both the verdict and the review request.

Refs #4155
2026-09-10 17:22:57 +02:00
atlas
55f01942a2 docs, prompts, hive-forge: stop handing readers the renamed verbs
docs/tools/forge.md already listed the nine renamed verbs as removed, then
used them ~30 more times in pasteable blocks. Sweeps every occurrence a
reader would type, including three runtime messages that told the user to
run a verb the same binary rejects.

The renamed-verb list itself keeps the old names; it is what documents them.

Refs #4155
2026-09-10 17:22:57 +02:00
atlas
d2175c84b3 docs: stop citing two hivectl commands that do not exist
mcp.md called it `hivectl choom`; the verb is `hivectl agent <name>
choom`. observability.md cited `hivectl shell`, which has never existed
at any level — the sentence only needs a shell, so it names one instead of
guessing which command an operator reaches for.

Found by subtracting the generated hivectl-cli.md's command headings from
every hivectl citation in docs/. Re-running it after the fix drops the
candidate list from 16 to 14, losing exactly these two.

Closes #4159
2026-09-10 17:22:28 +02:00
iris
225b1668ae docs: fix genuine Microsoft.Hyphens hits (redundant -ly adverb hyphens) 2026-09-10 15:49:53 +02:00
atlas
d2c501d1b2 docs/setup: say WHERE bao operator init runs, and how to reach the store from there
"On the host that RUNS the store" is ambiguous in the deployment we
actually ship: openbao runs inside the `swarm-bao` container, so the
host and the container are different vantages with different working
addresses, and the instruction reads as either.

From inside the container neither obvious address works — the server
certificate is issued for the store's DNS name and carries no IP SAN,
while that name resolves to the bridge rather than to the container. The
fix is an SNI override: verify the name, connect on loopback. The host's
own address is stated beside it so the reader can pick a vantage instead
of discovering the constraint from a TLS error.
2026-09-10 11:09:33 +02:00
damocles
1ad7145cc5 swap hive-claude back to the published 0.1.1 release now that it's out 2026-09-09 23:45:12 +02:00
damocles
c2fb3c6e3e subagent: add status tool, cut docs down to operator-facing + no cli flags 2026-09-09 23:45:12 +02:00
damocles
e64639c4d0 docs: document the independent subagent daemon (start/continue/interrupt) 2026-09-09 23:45:12 +02:00
iris
adfb0f9e02 shared terminal: align row icons via a shared grid, not offsets
Per mara's screenshot report (agent-terminal icons not aligned in the
first column) — and her explicit follow-up steer on the first version of
this fix: 'dont do it by offsets at all, it should be part of the layout
that they align.'

Root cause traced first, not guessed: .row-glyph relied on inheriting
text-indent: -1.4em from .live .row to paint its glyph in the reserved
prefix slot; details.row > summary separately reset text-indent: 0 for
its own hanging-caret needs, which also zeroed the inherited value for
the icon nested inside it (indent inherits by computed value, not by
rule). Two independently-computed offsets that had to agree by hand,
and silently didn't.

Replaced the whole offset scheme with a real structural fix: every row
is a 2-column CSS grid (icon column, width from one shared
--row-icon-col custom property, then content column), and a details
row grids its own <summary> with the exact same grid-template-columns
value instead of griding itself (so its <pre> body still stacks full
width below, not squeezed into column 2). Icon and content are placed
by explicit grid-column, not auto-placement inference, so an icon-less
row's lone child still lands in the content column. .row-content is a
new wrapper class (Row.tsx, terminal.js's row()/mutableRow()/
placeholder()) giving that content an element the grid can target by
class - a DocumentFragment (what linkify() returns) doesn't persist as
a node once appended, so without an explicit wrapper there was nothing
for the grid to place.

Caught and fixed a second real bug while building this for real (not
just reasoning about the CSS): the first draft's details.row { display:
block } had lower specificity than .live .row's display: grid and never
actually applied, squeezing <summary> into the row's own 1.4em icon
column and wrapping its text one character per line. Needed
.live details.row to out-specify it.

Verified with a headless-chromium render of all 6 row shapes (flat
icon / flat icon-less / flat markdown-body / details icon / details
icon-less / a long-wrapping flat row) - all align and wrap correctly.
Also verified via the earlier Range.getBoundingClientRect() measurement
(glyph paint position, not just the element's own box): diff 0.00 for
both this and the previous fix, but only this one is structural rather
than two numbers that happen to still agree today.
2026-09-09 23:35:20 +02:00
damocles
a5b07c55c8 docs/scheduler/ci.md: drop bare issue tag #4146 missed in ci.yml's own cleanup 2026-09-09 23:00:38 +02:00
iris
c13499c839 ci: split an error-only vale check out of the full prose-lint job
mara, on #4128: add a CI step (separate from the existing one) that
only flags error-level vale hits, so it can be made a required check
without also gating merges on the pre-existing warning/suggestion
backlog. The full job stays as-is, advisory, for that backlog.

Also brought docs/scheduler/ci.md's CI-checks table back in sync with
reality (it was missing the doc-pointer lint job already; now lists
both vale jobs and their required-check status).
2026-09-09 22:55:28 +02:00
iris
3ca81d388c docs: reword away from 'backend' instead of whitelisting it (mara, #4146)
mara disagreed with exempting this section from #4041's ruling.
'Backend' here meant 'which LLM API provider', and the doc already
uses 'provider' for the same concept a few lines down ('provider-
specific model string') - so reword to that instead of carving out
an exception. Drops the inline vale suppression comment entirely.
2026-09-09 22:55:28 +02:00
iris
397c0a9c7a docs: fix invalid -- inside an HTML comment body (argus nit on #4146)
Em dash to match house style everywhere else, plus HTML comment
bodies can't contain a literal --.
2026-09-09 22:55:28 +02:00
iris
f22791b7a4 docs: clear the remaining error-level vale lints
Per #4128 (mara: allow-everywhere false positives go in a central
list, otherwise fix in source). Testing surfaced better fixes than
the plan posted on the issue:

- 5x Microsoft.Contractions 'that is' idiom false positives: adding
  the missing comma ("that is, ...") both reads better and satisfies
  the rule's own negative-lookahead, so no suppression is needed at
  all. Fixed in docs/integrations/forge.md, docs/tools/forge.md,
  docs/tools/hivectl.md, docs/web-ui/dashboard.md, and
  swarmctl-cli.md's generated source (swarmctl/src/main.rs, doc
  comment regenerated via markdown-docs).
- persistence.md's 'is not' matching inside 'is nothing': reworded to
  'there'\''s nothing' rather than add any exception -- dodges the trap
  and is a genuine contraction besides.
- ca.md's 'it is' matching inside the already-correct 'it isn'\''t':
  tried a central .vale.ini TokenIgnores entry first per the
  allow-everywhere framing, but testing against the real file (not
  just a synthetic snippet) found it silently fails to suppress
  whenever markdown emphasis syntax appears earlier in the same
  file -- an offset-drift bug in how Vale applies TokenIgnores, not
  a config mistake. Reworded to "it'\''s not" instead, same fix
  shape as persistence.md.
- config.md's 3 genuine Microsoft.Avoid 'backend' exceptions (already
  flagged and accepted on #4139 -- an actually-pluggable LLM API
  provider, matching the nix option's own name, not one internal
  system to name): scoped inline vale suppression around just that
  section, since this one really is context-specific rather than a
  rule bug.

Verified: fresh 'vale docs/ --minAlertLevel=error' is 0 errors AND
0 warnings (was 10 errors). nix fmt 0 changed beyond the edits
themselves. pre-push lints (tracker-tag/comment-block/doc-pointer)
clean. cargo clippy -p swarmctl -- -D warnings clean. Diffed the
regenerated swarmctl-cli.md against the old copy to confirm only
the intended line moved.
2026-09-09 22:55:28 +02:00
damocles
a4f72365c7 check-issue-refs: catch full forge issue URLs too, drop internal links from docs entirely 2026-09-09 21:15:28 +02:00
damocles
e1e913015d check-issue-refs: blanket-ban tracker tags in markdown too, no exceptions 2026-09-09 21:02:48 +02:00
damocles
786ab304d6 check-issue-refs: catch ambiguous owner#N cross-repo refs in markdown too 2026-09-09 20:21:35 +02:00