`credential.rs` calls `SecretStore::from_env`, and nothing set `BAO_*` for
this unit — only `swarm-bao-matrix-token` had them. Fixing that alone would
not have been enough: hive-c0re runs as hive-core, and glue-bao-tls mints the
client key `0600` inside a `0700` directory, so the daemon cannot read the
file even when it is named.
Both halves go through LoadCredential, which this unit already uses for the
swarm-status client secret: root reads the identity at unit start, hive-core
sees it under `%d`, and no second on-disk copy exists.
The gate is the identity, never `deploy.bao.enable` — a hive that reads a
store on another machine holds a certificate and runs no store. Four
module-eval cases: the co-located and off-host presence arms, the absence arm
for a hive with no identity, and a presence/absence pair for the optional CA.
The only one of the 28 without one. Records the things a reader cannot get
from the source: why the field name is pinned by a literal-string test (its
other end is a shell line in a nix module, unreachable from any Rust test),
why the BAO_ spellings are read explicitly rather than left to vaultrs's
VAULT_ defaults, and that cert_role is the hive's own name because the
cert-auth role matches on the CN the glue module mints.
mara on #4015: "not merging code without callers", and on the same PR
"see issue, we decided what the first thing should be". #3726 decided it:
the controller writes a token to the store and tells the hive; the hive
reads it back and writes /agents/<agent>/state/matrix-token-<account> at
0600, where matrix.nix's existing systemd.paths glob re-fires the daemon.
So this is the hive half of that, and the library's first caller.
The notice names a credential and never carries one, and deploy_subject's
own doc is why: the auth-callout responder scopes publish and leaves sub
unrestricted, so a hive that wanted another's messages could subscribe to
them. A secret in that payload would be readable swarm-wide. The value is
read from the store under the reading hive's own certificate, where the
store's policy is what actually scopes it.
Two boundaries guard the two addresses, and they are not the same check.
`path::matrix_account` guards the address in the store. `Ident` guards the
address on disk -- `agent_state_dir` takes one, so an unvalidated name off
the queue cannot reach a directory. I had written the first and assumed it
covered both; the compiler refused the `&str` and was right. `token_path`
now takes the newtype so a call site cannot forget.
The write is atomic because the path-watcher fires on the file appearing:
written in place it would be visible while partial, and the daemon would
read a truncated credential exactly once, which is the hardest possible
failure to reproduce. The temp name is dot-prefixed so it cannot match the
`matrix-token*` glob on its way past.
The publish grant is here because without it the failure is invisible.
policy.rs already says why for its siblings: a refused publish reaches the
client as a timeout, so the symptom is a hive that never receives a
credential with nothing in either log naming a permission. Two tests: the
controller may publish, a hive may not -- its own subject included. A
forged notice leaks nothing, but it would make a hive fetch and overwrite
a token file for a name the forger chose.
Refs #3726
/knowledge/doc-standards.md lists "file added -> CLAUDE.md ## Repo map" under
always-update, and "CLAUDE.md after a file rename" under most-commonly-missed.
This branch added a workspace member and I missed it; a scheduled nudge to
re-read the hive rules is what caught it, not a gate.
The entry leads with the BAO_ vs VAULT_ env mismatch because that is the part
a reader cannot derive from the crate name: vaultrs' own defaults look for
VAULT_ADDR / VAULT_CLIENT_CERT / VAULT_CLIENT_KEY, no unit in this tree sets
those, and falling through to them produces a client with no identity whose
only symptom is a TLS handshake failure.
Measured while doing it: 6 of 28 workspace members were absent from the map.
Five predate this branch (hive-jobq-metrics, swarm-authelia-bridge,
swarm-authelia-bridge-sock, swarm-nats-auth, swarm-queue-client) and are
deliberately left alone here rather than widening this PR.
Refs #3726
mara ruled (a) on #3726: a thin workspace crate over `vaultrs` rather than
keeping bao access in nix and having each end trigger units. The HTTP is the
SDK's job; what this crate owns is the things the controller and a hive must
say *identically*, and which have no other home because neither end is senior
to the other.
Three such agreements:
`path::matrix_account` builds where a credential lives. It is fallible rather
than a `format!`, because both names reach it from elsewhere -- the agent name
from the topology, the account name from an agent's own config -- and a `/` or
`..` in either does not produce a malformed path, it produces a valid path to
a *different agent's* secret. The charset mirrors the KV bucket-name rule.
`Credential`'s `value` field is not a free choice: glue-matrix-bao-token.nix
reads the store with `bao kv get -field=value`, so the name is load-bearing
for a consumer no Rust test can reach. A test pins the serialised shape.
`client::Settings` reads BAO_ADDR / BAO_CLIENT_CERT / BAO_CLIENT_KEY /
BAO_CACERT explicitly instead of letting vaultrs fall through to its own
defaults, which look for VAULT_ADDR / VAULT_CLIENT_CERT / VAULT_CLIENT_KEY.
Every unit in this tree sets the BAO_ spellings, so the defaults would yield a
client with no identity at all -- surfacing as a TLS handshake failure, which
names neither the missing variable nor the reason.
The env read is split from the connect so every misconfiguration arm is
testable without a reachable store and without touching process-global env.
Dependency impact, measured against the lock at forge/main rather than assumed:
native-tls 0 -> 0, openssl-sys 0 -> 0, one reqwest (0.13.4) which vaultrs
shares, and 10 new crates that are all derive/proc-macro helpers.
Refs #3726
The swarm collector reads /var/log/journal and has never seen a single
container unit. mara's count-by-unit against VictoriaLogs returns four
units, all host-tier; openbao -- which runs inside the swarm-bao
container -- is absent.
Cause: nixos-containers.nix hardcodes `--link-journal=try-guest` for
every non-ephemeral container. With `guest`, the host's
/var/log/journal/<machine-id> is a SYMLINK into the container's transient
root; a reader in the host namespace cannot follow it, and it dangles as
soon as the container stops. `ls -la /var/log/journal/` on the host shows
one real directory and a pile of `-> /tmp/nspawn-root-*` links dating
back to May.
swarm-otel.nix asserted the opposite, and that assertion is why the
receiver's path was considered sufficient: it said the files "live here"
and are "bind-mounted into the guest rather than the other way round".
That describes `--link-journal=host`. The same sentence names the flag we
actually use. The flag was right and the behaviour it described was not,
so grepping for the flag confirmed the comment and taught nothing.
`containers.<name>.extraFlags` feeds EXTRA_NSPAWN_FLAGS, which the
invocation expands after the hardcoded flag, so `--link-journal=host`
wins. The comment now describes what the code does instead of the other
way round.
Two payoffs, and the smaller one is the one the issue is about:
container logs become collectable, and -- independently -- they become
durable at all, rather than dying with the container.
Ten identical edits because ten host-modules hand-roll their own
container block; that duplication is #3773, not something to invent an
abstraction for here.
NOT VERIFIED: that systemd-nspawn honours the last `--link-journal` of
two. Everything else here is read out of nixpkgs; that step is a claim
about its argument parsing which cannot be exercised without starting a
container. It is settled by deploying one and re-running the `ls`: the
machine-id entry becomes a real directory instead of a symlink.
Refs #3849
`ci-log --run <n> --job <idx>` accepted any index and exited 0. Past the
run's job count it printed job 0's log, with nothing marking the
substitution. Measured against run 3363 (4 jobs): indices 0-3 gave three
distinct md5s, while 99 and 12345 both returned output byte-identical to
job 0.
Both web log routes clamp an out-of-range index to job 0 and answer 200.
I had claimed nothing in the response distinguishes them -- that was
measured on the body only, and the body was the wrong place to look. The
`Content-Disposition` header names the job the server actually served:
--job 3 -> filename="ci-doc-pointer lint-8363.log"
--job 99 -> filename="ci-nix flake check-8360.log" (job 0's)
So the fix is to report what came back rather than to pre-validate the
index. Establishing the real job count needs two extra API calls on every
indexed read -- `ActionRun` carries no job count, and the only job
endpoints are the per-run listing and a per-job log keyed by internal job
id, not by index. The header costs nothing: it is already in the response
being read.
The provenance line goes to stderr, so it cannot corrupt a piped log;
under `--json` it is a `served` field instead. An out-of-range `--job` is
therefore no longer an error -- it is a read whose true subject is named.
`get_bytes_named` is a sibling of `get_bytes_raw` rather than a signature
change, leaving the attachment and artifact download paths untouched.
Rebased onto main after the two flat-rename PRs landed; the only conflict
was the test module's import list, resolved by keeping both sides. While
reading the surrounding context this commit's own defect surfaced:
`check_status`'s doc comment had been left glued to the head of
`disposition_filename`'s, so one function carried two unrelated
descriptions and the other carried none. No gate can see that -- it is
well-formed rustdoc either way.
Both were prose about the model this branch replaces, caught in review.
`swarm-controller/src/wanted.rs` was the worse of the two: its header
called the lifecycle "deliberately identical" to `status` and the handle
"resolved on first use and cached", while `store`'s own doc seventy lines
below says "resolved per call rather than cached". One file, two
contradictory claims, and the `OnceCell` that would have settled it is
gone. Rewritten to say where the mirror stops rather than to patch the
stale clause, since the divergence is the point of the change.
`hive-c0re/src/workers/wanted.rs` named a `hive-wanted` bucket that no
longer exists.
Swept by content rather than fixing only the two that were named: the
sweep surfaced a third candidate, `swarm-nats-auth/src/policy.rs`'s
"one key per hive", and reading it cleared it — that sentence is about
the hive-status bucket, whose shape is unchanged. Left alone
deliberately.
`docs-rustdoc` fails on the `//!` header: `unresolved link to bucket`,
for an item declared in that very module. The module's docs are merged
from two fragments — the `///` on `pub mod wanted;` in `lib.rs` and the
`//!` inside the file — and the merged doc does not resolve a bare item
name against the module. Every other link in the file was already
fully-qualified (`crate::status`, `crate::status::BUCKET`), so this one
was the outlier rather than the convention.
Evidence, same command before and after: `cargo doc -p swarm-queue-client
--no-deps --document-private-items` went from 1 occurrence of the error to
0, while the unrelated diagnostic in that narrower build stayed at 1 —
the surviving one is the control that the lint is still armed. The
CI-shaped run (`--workspace`, which is what unifies features) exits 0 and
documents 27 crates including this one.
Refs #4006.
A hive reads its own declaration today and that scopes cleanly: DIRECT.GET
carries the key in the subject, so the grant can name it. A *watch* cannot be
scoped that way — a consumer's filter travels in the request payload, so
$JS.API.CONSUMER.CREATE.<stream> grants the whole stream. With every hive in
one bucket, letting a hive watch its own declaration would let it read every
other hive's.
One bucket per hive (hive-wanted-<hive>) makes the stream a hive may hold
exactly as wide as what it is allowed to see, which is what #4006's live-watch
needs. That watch is a separate change; this only moves the boundary.
mara's calls, both on #4006: one stream per hive rather than teaching the auth
responder a hive roster, and a wildcard for the controller — "its okay if
swarm controller can theoretically override hive". A bucket name is a single
subject token with no prefix matching, so no wildcard narrower than * covers N
per-hive buckets; the controller's grant is account-wide by consequence, and
documented as chosen rather than left to look accidental.
The reader arm of #4005's key-layout guard asserted the opposite of that
ruling, so it is replaced rather than deleted: the hive arm survives as
no_hive_may_write_another_role_s_agent_status (with a positive control), and
the_readers_grant_is_deliberately_account_wide pins the decision and names the
ruling, so the width reads as chosen to whoever finds it next.
Two pre-existing negative assertions were silently defanged by the rename --
they matched hive-wanted.beta and $KV.hive-wanted.alpha, strings nothing
produces any more, and kept passing. Both now match current names.
swarm-controller resolves the store per hive per call instead of caching one in
a OnceCell: there is no single handle that serves N buckets, and declarations
change on operator action rather than per tick.
CHECKPOINT, NOT A PROPOSAL. mara paused the plan ("i dont quite understand
the plan") before any behaviour-changing edit; this commit exists so the work
survives a container stop, not because it is ready.
wanted.rs: BUCKET -> BUCKET_PREFIX + bucket(hive) producing hive-wanted-<hive>;
open_or_create/open_read_only take the hive. lib.rs: Error::CreateBucket.bucket
becomes String, since a per-hive name is built at runtime.
Nothing else is touched, so no caller compiles against the new signatures yet
and no deployed behaviour changes. Remaining, if she approves: the two other
CreateBucket sites, swarm-controller's OnceCell (one store -> N), hive-c0re's
pull, policy.rs per-hive streams + the reader roster, swarm-nats.nix.
argus's #3989 review caught it: a <details><summary> line isn't
markdown-processed, so backticks in it render as literal characters
rather than styled code. Worth banking now, before it gets rediscovered
per-file across the other ~19 docs #3902 still has to touch.
mara: 'separate technical status from agent provided status string' —
the agents page's status column concatenated the freshness label with
the agent's own free-text status_text into one Badge pill, meant for a
short discrete label, not a full sentence. Long status strings blew
the row out and made the table look messy.
Now two columns: 'status' is just the freshness badge (fresh/stale/
never reported/not in swarm identity) plus relative time, unchanged in
meaning; 'message' is the agent's own status string as plain prose in
its own capped-width, wrapping cell (Table's new cellClass, ui-table-
prose), not a badge. A stopped agent (status_text always null per the
wire contract) shows an em dash there instead of a stale message.
Verified against a static render of the built CSS (real dark theme,
real Badge/table classes, the exact long strings from the screenshot
mara attached) — wraps cleanly within the column instead of
overflowing. Typecheck + build clean.
hyperhive#4008, mara: 'add a setting to not display verbose output...
like the grey colored debug stuff.'
Same shape as the existing expand-tool-output preference
(ExpandDetailsSetting.tsx): a new shared/src/prefs.ts key pair
(getHideDebugPref/setHideDebugPref), a new settings-menu-row component
owning its own state (HideDebugSetting.tsx, not a prop threaded through
the shared SettingsMenu component — per mara's earlier review on the
first one, more per-page options as props there is how that component
accumulates cruft), mounted next to ExpandDetailsSetting in Root.tsx.
Row.tsx skips (returns null for, not CSS display:none) any TermMsg
whose level is 'debug' when the pref is set — matches the muted 'debug'
row this issue is about (see docs/web-ui/terminal-rendering.md's Levels
table). Read live per-row, same as expand-details, so toggling applies
to newly streamed rows in an already-open tab without a reload; already
-rendered rows are unaffected either way, same non-retroactive
precedent the existing preference already sets.
hyperhive#3902, mara: option (a) - a content pass splitting mixed docs
into operator-facing content plus implementation detail. One file
first, to agree on the split pattern before doing the other ~19.
Went through 3 shapes on review before landing here: a sibling
-internals.md file (mara: clutters the navigation), then two
tree-precedent alternatives damocles raised (subdir+README like
web-ui/; or only split docs with a pre-existing boundary marker,
which would've covered 3-4 of the ~20 flagged docs and left the rest
untouched), then mara's own proposal - a collapsed <details> section
in the same file. Verified empirically (cmark-gfm --unsafe, the
website's own render pipeline) that markdown headings nested inside a
<details> block still parse as real headings with heading-id anchors
once separated from <summary> by a blank line, so anchor links into
the collapsed section keep working.
github.md keeps enabling/provisioning/security up top; its
'Implementation' section is now a <details> block holding what was
briefly a separate github-internals.md (deleted again) - how the
agent's gh/git-push actually authenticate, and the notification
poller's internals. Reverted the two cross-references + the
docs/README.md entry back to pointing at github.md now that the
content lives there again.
Added a short CLAUDE.md note recording the pattern per mara's ask,
including the one real caveat damocles flagged: <details> only
collapses in a rendered browser, a raw-text read (cat, the Read tool)
still sees everything, same as today.
Second round for the flat-alias deprecation. `pr-commits` and
`pr-assign-reviewer` are back-compat aliases with an exact scoped
equivalent, and they were missed by all three buckets of the original
accounting -- which was wrong in both terms: `reopen`/`dependency`/
`reaction` appear only in the doc-comment prose listing the scoped
surface and are not Verb variants at all, so the real count is 17
(7 + 2 + 7 + 1), not 18.
The test for "pure rename" is that an exact scoped form exists AND the
verb is PR-only, so a caller holding just a number can be sent somewhere
unambiguous. Verified rather than assumed: `pr commits` and
`pr assign-reviewer` exit 0, while `issue commits` and
`issue assign-reviewer` exit 2 -- the same code a deliberately bogus
verb returns, which is what makes those 2s mean "no such verb".
That is also why the seven kind-agnostic aliases are untouched here:
each exists as both `issue <verb>` and `pr <verb>`, so refusing them
would remove a capability rather than rename one.
Both run functions are still called from pr_cmd.rs, so nothing becomes
dead code. The enum's block comment enumerates the pure renames by name
and is extended to match -- left alone it would be a stale claim naming
seven of nine.
An agent whose freshness is "unknown" is a distinct case, not a
fallback: it reported into the KV bucket but isn't in the swarm-identity
roster. The label used to just reuse the enum name ("unknown"), which
doesn't tell an operator what to do about it. Give it a label that
names the actual situation ("not in swarm identity") plus a tooltip
spelling out the fix (register/migrate it), so an operator scanning the
roster during a migration can immediately tell which agents still need
work.
No backend change needed — GET /api/agents/status already emits a row
for every hive-reported agent outside the roster (AgentStatusReader,
unit-tested as an_agent_outside_the_roster_is_surfaced_as_unknown), and
the page already renders every row it gets back.
A KV entry publishes to $KV.<bucket>.<key> and NATS wildcards match whole
.-delimited tokens, so a /-joined {hive}/{agent} key is a single token: the
only expressible write grants are one exact subject per agent (needs a roster
in the auth responder, which Policy::hive_name argues against) or a bucket-wide
wildcard that lets any hive overwrite any other hive's agents.
Joining with a dot puts the hive in its own token, so hive_subjects can grant
$KV.agent-status.<hive>.* — every agent of one hive and nothing else, the same
containment hive-status already has.
The grant lands with the bucket-open pair (STREAM.INFO + STREAM.CREATE):
open_or_create resolves the bucket before it writes, so alone the publish
subject is unreachable and the sweep fails one step later instead.
Verified the client accepts a dotted key rather than assuming it: async-nats
0.50.0 VALID_KEY_RE is \A[-/_=.a-zA-Z0-9]+\z and is_valid_key rejects only
empty / leading / trailing dot; the subject is prefix + key verbatim.
hyperhive#3896. The backend for start/stop (Up/Offline wanted-state
declarations) already existed and was merged (#3905's writer, the
PUT /api/hives/{hive}/agents/{agent}/state route) — nothing here was
waiting on Paused/Destroyed, which I'd mistakenly conflated with this
issue in an earlier comment (that's #3803, a different feature).
swarm-controller: merges each row's declared wanted state into
GET /api/agents/status, same shape as the config_pr merge (one read
per distinct hive, not per agent, since a declaration is a hive's
whole agent map).
swarm-ui: AgentsPage gets a "wanted" column — clicking the current-
state badge toggles it (Badge's own chip-plus-control shape, same as
its own header comment's pause/resume example), backed by the PUT
route above. A row with no declaration yet reads its implied current
state off the agent's own last-reported running flag. Stop asks for
confirmation (native window.confirm — no confirm-dialog component
exists in swarm-ui yet); start doesn't.
The swarm-controller had no grant for the per-agent status bucket, so
resolving it failed and the dashboard answered 503.
Grants the reader the same five subjects the hive-status bucket already
has, for the same measured reasons -- including BOTH `CONSUMER.CREATE`
forms. `keys()` builds an ephemeral consumer whose create subject carries
no consumer name, and `>` matches one or more tokens, never zero, so the
`.>` form alone does not cover it; that omission surfaces as a client
timeout and an operator-visible 503, which is the shape reported here.
Deliberately grants NO `$KV.agent-status.*` subject, to anyone. That is
the write side, and writing is what picks a key layout -- whether agent
status is published by the agent or forwarded by its hive, and under
which key, is still open. A write grant here would answer that by
implication. Reading needs none of it: a KV read is a DIRECT.GET, and the
`.>` form is bucket-wide rather than per-key, so nothing added here
encodes a layout. A test asserts that, for the reader and for a hive,
with a control proving the same matcher fires on a bucket that does have
write grants.
The stream name is derived from the crate constant, like the wanted-state
bucket beside it, so writer and reader cannot disagree about it.
dep_meta's comment records a measured 404: leaving owner/repo as None
made every same-repo dependency fail, because forgejo resolves the
dependency's repo from the request BODY rather than the URL. Nothing
stopped a later tidy-up from simplifying them back to None.
index() promises "error instead of wrapping". A wrap would not fail
loudly -- it would hand the forge a negative index it looks up as some
other issue.
Rust's runtime sets SIGPIPE to SIG_IGN at startup, so writing to a pipe
whose reader has gone away returns EPIPE and println! panics. `hive-forge
<verb> | head` printed a panic and exited 101 where cat, grep and every
other pipeline member exit quietly.
101 is not cosmetic: to a caller running `set -o pipefail` it is a real
failure, so a shell script that pipes our output stops on a condition
that is not an error.
Restore SIG_DFL first thing in main, before any output. Measured against
the same command: piped to head, 141 (killed by the signal) with empty
stderr; unpiped, 0; the pre-change binary, 101 with a panic.
Closes#3972
Seven hidden aliases are pure renames — `pr status` does exactly what
`pr-status` did. They now refuse and name the replacement instead of
running, which is the deprecation step before removing them outright.
Deliberately not all of the hidden verbs. The other ten (`view`,
`comment`, `comments`, `close`, `reopen`, `assign`, `labels`, `timeline`,
`dependency`, `reaction`) exist as BOTH `issue <verb>` and `pr <verb>`,
so a caller holding only a number cannot be told which to run. Refusing
those removes a capability rather than renaming one, and there is no
message that would help; they keep working pending a decision on what
replaces them.
Measured: each of the seven exits 1 naming its replacement; `view` still
exits 0; `pr status` still returns its merge-readiness verdict (0 on a
ready PR, 1 on one with CI pending).
Refs #3974
Filtering the gateway's journal by severity returned nothing while nginx
was logging [error] continuously. Measured against nginx.service:
`-p warning` and `-p err` over 24h both said "No entries", while the
unfiltered query returned a continuous stream of [error] lines.
The failure is inverted rather than merely missing. A severity filter is
how you ask "is anything wrong", and here it answered "no" *because* the
errors were misfiled -- one step from recording "gateway clean, no
warnings in 24h".
Cause: the module default is `error_log stderr`, and systemd files that
whole stream at one priority regardless of what each line says. Routing
to syslog preserves per-line severity, so [error] lands as err and every
existing severity filter starts working without changing a consumer.
Uses the module's own `logError` option rather than appendConfig:
nixpkgs emits `error_log ${cfg.logError};` at exactly one site, in the
main context, so the option guarantees the placement. Transport only --
no level suffix, so nginx keeps its default `error` threshold and this
changes where lines are filed, not which lines exist.
Gated with `nginx -t` on the flake-locked nginx against the exact string
shipped here, with a deliberately invalid syslog target as the control
that the test can fail on this directive. gixy (what the module's own
validateConfigFile actually runs) reports no issues.
What that does NOT prove: that journald files the lines at the right
severity. `nginx -t` is a parse check; only a deploy shows the effect.
An option name that is absent reads exactly like one that is present —
the reader has no way to tell, and the failure surfaces as a nixos eval
error at deploy time. Two namespace reorgs the prose never followed:
`forge.*` moved under `swarm.`, and several flat paths under `c0re.`.
docs/networking/gateway.md:335 services.hyperhive.forge = { … }
docs/networking/gateway.md:367 forge.rootUrl → swarm.forge.rootUrl
docs/networking/gateway.md:389 frontend → c0re.frontend
docs/tools/hivectl.md:193 agentCpuQuota → c0re.agentCpuQuota
docs/tools/hivectl.md:317 forge.behindGateway → swarm.forge.behindGateway
docs/agent-lifecycle/persistence.md:404 agentCpuQuota → c0re.agentCpuQuota
docs/web-ui/dashboard.md:610 modelPrices → c0re.modelPrices
docs/web-ui/dashboard.md:789 forge.publicUrl → swarm.forge.publicUrl
Both `gateway.md` blocks are fenced nix an operator is meant to paste.
Two sites needed more than a rename:
dashboard.md:781 cited `gateway.enable` as a live opt-in. It was
removed; `gateway.md:269` already says the gateway is unconditional,
and `hive-c0re/environment.nix:208` sets HIVE_GATEWAY_ENABLED = "1"
with no condition. Rewritten to say so, keeping the flag-absent
fallback the dashboard still implements.
turn-loop/config.md:4 attributed the page's knobs to
`services.hyperhive.agents.<name>`. No such option exists — every
knob the page documents is agent-side `hyperhive.*`, set in the
agent's own agent.nix.
Measured against the module system rather than a grep: `nix build
.#docs` renders nixosOptionsDoc over the evaluated trees (279 option
paths). That arm alone is wrong — it omits `internal = true` options
and called `gateway.localNames` and `swarm.peerHives` dead, so a second
arm requires the literal path to appear in nix/ and rescues exactly
those. Four remaining unresolved citations are correct prose saying
`gateway.enable` / `network.{enable,isolateContainers,upstreamDns}`
were removed, and are left alone.
Closes#3948
The runs-list query's `ref` filter matches only a fully-qualified ref,
while the listing prints `prettyref` (`main`, `#3967`). So no value a
caller can read off the output was accepted by the filter, and the
rejection rendered as "no matching runs" — indistinguishable from a
branch that has never been built.
Qualify the value before it reaches the query: `refs/...` passes
through, `#N`/`N` becomes the pull head ref, anything else becomes a
branch. A slash cannot mark an already-qualified ref, since branch
names contain them.
Not shared with ci-rerun: its `ref` is a workflow_dispatch body field
taking a bare branch name, so the same normalisation would break it.
Closes#3970
docs/tools/swarmctl-cli.md is rendered by `swarmctl markdown-docs`, and
nix/checks.nix's swarmctl-docs-fresh check diffs the committed copy
against fresh binary output. Enabling prettier on markdown rewrote its
list bullets and footer indentation, which no regeneration can settle:
formatting it fails the freshness check, not formatting it fails
treefmt. .prettierignore already carried hivectl-cli.md for exactly
this reason; swarmctl's doc was added later and the entry was not.
Restore the file to its generated bytes, list it alongside hivectl's,
and state the invariant where the next CLI doc gets added.
`docs/scheduler/ci.md` told the reader to run `nix develop -c treefmt`.
treefmt is not on the devshell PATH — `nix develop -c which treefmt` exits
1 while `which cargo` resolves, so the check discriminates and the
documented command has never worked from that shell.
The same line enumerated the formatter set as "rustfmt + nixfmt + taplo",
which was already wrong before this branch: keep-sorted has been enabled
all along, and the commit before this adds prettier. Replaced the list
with a pointer to nix/treefmt.nix so it can't drift out of sync again.
Also drops "+ rustfmt" from the nix-flake-check row: rustfmt is one of
treefmt's formatters, not a separate thing that runs alongside it.
Pure `nix fmt` output from the commit before this one — no hand edits.
203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs.
Reproduce with `nix develop -c nix fmt` on the parent commit; the result
should be byte-identical to this tree.
None of the 13 `.prettierignore` entries appears here — verified by
intersecting the changed-file list against the ignore file, with a
control proving the intersection finds a match when one exists.
`nix/treefmt.nix` gated nix, rust and toml and nothing else, so markdown,
css, js/ts/tsx, html and json had no formatting gate at all. Operator
asked for "the web stuff to be formatted as well" (#1997).
This commit is the one-line enable; the reformat it produces is the
commit after it, kept separate so this one stays reviewable.
Scope is treefmt-nix's `programs.prettier` default rather than a
hand-written `includes` list. The default is wider than the requested
md/css/js/ts/tsx/html/json — it also covers yaml, scss, vue, mdx, json5,
cjs/mjs/jsx — but measured against this tree that width costs exactly 3
extra files, all `frontend/packages/*/build.mjs`, which are web build
scripts. `.forgejo/workflows/{ci,coverage}.yml` are in the default set
and already conform, so they reformat to nothing today; from here on they
are gated too, which is what a repo formatter is for.
`.prettierignore` is honoured: prettier applies it even to the explicit
paths treefmt passes, so the `hivectl markdown-docs` generated file and
the 12 files awaiting manual list-item re-wrap stay untouched.
`age_seconds` is documented on the LooseEnd enum as saturating to zero on
any clock anomaly, but the derivation was in three places: hive-c0re had a
named `saturating_age` helper with tests, and the in-agent socket server
hand-rolled the same two lines twice, untested.
Move the helper to hive-sh4re::inbox, beside the enum whose contract it
implements and inside the one crate both producers already depend on. Its
three tests move with it (not dropped) and gain two arms: the whole-i64
range, where the saturating_sub is what stops the subtraction overflowing,
and a far-past control so those zeros are the clamp firing rather than the
function bottoming out on large inputs.
The two clamps are not redundant, which is what `to_loose_end`'s doc got
wrong: it credited "saturating" for the zero, but saturating_sub bottoms
out at i64::MIN, still negative. The try_from is what yields 0.
Also cover the two projections themselves, which is the part the shared
helper cannot: that a reminder ages from created_at rather than due_at,
and a todo from updated_at, with a future timestamp reading 0 through
both and a past-timestamp control on each.
`nix/templates/harness/` is cited from `send_allow.rs` and twice from
`mcp_config.rs`, in the present tense, as where the NixOS module writes
the send allow-list, writes the extra-MCP spec, and defines the MCP http
port default. There is no such directory — `nix/templates/` holds only
`agent.nix` and `ruth.nix`.
All three describe things that live in `nix/agent-modules/mcp.nix`:
send-allow.json is written at :230, extra-mcp.json at :228, and the
`hyperhive.mcp.httpPort` option is declared at :143.
These are the mechanical half of #3923, which explicitly carved the
repointing out from the question of whether the reference gate should
widen. Nothing here changes that question.
Per mara's review call on this PR: "the view should be filled by a single
backend call." AgentsPage.tsx was doing three fetches (/api/agents,
/api/config-prs, /api/agents/status) and joining them client-side by name.
Moves the config-PR join server-side instead: AgentStatusRow gains a
config_pr field, populated by get_agents_status's handler from
AppState::config_prs after agent_status::AgentStatusReader::view() returns
- not inside that module, which has no forge client and stays that way (see
the field's doc comment for why the handler is the right layer for this
merge, not the reader).
AgentsPage.tsx now does exactly one fetch and no client-side joining at all
- the wire row is the table row. Dropped the separate AgentStatusRow TS
interface (folded into AgentRow, which now mirrors the backend type
field-for-field) and the /api/agents + /api/config-prs fetches entirely;
neither is needed once /api/agents/status already returns every roster
agent with its config PR attached.
ConfigPrStatus gained Deserialize (previously Serialize-only) since
AgentStatusRow derives both and a struct's derive requires every field to
support it.
Continues #3341 item 3, unblocked now that #3568/#3569 (items 1/2)
are merged and GET /api/agents/status is live.
Third fetch alongside the existing roster + config-PR ones, joined
client-side by name same as the config-PR merge. Adds a hive column
and a status column (freshness badge + status_text + relative-time,
same rendering AgentsPage's sibling HivesPage already uses for the
hive-level status endpoint).
Its doc justified the pub with "so socket_server and socket_server can
populate AgentMeta" — the same module named twice, and both of those call
read_agent_status_live instead. The bare function has exactly one caller,
the wrapper eleven lines below it. container_view is not a pub module and
this is a binary crate, so the pub only ever granted sibling access that
nothing took.
Also splits the parsing half of read_meta_locked_revs into
parse_locked_revs, which needed a flake.lock on disk to exercise, and
tests it: alias-to-rev mapping, a follows input (an array of path
segments, which is why that arm is a continue), a node with no rev, and
the malformed shapes that must yield an empty map rather than panic.
Coordinator::transient_snapshot builds a fresh HashMap on every call, so
each /api/state render iterates it under a different hash seed and the
dashboard's transient rows reorder while an operator watches an operation
run. Nothing downstream sorts them.
build_port_conflicts, three functions up the same file, already sorts both
its levels. This is the asymmetry, not a new convention.
Sorted by (name, kind, secs) so ties cannot drift either, plus five tests
on a builder that had none. The clamp one is the reason to bother: `since`
in the future yields a negative duration, and cast_unsigned would render
that as an age near u64::MAX rather than zero.
The pinned nixpkgs cannot evaluate prettier: its closure carries
pnpm-9.15.9, which is marked insecure for seven CVEs, and the refusal
happens at evaluation. That breaks nix fmt and the formatting flake check
for every language at once, not just prettier's, which is what blocks
wiring prettier into treefmt.
Measured against both pins, prettier.outPath only:
569d5785 refused, suggesting permittedInsecurePackages
5dfba623 /nix/store/7ryzvaaks7m71lilhzalcbay57nqqm78-prettier-3.8.3
Prettier is 3.8.3 on both. What moved is the pnpm in its closure, not the
version.
Landed on its own so a two-month pin bump gets its own CI run and its own
bisect point, separate from the treefmt change that needs it.
Two more statements of the claim the previous commit removed: the
snapshot field called the clusters "sub-agents", and WEB_PORT_BASE's own
doc called the range sub-agent-only while the function that reads it
says "every agent including the manager" three lines below.
docs/process/gotchas.md already had this right.
The doc comment said the manager sits at a fixed 8000 and therefore
cannot collide with a sub-agent. No such special case exists:
ContainerView.port is agent_web_port(name) for every container, and that
function hashes every name — including the manager's — into one range.
The only 8000s in the tree are test-fixture arguments in meta.rs.
The code was always right; the comment would send an operator hunting a
detector bug instead of renaming an agent. It now points at the allocator
that owns the rule, and the manager-collides case is a test rather than a
sentence.
Nine tests on a module that had none, covering the cluster grouping, the
name sort, the port ordering, and that a stopped container still claims
its port.
Mutation testing found this arm untested: classify_tool_result has its
OWN `<= 120` deciding icon-vs-body, separate from the one in
summarize_tool_result that picks the summary text. The existing case
used 2 and 500 characters, so neither arm went near the boundary and
moving it to 119 changed nothing.
Verified the new test fails under exactly that mutation.
clippy::format_collect (pedantic, denied) fires on
`(0..40).map(|i| format!(...)).collect::<String>()`. The test only needs
40 non-empty lines, not numbered ones, so `"line\n".repeat(40)` says the
same thing without the lint.
`r.find(':')` returns a BYTE offset; it was being passed to
`r.chars().take(colon.min(9))` as a CHARACTER budget. For a multi-byte
room-id local part the two disagree, and the extra characters come out
of the server half:
fmt_room("!ÄÖÜ:server") == "!ÄÖÜ:se" // want "!ÄÖÜ"
The doc comment above it already said the `chars().take()` was there to
handle non-ASCII, so the intent was recorded and the implementation was
half of it. Split on the colon first, then take 9 characters of the
local part.
ASCII behaviour is unchanged and pinned by the existing case:
`!abcdefghijkl:server` -> `!abcdefgh` before and after.
Found by the tests in the previous commit — written red, then fixed.
check-doc-refs.sh's relative arm resolved every link against the linking
file's directory. That is right for markdown and wrong for a .rs doc
comment: rustdoc emits the href verbatim onto a page under
target/doc/<crate>/<module...>/, so the link resolves against THAT
directory instead.
The consequence was a false pass, not a miss -- a link resolving from the
source dir got certified while rendering broken in the published docs.
Reproduced against main's script: a `](../../docs/README.md)` planted in
hive-forge/src/client.rs resolves from hive-forge/src/ and the old rule
exits 0.
Refused rather than resolved rustdoc-aware. The repo has zero relative
links in .rs doc comments (measured on 54daf9ac), so nothing needs the
second reader, and the backticked repo-root form the root arm already
gates renders correctly under both. Inferring module nesting would be
machinery in service of a form nothing uses.
Closes#3928
clippy::items_after_test_module (pedantic, denied) — a #[cfg(test)] mod
has to be the last item in its file. Pure move: same line count, same
line multiset, no content change.
`cargo test` was green while this was broken; only clippy saw it.
Pointing at a doc instead of duplicating its prose next to the code is what
the comment-block lint pushes people toward, and nothing checked that the
pointers still resolve. The reorg that prompted the preceding commit broke
41 references and every one of them merged green.
Two arms: repo-root-relative `docs/` paths, and relative markdown links
resolved against the linking file. Scope is every tracked file rather than a
type list — the dead references were in css, html and an ignore file as well
as in markdown and nix.
Three controls, because a link checker whose extractor quietly matches
nothing passes forever while checking nothing: each arm must have found
candidates, and at least one pointer must have resolved. Any of those failing
exits non-zero and says the script is broken rather than the tree.
Escape hatch is the `lint:allow` marker the tracker-tag lint already uses.
Its only current users are in reference-docs.nix, where a path naming a file
the derivation synthesizes into $out is correct precisely because the repo
does not have it.
Also runs in the pre-push hook alongside the other two.
`docs/` was reorganised into topic subdirectories and the references to it
were not moved with it. Thirteen distinct paths and three relative links no
longer resolved, spread across nix, css, html, js, markdown and
.prettierignore — a stale pointer is not a markdown problem, it is wherever
someone wrote a path down.
Each mapping resolved to exactly one target. `docs/matrix.md` was the sole
ambiguous basename: .prettierignore lists `docs/tools/matrix.md` separately
and that entry still resolves, so the stale one is the integrations doc.
The three relative links were each one `../` too deep — from `<crate>/src/`
two levels reach the repo root. `hive-agent/src/login.rs` already had the
correct form, in the same crate, at the same depth.
.prettierignore is repointed rather than dropped, though nothing in the tree
runs prettier: no treefmt entry, no CI job, no package script. Whether that
config should exist at all is a separate question from whether it names
files that do.