Per mara's ruling on hyperhive#4041 (Microsoft.We): keep the rule enabled,
same treatment as the 'backend' rewrites. Traced all 21 genuine hits (4
gateway.md 'Let's Encrypt' hits are a substring-match false positive, left
alone) to their actual referent: some name a specific component already
established nearby in the same doc (forge_notify, hive-github-notify's
poller, hive-forge, hive-agent/the harness, colors.css, the dashboard),
others were pure filler that adds nothing once dropped.
The section this branch adds was the only source of `Microsoft.Contractions`
errors in the file — 9 of them, and vale over the pre-change bytes still
reports exactly those 9, so the count is attributable rather than inferred
from a repo-wide total that was already red.
One of the nine is not a contraction: vale reads `it is` in "Narrowing it is
tracked in ..." as a subject-verb pair, but it is `it` (the object of
narrowing) plus `is tracked`. Rephrased so the sentence leads with the issue
instead, which drops the false hit and the passive-voice warning together.
A hive reads its agents' credentials with its own certificate, and nothing
said which paths that certificate may read, so the read half of a delivery
answered 403.
The grant is wide on purpose. An agent's path does not name the hive
hosting it -- agents move -- so a per-hive grant has to be an enumeration
the controller re-emits whenever the roster changes, and an enumeration
that can drift or land out of order advertises a boundary it does not
hold. A wide grant that says what it is beats a narrow one that only looks
narrow. mara's call, on the PR: rather a too-lax scope than one that
pretends to be strict.
What that buys, beyond honesty: the document is identical for every hive
and depends on nothing, so it is written once at startup beside the rest of
a hive's provisioning instead of on every declaration. No derived state, no
re-emission, and the ordering hazard that came with one stops existing.
What still holds is read-only. A hive cannot write an agent's credential,
so it cannot hand itself an agent's identity, and the grant reaches nothing
in the store outside the agent-credential prefix.
The fact is documented where someone meets the boundary rather than only in
this message, and the two ways to narrow it later -- scope per hive, or
give agents their own store identity -- are tracked.
The controller writes an agent's credential; the hive fetches it back with
its own certificate. Nothing said which paths that certificate may read, so
the read half of a delivery answers 403 with no way to tell why.
The grant is derived from the declaration, so it is re-rendered at the one
place the declaration changes -- WantedWriter::set -- rather than at its
caller, which would work today and break on the second caller.
Emitted before the KV write: a grant that lands late is a 403 on an agent's
first fetch, while one that shrinks early only affects an agent already
being torn down. A failed write then leaves a superset the next declaration
re-renders.
Destroyed agents are filtered out. The declared set is a hive's whole
history -- a destroyed entry stays so that redeclaring it Up is refused as
the terminal transition it is -- so granting every declared agent would
leave a torn-down agent's credentials readable forever.
The sink is a trait because a missed emission is that same untraceable 403:
the double pins which agents were published, and the no-sink and refusing
arms pin the two deployments that are not a happy path. Not covered: the
call site inside set(), which needs a live queue.
The cert role moves to its own module on the way past. It is the
controller's identity at the store, not something the matrix route owns,
and the policy writer needs the same login.
The renderer beside this produces a hive's read policy; nothing could put
one in the store. `read`/`write` are KV v2 verbs and a policy is not a
secret, so this is a second endpoint rather than another path.
A whole-document write, because the store has no merge verb: the caller
renders from the current agent set, so a stanza missing from the render
is a grant that is meant to be gone.
The controller writes an agent's credential; the hive reads it back with its
own token. Nothing says which paths that token may read, so the read half of
a delivery has no identity at all and answers 403.
This is the pure half of the fix: the policy text is a function of a hive name
and its agent set, so the shape can be asserted with no store to talk to.
One stanza per hosted agent rather than a prefix grant. An agent's credential
path does not name the hive hosting it -- deliberately, since agents migrate --
so "this hive's agents" has no prefix expression and must be enumerated. The
grant is read-only: the controller mints these and never reads one back.
The paths come from MOUNT and AGENT_PREFIX rather than literals, so the policy
cannot drift from the module that builds the paths it grants.
checked_segment runs before any name reaches HCL. That is policy injection
rather than path traversal -- a name can close a stanza and open a wider one --
so the test carries a real injection string, paired with the reachable-charset
control that stops it passing by refusing everything.
An empty agent set renders an empty policy, which grants nothing.
Per mara's ruling on hyperhive#4041 (Microsoft.Avoid): 'backend' never says
which system it means. Fixed all 21 fixable hits to name the actual daemon
(mostly hive-c0re; one hive-agent instance and one hive-matrix-daemon one,
both confirmed against surrounding context, not guessed). Also tightened
Weston's own 'VNC backend'/'RDP backend' plugin references to their literal
upstream names (vnc-backend/rdp-backend) rather than dropping the accurate
term entirely, and reworded one unrelated Microsoft.Avoid hit ('and so on').
Left alone (flagged on the issue, not guessed): docs/turn-loop/config.md's
3 remaining hits match the actual nix option name (backendEnvironmentFile)
and describe a genuinely pluggable concept (which LLM API provider), not
one specific internal system.
cc8fb0ee44 (swarm-bao controller-write-credentials
work) added a fresh 'did not' warning callout in the bao-provision section,
right after #4129 zeroed out the genuine error-level Microsoft.Contractions
backlog, and its passive 'has been exercised' phrasing put getting-started/
back above its documented Passive-pass accepted-residual baseline (10) too.
Contracted the 'did not' and reworded the passive opener to an active one
('This has only run once...') in the same sentence, since both hits were on
the same line.
16 of the 23 error-level hits from a full-repo vale run were real:
'is/are/does/do NOT' and bold 'not' expanded per Microsoft.Contractions,
one hyphen-as-negative-number fixed to an en dash per Microsoft.Negative
(docs/web-ui/dashboard.md's -45 deg column-header tilt).
Where the caps/bold was there for emphasis (contrasting X does this,
Y does not), kept the emphasis by bolding the contraction rather than
dropping it silently -- 'is not' -> 'isn't', 'does NOT' -> '**doesn't**',
etc.
docs/tools/swarmctl-cli.md's one genuine hit is generated -- fixed the
source doc comment in swarmctl/src/main.rs and regenerated via
'cargo run --bin swarmctl -- markdown-docs', diffed against the old
copy first to confirm only the intended line changed.
Left 7 hits unfixed, false positives from the rule matching a
substring rather than the actual pattern it names:
- 4x 'that's instead of that is': every one is the explanatory 'that
is' idiom (~= i.e.), not a contractible subject+verb -- 'that's'
would misparse as a demonstrative-pronoun contraction and change the
sentence's meaning. docs/integrations/forge.md:282,
docs/tools/forge.md:260, docs/tools/hivectl.md:243,
docs/web-ui/dashboard.md:457 (and the swarmctl-cli.md generated
twin of the same category, source left alone for the same reason).
- docs/agent-lifecycle/persistence.md:482 -- 'is not' matched inside
'is nothing' (the same has-not/has-nothing substring trap noted
before in this doc's own git history).
- docs/swarm/ca.md:182 -- 'it is' matched inside the already-correct
'it isn't'.
Per #4128. Remaining errors on main after this: Microsoft.Avoid's 26
hits, already tracked + awaiting a house-style ruling on #4041.
- resolve_credential (+ token_credential / password_fields helpers)
extracted out of put_matrix_account, which was tripping
clippy::too_many_lines (101/100). Both helpers surface plain &str
errors rather than ProblemDetails to dodge clippy::result_large_err on
a private fn (put_matrix_account itself is exempt only via clippy's
avoid-breaking-exported-api default, which does not cover these);
resolve_credential boxes its own ProblemDetails Err for the same
reason, unboxed at its one call site.
- Added unit tests for the extracted logic: main-reserved, token-mode
missing-token / pass-through, password-mode missing-fields / trailing-
slash trim, and two async resolve_credential end-to-end checks
(token mode, unknown mode) that need no network access.
- Dropped PutMatrixAccountRequest's Debug derive to match
hive-c0re::dashboard::matrix_accounts::MatrixLoginForm's existing
precedent of not deriving Debug on a struct carrying a password field.
Extends PutMatrixAccountRequest with a mode field (token, the existing
behavior and default; or password). Password mode has swarm-controller
itself perform m.login.password against the caller-given homeserver
(mirrors hive-c0re's own /api/matrix-account-login for the hive-local
case) and stores the resulting token instead of a caller-supplied one
-- the password is used once, over this PUT, and never stored. Also
adds the 'main is reserved' guard hive-c0re's login form already has,
which swarm-controller had no equivalent of before this.
swarm-ui's link-matrix-account form gets a credential-mode toggle
wired to the same contract: token mode is unchanged, password mode
swaps the token field for user-id + password fields and makes
homeserver required (no hive-side fallback to resolve it against, per
PutMatrixAccountRequest::homeserver's own doc).
Per #4122.
Closes#4124.
The controller's policy granted only the bootstrap paths -- hive cert-auth
roles and hive ACLs. #4113 then made it a secret WRITER, and nothing related
the grants to the paths the code writes, so every matrix token provision
answered 403. The two halves landed on different issues and neither looked
wrong on its own.
`secret/data/` is KV v2's ACL prefix and is absent from the path the code
passes, so matching `swarm-secret-client`'s spelling literally would have
granted nothing. Write-only: the controller mints these and never reads one
back, and a read capability would let it recover every agent's credentials
rather than only replace them.
The gate is the point. Two module-eval arms -- the grant exists and is not
a broader wildcard, and its capability list is pinned whole, because an
ADDED capability is what a presence check misses -- plus a test in path.rs
pinning MOUNT/AGENT_PREFIX and naming the nix file, since renaming either
constant is a silent 403 rather than a compile error.
setup.md carried two warnings this makes false: that nothing in the tree had
ever authenticated to the store, and that no deployment shape mints a leaf
whose CN reads swarm-controller. glue-bao-tls.nix has minted one since #3726
item 1.
The link-matrix-account form's placeholder read 'e.g. primary', which
sounds like it names the hive-provided default account. That account is
actually named main (reserved in hyperhive.matrixAccounts's schema) and
this route can neither create nor touch it. Swapped to an example with
no relationship to the real default, per #4122.
nix fmt table-column-width drift from the earlier comma fix (f3fe0604/82236952)
- shortening that cell by one char left the row under-padded relative to the
table's other rows. Pure whitespace, no content change.
argus's review on this PR caught a real regression: the earlier
passive-voice rewrite ('answers "did the agent come back up?", the
same way...') put a comma immediately outside a closing double-quote,
which vale's Microsoft.Quotes rule flags ("punctuation should be
inside the quotes") -- a brand new error not present on main. The
original phrasing never had a quote directly followed by a comma, so
this was a real side effect of the rewrite, not a pre-existing hit.
Simplest fix: drop the comma. The sentence reads fine without it and
the closing "?" already does the job.
Verified via vale: back to 2 errors (matching main's baseline, down
from the 3 this rewrite introduced), write-good.Passive count
unchanged at 24.
Seventh batch of the ongoing write-good.Passive pass (hyperhive#4042):
read all 65 hits across jobq.md/ci.md/observability.md/coordinator.md
in context and rewrote 41 with a clearly nameable actor -- mostly
hive-c0re, nix/the nix module, the harness, or a specific fn/type
named right there or nearby (coordinator.md's node-inventory table
and DAG-shape descriptions name concrete Rust items constantly, so
the actor is almost always sitting in the same sentence).
Left 24 alone: predicate-adjective-copula state descriptions ("is
stuck", "is gone", "is unaffected", "is done", etc. -- the largest
recurring bucket this batch, especially in observability.md's
scope/status descriptions), negative-capability idioms ("no X is
needed/left", "X can't be written down"), the established "is
tracked as a follow-up" idiom, a firewall-shorthand notation
("bridge->127.0.0.0/8 is dropped") where rewriting would break the
compact rule-like format, a CLI-flag "(repeatable)" annotation ("May
be repeated"), a Rust type-signature fact ("`moves` is typed ..."),
a hypothetical/counterfactual maintenance-burden clause, a
readiness-condition list ("a node is ready when ... every dep is
satisfied"), and one deliberately-parallel idiom pair
("When OTEL is enabled" used identically twice as a section-opening
convention -- fixing one would break the parallelism, not the
opposite).
One self-caught regression: an early attempt to fix "used by every
`Reconcile` node's start action" (a reduced participial clause, not
flagged) into "is used by every `Reconcile` node's start action"
introduced a brand-new flagged passive. Caught by the post-edit vale
count (expected 65->24, got 65->25) not matching, same discipline as
the docs/turn-loop batch's tail-truncation catch -- re-ran with
active voice instead ("Every `Reconcile` node's start action uses
this fallback").
Verified via vale before/after: 65 -> 24 write-good.Passive hits,
exactly the 24 left alone above; error count and other warning
categories unchanged. Re-read every changed line in full surrounding
context after editing before running the final vale check.
Two genuine instruction-verb hits: the container-row selection toggle
and the schedule-table per-target cancel button both said 'click' where
the action also works via keyboard (Enter/Space) or is otherwise
input-device-neutral.
Left the other 4 hits found in the same #4041 sweep alone (false
positives, not fixed here): agent.md's 'ctrl/middle-click' and
'clicks' (real mouse-button-specific browser/VNC behavior, not reader
instructions), shape.md's 'swipes in' (describes the drawer's own
animation, not a user gesture), and process/gotchas.md's
'click-to-unlock' (weston's actual lock-screen feature name).
Second half (lines 762-1389) of dashboard.md's write-good.Passive backlog --
22 rewrites, reusing an actor already named nearby wherever one exists
(the dashboard, hive-c0re, the harness, the operator, the endpoint, the
broker forwarder, CSS, Client, the manual fan-out, ...). Companion to
part 1 (PR #4117, lines 1-697).
10 left alone in this half: five established idioms (config-state
conditionals, negative-capability "nothing is X", state descriptors
"is untouched"/"is gone"), and one literal quoted UI string
("container will be created") that documents exact displayed text
rather than the doc's own voice. One rewrite (naming "the dashboard"
for the old L762 'is known stopped') incidentally leaves behind a new
instance of the same 'is stopped' idiom already accepted at L797 --
21 net fewer hits from this half, not 22, though 22 edits were made.
Together with part 1 (31 rewrites, 7 left alone), this closes out
dashboard.md's 70-hit Passive backlog: 53 edits total, 52 net fewer
hits, 18 left alone.
First half (lines 1-697) of dashboard.md's write-good.Passive backlog --
31 of the file's 70 hits rewritten to name an actor (hive-c0re, the page,
the operator, the table, the filter, builds.js, Provision, the matrix
daemon, ...), reusing an actor already named nearby wherever one exists.
7 left alone in this half: a fixed idiom ("not to be confused with"),
two false positives ("named buckets" / compound adjective; a
field-is-set data-state conditional), a negative-capability idiom
("nothing is written"), and two established state-descriptor idioms
("is unchanged", "is tooltipped").
Part 2 (lines 762-1389, ~32 more hits) is a separate follow-up, not
started -- dashboard.md is too big for one PR.
Read all 94 write-good.Passive hits across docs/swarm/ (ca.md,
README.md, secrets.md, services.md, sso.md, ui.md) in context. 44 are
genuine catches with a nameable, usually already-established actor
(swarm-controller, authelia, swarmctl, the controller, the gateway,
this module, hyperhive itself, or 'the operator' for manual actions) —
rewritten to active. 50 are legitimate passives or false catches, left
alone: predicate-adjective state descriptions (is expected/misconfigured/
broken), negative-capability idioms (no X is needed/placed, can't be
Yed/listed/fetched), config-state conditionals (whenever/when X is
enabled/configured/set), requirement-list labels (is required),
'is tracked as' idiom, backward-looking changelog facts with no actor
(was removed/verified/introduced), ambiguous-actor statements left
conservatively alone (agents are created and destroyed — could be
hive-c0re or swarm-controller, doc doesn't say), and a couple of
deliberately-parallel idiom pairs.
Several sibling-inconsistency fixes: a passive clause sitting next to
an already-active sibling describing the same fact/mechanism (ca.md's
two-bullet consumer list, README's 4-item WireGuard-mesh bullet list,
README's controller-registers-hooks paragraph, sso.md's followed-a-302
sentence).
Verified via vale on the whole directory, diffed against main's exact
baseline (not just the Passive count): write-good.Passive 94 -> 50
exactly, every other category unchanged (1 pre-existing
Microsoft.Contractions error at services... at secrets.md:182,
8 TooWordy, 1 Microsoft.We, 1 Microsoft.FirstPerson — same counts,
same locations).
agent.md, css-vars.md, design-guide.md, shape.md, terminal-rendering.md --
26 of 40 write-good.Passive hits rewritten to name the actor (hive-agent,
browsers, the harness, the client, lifecycle::{spawn,rebuild}, CSS, the
router, ...), reusing an actor already named nearby wherever one exists.
14 left alone: established config-state conditionals ("when X is
set/enabled/given/called"), negative-capability/state-descriptor idioms
("is gone", "is hidden", "is unchanged"), one false-positive
("typed slash commands" as a compound noun, not a passive action), a
backward-looking changelog fact with no actor worth naming, and two
deliberately-parallel rhetorical contrasts (sanitized vs XSS-safe;
cache-hit vs cache-miss) left symmetric on purpose.
dashboard.md (70 more hits) is the next docs/web-ui/ batch, not part of
this one -- big enough to deserve its own PR.
New per-row action on AgentsPage: a quiet-variant icon badge (LinkIcon,
matching the LinksMenu/SettingsMenu chrome-not-chip convention) opens a
dialog with an account/token/homeserver form, PUTting
/api/hives/{hive}/agents/{agent}/matrix-accounts/{account} per the
contract atlas posted on hyperhive#3726 (issuecomment-72355).
Built against the contract before the backend endpoint exists per
atlas's explicit note that it doesn't change when the implementation
lands -- this 404s until that item merges. No linked-accounts list:
no route exposes one, and a credential store shouldn't hand a secret
back out anyway, so this is a blind set/update action, matching
mara's 1:1-for-now ruling on the issue.
Verified: tsc --noEmit and nix fmt clean, esbuild build clean. Real
DOM-interaction screenshots against a throwaway mock server (deleted
before this commit, never tracked) -- table column render + disabled
state on a hiveless row, dialog open, form filled with the token
masked, and the success path end to end (token field clears, success
message shows) against a mocked 200 response.
The swarm UI had nowhere to POST an external matrix account to: this daemon
had no matrix-account code at all and no `swarm-secret-client` dependency, so
the last leg of #3726 — a credential reaching an agent — had no entry point.
`PUT /api/hives/{hive}/agents/{agent}/matrix-accounts/{account}` writes the
credential to the store under the agent's own path and publishes a
`CredentialNotice` on that hive's credential subject. All three path names are
load-bearing: agent + account locate the secret, hive routes the notice. The
account is a path segment rather than a body field so that splitting the 1:1
account-to-agent mapping later is a new route, not a changed payload.
Store first, notify second, and the order cannot be swapped: a notice that
overtakes its own write reaches a hive that reads nothing, and the hive
deliberately does not retry. The publish is followed by a flush for the reason
`publish_deploy` flushes — `publish` hands the message to the connection's
write buffer and returns, so the response could otherwise outrun the notice it
reports as sent.
The store client is built per request rather than held in `AppState`, matching
what the hive side does inside `deliver`: a login that expires is not worth
caching for a route this cold.
`swarm_hive` is `declaration_target`'s two name checks, extracted so this
handler makes them identically rather than in a second copy free to drift.
`declaration_target` still tests the writer first, so a deployment with no
queue answers 503 whatever the caller spelled.
## The nix half
#4081 minted the controller's leaf and gave it `baoClientCertFile` /
`baoClientKeyFile`, deliberately stopping there — the leaf is minted whether or
not a controller runs on that host. Nothing consumed those options, so the
identity never reached the process. Measured before writing: `git grep
baoClientCertFile` returned 5 sites and zero consumers, against a control
(`tokenEndpoint`, 4 hits in the same file) proving the search can see
consumption where it exists.
The unit now gets `BAO_ADDR` / `BAO_CLIENT_CERT` / `BAO_CLIENT_KEY` /
`BAO_CACERT` and the matching `LoadCredential` entries, following
`hive-c0re/environment.nix`'s `%d` credential shape.
The gate is `deploy.swarm-controller.baoClientCertFile`, NOT
`deploy.bao.clientCertFile`. The latter is the hive reader's identity and its
policy scopes a hive's own secrets; wiring it here would evaluate, deploy, and
fail only when the daemon tried to write an agent's credential.
Two `module-eval` arms cover exactly that. The presence arm asserts the
`LoadCredential` *source path* (`…:/var/lib/swarm-bao-pki/controller.pem`) and
not just the `%d` name, because a `%d`-only assertion passes while the daemon
holds the wrong policy. The absence arm (`controllerNoStore`) is what makes the
presence arm mean anything.
`RestrictAddressFamilies` already covers the store client; its own comment asks
for the family to be added with the client, and AF_INET/AF_INET6 are present.
Contributes to #3726
mara, reviewing the previous commit: "the field is specific to matrix, why
add it to the general struct". She is right, and the answer is that there was
no general struct — `Credential` had one consumer, the crate's only path
builder was `matrix_account`, and `value` is pinned by
`glue-matrix-bao-token.nix`, a matrix unit. It was matrix's throughout,
wearing a general name; adding `homeserver` is what made that visible.
`client` now moves whatever type a caller names and decodes nothing itself.
That is forwarding rather than machinery: `vaultrs::kv2::read`/`set` are
already generic over the payload.
The matrix agreement moves to its own module holding both halves — where a
credential lives (`account_path`, was `path::matrix_account`) and what the
object at that path holds. `path` keeps only what every path obeys, so a
second kind of swarm secret becomes a module beside `matrix` rather than
another optional field on a struct it shares. argus raised the same collision
from the other direction on #4092: two mutually-exclusive `Option`s modelling
one concept is the failure mode this forecloses.
`checked_segment` stays public in `path`: hive-priv builds an on-disk path
from the same names and must accept the same charset.
Behaviour is unchanged. The compatibility properties move with the struct —
`Option` is what lets a pre-`homeserver` stored object decode, and
`skip_serializing_if` is what keeps a token-only object free of
`"homeserver":null` for that nix reader.
Refs #3726
A delivered matrix account needs two things: the token and the homeserver it
belongs to. Only the token was stored, so the homeserver had to ride on the
queue notice — and a notice is not persistence. Re-delivering a credential
(agent moved, hive re-provisioned, token rotated) has to reconstruct it from
somewhere, and there is nowhere; keeping it separately at swarm level would be
a second store for one logical object, free to drift from the first.
So `Credential` grows a `homeserver` field and `read`/`write` carry the whole
object rather than a bare string.
`value` keeps its name. `nix/host-modules/glue-matrix-bao-token.nix` reads the
store with `bao kv get -field=value` and is the only nix reader of it, checked
rather than assumed — so this had to be an addition, never a rename.
Two compatibility properties, both of which fail silently if broken:
KV2 keeps every prior version, so objects written before this field existed
are still decoded by this type. What tolerates their absence is the field
being `Option` — a bare `String` would not fail as a migration, every stored
credential would become unreadable at once. The new test pins that, with a
presence control so the arm is about absence being tolerated rather than the
field being ignored.
`skip_serializing_if` keeps a token-only credential serialising to exactly the
bytes the previous version wrote, with no `homeserver` key rather than a null,
which is what that nix reader would otherwise trip over. The existing test
pinning `{"value":"t"}` proves it and became the control for free.
Mutation testing earned its place here: `#[serde(default)]` was in the first
draft and its comment claimed it was what made old objects decode. Dropping it
changed nothing — serde already decodes a missing field to `None` for an
optional type — so the attribute was redundant and the comment was wrong about
its own mechanism. Both removed rather than left to mislead the next reader.
The delivery half needed no change: `write_agent_matrix_token` already took a
homeserver and already wrote the `matrix-account-<name>.json` sidecar beside
the token. `deliver` simply stops passing `None`. A credential stored without
one still works exactly as before — no sidecar, and the account needs a
configured entry.
Refs #3726
hive-forge and hivectl doc-comment strings for pr-create --label, diff
--full, list --label, ci-runs --branch, and several hivectl subcommand
options read as if things happened to themselves. Name the actor (hivectl,
hive-forge, or the CLI) instead, and regenerate the three docs/tools/*-cli.md
files from the rebuilt binaries.
swarmctl-cli.md needs no source change: its only passive hit is
clap-markdown's own generated boilerplate, out of scope here.
Read all 62 write-good.Passive hits across the 6 hand-written files in
docs/tools/ (bash.md, lifecycle.md, forge.md, hivectl.md, matrix.md,
scheduling.md) in context. Excludes forge-cli.md/hivectl-cli.md/
swarmctl-cli.md (57 more hits) -- those are CI-generated from the
clap doc-comment strings in hive-forge/hivectl/swarmctl, freshness-
checked against the committed markdown, so a fix there has to go in
the Rust source and get regenerated -- separate batch.
33 rewritten to active, actor usually already established in the same
paragraph or given via a 'by X' clause (the daemon, hive-c0re,
hive-forge, hivectl, hyperhive itself, or an established module name
like nix/agent-modules/mcp.nix). Several sibling-inconsistency fixes:
a passive clause next to an already-active sibling stating the same
fact (lifecycle.md's server-scoping sentence, forge.md's clone-derives
sentence, hivectl.md's daemon-hashes-passwords sentence, matrix.md's
daemon-upserts-a-todo paragraph).
29 left alone: no-X-is-Y / no-X-is-needed invariant idioms, config-state
conditionals (whenever/when X is set/configured/given), requirement-
list idiom, a false-positive tokenization (vale matching 'are read'
inside 'read-only'), definitional facts about CLI/attrset structure
with no real actor, hypothetical/counterfactual clauses describing a
rejected design alternative, a forward-looking not-yet-real removal,
compressed bullet-fragment conventions, and one deliberately-parallel
sentence structure left both halves alone to avoid breaking the
symmetry.
Verified via vale on the 6 touched files, diffed against main's exact
baseline (not just the Passive count): write-good.Passive 62 -> 29
exactly, every other category unchanged in count AND location
(TooWordy 16, Microsoft.We 1, Microsoft.FirstPerson 1, and the same
4 pre-existing Microsoft.Contractions errors at the same 4 lines).
argus caught this reviewing PR #4110: Badge only applied the disabled
prop inside its onClick-present branch (a real <button disabled>).
When onClick is undefined -- exactly the case every current caller
hits when its gating condition is false, since onClick and disabled
are computed off the same condition -- it fell through to a plain
<span> that never reads disabled and never gets the
.ui-badge-interactive class the dimming CSS depends on. WantedMenu
and the matrix-account trigger both silently lost the disabled
affordance to this.
Fixed at the root: both the button-vs-span branch and the
interactive-styling class now key off disabled || onClick (extracted
to one interactive flag) instead of onClick alone.
Verified: tsc --noEmit clean on both swarm-ui and agent (Badge's two
consumer packages), nix fmt clean. Screenshot against a throwaway
mock roster (deleted before this commit, never tracked) showing a
hiveless row and a destroyed row both visibly dimmed now, next to an
enabled row at full opacity -- previously all three looked identical.
The rel arm required a leading `./` or `../`, so `[x](swarm/services.md)` —
the form a sibling link is usually written in — matched none of the three
arms. That is exactly the shape a directory move breaks: during #3911 a link
in that form moved into a new directory, resolved to nothing, and the lint
stayed green. Reproduced by putting the broken form back with `sed`
(MUTATED_DIRTY=1) and re-running: exit 0, a control that fails to fail.
Widening the pattern surfaces 116 links the arm never saw (76 -> 192 matches).
111 are in `.md` files and all 111 resolve today, so this adds coverage
without a single new failure in the tree; the other 5 are the interesting
part, and they are why the arm now selects on file type.
A link is only resolvable against its own directory when the file is
*rendered where it is stored*. The four in `nix/docs/default.nix` are inside
a markdown string this derivation writes to `$out`, and the one in
`hive-c0re/src/workers/knowledge.rs` is template text for another repo's
README — resolving either from the source directory certifies a path no
reader ever requests. `lint:allow` is the wrong tool for them: the marker
lives inside the generated text, so it would ship in the published page.
The `.rs` arm keeps its error, narrowed to doc-comment lines. Its premise
still holds — the repo has zero relative links in `.rs` doc comments — but
without the narrowing the widened pattern reports the knowledge.rs template
string with a message about rustdoc that is wrong for it.
Measured: the old arm's 76 hits are all in `.md` files, so restricting
resolution to `.md` removes no existing coverage.
Closes#4076
argus caught this on PR review: the passive-to-active rewrite of the
packet-authentication sentence in snapshot-store.md started with 'So ',
which write-good.So flags as a new error not present on main. Drop the
leading word; meaning is unchanged.
Eighth batch of the ongoing write-good.Passive pass (hyperhive#4042):
read all 69 hits across network.md/snapshot-store.md/gateway.md in
context and rewrote 34 with a clearly nameable actor -- mostly
hive-c0re, dnsmasq, nginx, or a specific systemd unit/fn named right
there or a sentence or two earlier. snapshot-store.md's operator-facing
prose ("you can deploy it", "you must tell every hive") reuses this
doc's own established "you" address (already present two sentences
away in one case) rather than inventing a new register.
Left 35 alone -- the largest leave-alone count yet, dominated by two
shapes gateway.md is dense with: negative-capability/requirement
idioms ("no X is required/needed/exposed", "can't be added/wired",
"must not be exposed") and predicate-adjective state descriptions
("is misconfigured"/"is broken" in negative-contrast "not that X"
framing, "is privileged" matching the established "is trusted" bucket,
"is closed/internal" deployment-posture, "is encrypted and
unauthenticated" connection-state pair). Also: two "X was removed"
changelog-style facts with no actor clause (same family left alone in
the scheduler batch), a gerund nix-conditional pair ("X being enabled
and Y being set are assertions, not documentation"), and a passive
embedded inside an already-active sentence ("X controls whether Y is
opened" -- the real verb is already active).
One caught-and-reverted mid-edit mistake: an early attempt at the
"nginx is handed the leaf" fix landed on the wrong paragraph (an
unrelated CA-generation sentence two paragraphs up) and left a
nonsensical forward-reference behind. Caught by re-reading the diff
before running vale, not by the count -- same discipline as the
docs/process batch's original catch. Reverted cleanly and reapplied at
the actual target sentence.
One small sibling-consistency fix beyond pure voice-flipping: "The
gateway emits the following headers..." (rewritten) sat two paragraphs
above "a header is added alongside the other security headers" (still
passive) describing the same mechanism -- rewrote the second to match
("the gateway adds a header...") rather than leave one active and one
passive version of the identical fact sitting near each other.
Verified via vale before/after: 69 -> 35 write-good.Passive hits,
exactly the 35 left alone above; error count and other warning
categories unchanged. Re-read every changed line in full surrounding
context after editing before running the final vale check.
Ninth batch of the ongoing write-good.Passive pass (hyperhive#4042):
read all 75 hits across agent-hierarchy.md/persistence.md/approvals.md
in context and rewrote 46 with a clearly nameable actor -- mostly
hive-c0re, a specific fn/type named right there or a sentence or two
earlier (approvals.md's numbered "Reminder delivery" list all share
hive-c0re as the established actor across three consecutive bullets),
or "the worker"/"the daemon" reused from the section's own established
subject.
Left 29 alone. Recurring buckets: predicate-adjective state pairs
("is wedged"/"is stopped", "is unaffected", "are unrelated", "is
overloaded" -- matches the established "is trusted"/"is privileged"
family), negative-capability/invariant idioms ("no X is needed",
"can't be removed/started", "no such transient was cleared"),
forward-looking design-intent statements about not-yet-built
enforcement (agent-hierarchy.md's whole "Planned topology semantics"
section leans on this: "are meant to run", "once enforcement is
finished", "will be gated"), the "audit trail, not cache" policy
invariant restated twice verbatim ("Approvals are kept forever" /
"indefinitely" -- left both alone consistently), and a config-
conditional state idiom ("when interval_seconds is set"). One more
false-positive tokenization, same bug as knowledge.md's earlier case:
persistence.md's "`config` is read-only" trips vale's "is read" match
inside the compound adjective -- not a real passive at all.
One deliberately-conservative leave: agent-hierarchy.md's "Reminder
cancellation is handled fully in-agent" had an ambiguous actor (the
in-agent socket vs. the hive-c0re source file the doc points readers
to for detail) -- left alone rather than risk a wrong attribution,
unlike the higher-confidence rewrites elsewhere in the same batch.
Verified via vale before/after: 75 -> 29 write-good.Passive hits,
exactly the 29 left alone above; error count and other warning
categories unchanged. Re-read every changed line in full surrounding
context after editing before running the final vale check.
argus's review on this PR flagged a real clarity regression: "Forge
notifications are polled by their own process, not this loop" became
"Their own process polls forge notifications, not this loop" -- the
original's "their" clearly referred back to "Forge notifications"
within the same clause; the rewrite put "Their" first with nothing
plural preceding it in the paragraph, so the pronoun has no
referent when the reader hits it. This was exactly a case where the
passive was doing real work (subject-first noun introduction), not
just avoiding an active verb.
Uses argus's own suggested fix: name the process directly instead of
leaning on a pronoun at all, and drop the now-redundant second
mention of the same name a few words later.
Fifth batch of the ongoing write-good.Passive pass (hyperhive#4042):
read all 42 hits across the four docs/turn-loop files in context and
rewrote the 36 with a clearly nameable actor, usually one already
sitting in the same sentence or established a sentence or two earlier
(the broker, the harness, drive_turn, the renderer, hive-c0re, or a
specific fn/type named right there). Left 6 alone: predicate-adjective
copulas that only look passive ("is gone", "Bash is disallowed", "is
documented behavior" — nothing actually acts on the subject), a
config-conditional idiom with no in-file inconsistency to fix ("when
hyperhive.docs.enable is set"), and a paraphrase of what a rendered
message tells the agent ("the docs are mounted at that path").
Verified via vale before/after: 42 -> 6 write-good.Passive hits in
docs/turn-loop, exactly the 6 left alone; error count and other
warning categories unchanged. Re-read every changed line in its full
surrounding context after editing (not just the vale count) per the
lesson from the docs/process batch.
Sixth batch of the ongoing write-good.Passive pass (hyperhive#4042):
read all 52 hits across knowledge.md/github.md/matrix.md/forge.md in
context and rewrote 39 with a clearly nameable actor -- mostly
hive-c0re, forge_notify, or a specific fn named right there or a
sentence or two earlier. forge.md's notification poller is the
densest yet (19/20 hits rewritten): forge_notify is established as
the section's sole actor early and reused throughout, the shape
that's produced the highest catch rates all along.
Left 13 alone: the "no X is needed" negative-capability idiom (x2),
a container-lifecycle state descriptor ("when container is stopped"),
a false-positive tokenization ("read-only" split across a line wrap,
vale matches "is read" inside it -- not a real passive at all), the
"X can't be Yed" idiom, a generic "before the ids are minted" timing
clause with no natural actor to name, a room-join policy-state
descriptor, an "is enabled"/"is trusted" pair describing a config/
trust state (predicate-adjective-copula bucket, same family as
"is privileged" from an earlier batch), three "**X is required**"
bolded requirement-list labels (structural convention, not really
mid-sentence passives), and a contrastive "are shared" clause
mirrored against an active sibling clause exactly like
claude-invocation.md's "everything else is shared" from the
turn-loop batch -- left alone there for the same reason.
One sibling-inconsistency catch worth flagging: forge.md's merge-
racing-comment paragraph had two passive clauses ("is left off",
"is dropped") sitting next to a third, already-active clause
("appends nothing") in the same three-item parallel list -- rewrote
all three under one active subject (forge_notify) for consistency.
Verified via vale before/after: 52 -> 13 write-good.Passive hits,
exactly the 13 left alone above; error count and other warning
categories unchanged (still on TooWordy since #4097 hasn't merged to
this branch yet). Re-read every changed line in full surrounding
context after editing, matching the diff to intent before running
the final vale check.
Fourth batch of hyperhive#4042's Passive pass (see #4098/#4099/#4100
for the first three and the read-every-hit discipline this pass
uses). 44 hits across pr-review-gate.md (4), conventions.md (18), and
gotchas.md (22) -- highest genuine-catch rate so far, 23/44 (~52%),
because this architecture/mechanism documentation has a lot of "X
does Y via Z" sentences where the actor is already named
parenthetically or in a nearby clause -- the single most productive
rewrite shape across every batch so far.
Recurring rewrite shapes this batch:
- Actor already named in the same sentence, just not as the
grammatical subject: "X is configured per repo in its
branch-protection settings" -> "Each repo's branch-protection
settings configure X" (pr-review-gate.md); "the broker" (reserved
names), "rsvg-convert" (PNG rendering), "the website repo" (HTML/CSS
rendering), "systemd.globalEnvironment" (D-Bus address export), and
several more -- all the same shape.
- Subject already established one clause or one sentence earlier,
just needs continuing rather than restarting with a new passive
subject: "the harness reads HIVE_TOOL_GROUPS (...). Unrecognised
tokens are logged and skipped." -> "...logging and skipping
unrecognised tokens" (continues "the harness"), same pattern twice
more (job_queue::templates::rebuild, HIVE_CAPABILITIES resolution).
- Sibling-inconsistency: a bolded lead-in bullet was the one passive
sentence in an otherwise-active paragraph/table (the
read_host_journal capability row sat between two "may X" rows; the
HTML+CSS bullet's own tail clauses were already active voice around
the one passive lead phrase).
- One caught-and-fixed authoring mistake worth noting for future
passes: the first attempt at the "Nix treats X as a package" rewrite
landed in the wrong sentence (a similarly-worded but unrelated
passage two paragraphs up) -- caught by re-reading the diff before
running vale, not by vale itself (which would have shown 0 remaining
hits either way, since the intended sentence's hit just wouldn't
have been touched -- a silently-wrong edit vale's own count can't
catch). Re-reading the actual diff, not just trusting the before/
after hit count, is what caught it.
21 of 44 left alone -- same recurring legitimate shapes as prior
batches (predicate-adjective copulas, quoted/literal text, generic-
actor statements, negative-capability invariants, "is tracked/rooted/
scoped at X" property-description idioms, and two more thesis-
statement headings matching the "Ownership is declared, not repaired"
precedent from #4100).
Verified: vale docs/process before/after -- 44 -> 21 write-good.Passive
hits, exactly the 23 rewritten, re-read every changed line's full
surrounding context after editing (not just the vale count) to catch
exactly the kind of misplaced-edit mistake described above.
Third batch of hyperhive#4042's Passive pass (see #4098/#4099 for the
first two and the read-every-hit discipline this pass uses). 23 hits
across boundary.md (10) and security.md (13).
boundary.md, 2 of 10 rewritten:
- "Operator-authority actions ... are served by the core daemon and
only reachable via the gateway" -> "The core daemon serves
operator-authority actions ..., reachable only via the gateway" --
the actor was already named in the sentence, and the parallel
"Agent" bullet right below it is already active voice ("speaks only
for itself"), so the Operator bullet was the inconsistent one.
- "ownership set afterwards is reverted the next time any agent
changes" -> "and reverts any ownership set afterwards the next time
any agent changes" -- continues the same subject ("the `d`
re-applies ... and reverts ...") already established one clause
earlier, avoiding a subject switch entirely.
security.md, 6 of 13 rewritten (across 3 edits touching 6 flagged
locations):
- "A compromised/confused agent's reach ... is bounded by its own
account's scope" -> "Its own account's scope bounds a
compromised/confused agent's reach" -- matches the section's own
header ("Scoped tokens bound the blast radius"), which is already
active voice.
- "Two allow-listed root prefixes are accepted; all other paths are
rejected" -> "It accepts two allow-listed root prefixes and rejects
all other paths" -- continues "it" from the endpoint named one
sentence earlier.
- The container-allowlist paragraph (3 flagged locations: "is
validated", "are accepted", "are rejected") rewritten as one
consistent-subject passage with `hive-priv` as the actor throughout,
matching the neighboring bullets in the same subsection
("**Socket-activated** -- systemd starts hive-priv...", already
active voice) -- the passive version was the odd one out among
siblings, not the house style.
15 of 23 hits left alone. The recurring legitimate shapes, same
categories as #4098: predicate-adjective copulas that only look like
passives ("an agent is trusted code", "the agent is privileged" --
"trusted"/"privileged" modify the noun, there's no actor to name),
quoted rhetorical contrasts where the passive is doing real work (a
"wrong-framing" quote left passive on purpose, paired with an active
"right-framing" quote right after it), the "is tracked as/in X"
idiom (twice, same as #4098's precedent), and "X can't be Yed" /
negative-capability invariant statements (matches #4098's "No X is Y"
security-guarantee idiom). One deliberately left despite a nameable
antecedent ("hive-gateway's access is scoped [by ReloadGatewayNginx]
instead") -- lower-confidence rewrite than the others, left rather
than force it.
Verified: vale docs/trust-boundary before/after -- 23 -> 15
write-good.Passive hits, exactly the 8 rewritten, no other rule's hit
count moved (the TooWordy/Microsoft.Avoid/alex hits vale also reports
on these two files are pre-existing and out of scope for a
Passive-only pass; TooWordy's hits specifically already have a fix
queued in #4097, not duplicated here).
Second doc-directory of hyperhive#4042's Passive pass (see #4098 for
the first batch's full read-every-hit reasoning). Only one hit in this
whole directory: "Every page is generated at build time
(nix/packages/reference-docs.nix, hyperhive#3051)" -- a genuine catch,
same shape as #4098's swarm-SSO rewrites -- the actor is already named
parenthetically in the same sentence, so naming it as the grammatical
subject too is strictly clearer: "nix/packages/reference-docs.nix
(hyperhive#3051) generates every page at build time".
Verified: vale docs/crates/README.md before/after -- 1 -> 0 warnings.
First doc-directory of hyperhive#4042's Passive pass (705+ hits across
docs/, genuinely mixed unlike TooWordy -- needs a real per-hit read,
not a dictionary shortcut, so this is going doc-directory by
doc-directory in small PRs, per the plan posted on the issue).
Read all 13 flagged hits in this file in context, not just the flagged
word. 3 were genuine catches with a real active-voice improvement and
a nameable actor:
- "the human operator's own forge/matrix account is created via swarm
SSO" -> "swarm SSO creates the human operator's own forge/matrix
account" (x2, identical sentence shape for both accounts) -- swarm
SSO is a nameable, specific actor already named later in the same
sentence, so naming it as the subject too is strictly clearer, not
just different.
- "Do this before anything is pointed at it" -> "Do this before you
point anything at it" -- matches the doc's own established
second-person imperative voice used throughout ("Do this", "Put the
token's value", "Delete the file"); the passive here was the odd one
out, not the house style.
The other 10 are legitimate passives, left alone:
- Generic/unspecified-actor statements ("is needed", "is bound", "is
issued", "been run") where forcing a subject would either invent an
actor the doc never established or read worse than the original.
- Security/architecture invariants ("No forge admin token is stored in
any agent state dir", "Telemetry ingest is authenticated per hive")
-- "No X is Y" / "X is Y" is the standard idiom for a guarantee
statement in security docs, not a clarity problem to fix.
- "operator-only surfaces ... are gated on that group" -- borderline
(could name the group as subject), judged idiomatic access-control
phrasing rather than genuinely clearer active, but flagged as the
closest call in this batch.
Verified: vale docs/getting-started/setup.md before/after -- 13 -> 10
write-good.Passive hits (exactly the 3 rewritten), the pre-existing
unrelated alex.Suicide hit on "hang" (line 51, untouched) still
present and correctly out of scope for this pass.
argus's review (reproduced, not speculative): .ui-table-scroll sets
overflow-x: auto with overflow-y left unset. Per the CSS overflow
spec, an axis left visible computes to auto too once the other axis
isn't visible -- so this box silently clips vertically as well as
horizontally. The popover was position: absolute; top: 100% under its
header <th>, itself inside .ui-table-scroll -- on a table shorter than
header-plus-popover (a small hive's roster, or any table narrowed by
an existing filter), the popover got cut off at the scroll box's own
bottom edge, with a stray vertical scrollbar as the visible symptom.
Computes the popover's viewport position from the anchor <th>'s own
getBoundingClientRect() and renders it via a portal onto
document.body, position: fixed -- escapes .ui-table-scroll's clip the
same way any position: fixed element escapes an ancestor's overflow
(neither .ui-table-scroll nor .ui-table establishes a new containing
block). Recomputes on scroll (capture-phase window listener, the
standard technique for detecting scroll on a nested scroll container
without binding to every ancestor by hand) and resize while open, so
the popover stays anchored rather than only positioning once at click
time. createPortal comes from preact/compat, already resolvable
through the existing preact dependency -- no new package.json entry.
Verified against argus's own repro shape: a 1-row table, scripted
click on the filter icon (same real-DOM-event technique as this PR's
first round), screenshot shows the popover rendering fully rather than
clipped, no stray scrollbar.
Replaces the permanent filter-row under Table's headers with a small
filter-icon button in each filterable column's own header cell. The
icon fades in on header hover/focus via a CSS opacity transition, or
stays visible outright once that column actually has a filter set
(mara: "instead of a filter row, add little filter icons on header
hover with fade in out animation ... when a filter is set, the filter
icon does not disappear"). Clicking it opens a small anchored popover
directly under the header holding the exact same filter control
filterMode already provides (text input or select) -- the underlying
filter mechanism from hyperhive#4088 is unchanged, only where the
control lives moved. Close-on-outside-click/Escape mirrors the
contract Dropdown already gives its own popover, adapted to a shared
listener across every column instead of a ref per column since only
one popover is ever open at a time.
New FilterIcon in @hive/shared's icons.tsx (a plain inline SVG funnel,
same Feather/lucide-style shape as the existing GearIcon/LinkIcon) --
found and reused that pattern rather than reaching for an emoji glyph,
matching the documented reason those two exist as SVG in the first
place (mara, on the old emoji icons: inconsistent size/weight across
platforms).
Three columns gain Table's filterValue/filterMode (the mechanism
hyperhive#4088 added): title (text, substring search -- there was no
way to search by title text at all), assignees (text, not select --
a row can carry more than one assignee and Table's select mode
matches one whole string per row exactly, so substring search over
the joined string is the shape that actually fits multi-value data),
and blocked (select, synthesized "blocked"/"not blocked" strings --
distinct from the existing "hide blocked" toggle, which only hides
blocked issues and has no way to show only them).
Deliberately not touched: repo (redundant with the existing repo
SelectField), labels (redundant with the existing label chip
multi-select -- chips are the better UI for a bounded label set
anyway), the three numeric columns (no clean filter shape, already
sortable). The existing hand-rolled sort (SortHeader, useLocalSetting-
persisted) is untouched too -- migrating it onto Table's own sortBy
would drop the localStorage persistence this page specifically wants,
and Table doesn't expose controlled sort state to a caller today. The
two layers compose without conflict: Table's own filter/sort runs
over whatever rows it's handed, which is already this page's own
filtered+sorted array.
Scoped on the issue first, including this exact reasoning, before
writing any code.
Dialog previously forced every caller to a fixed width: 90vw; max-width: 44em
shell regardless of content — fine for the create-agent form's wide
two-panel layout (the only caller until now), but a ConfirmDialog's short
paragraph then wrapped at its own narrower max-width while the shell stayed
the wide default, leaving a dead gutter before the close button (mara filed
a screenshot showing exactly this).
First pass added a narrow prop/second CSS class for ConfirmDialog to opt
into a smaller fixed width. Review pointed at the actual root cause one
level up: width: 90vw is a forced width, not a cap — a native dialog's own
UA default is width: fit-content. Switching .ui-dialog to
width: fit-content; max-width: min(90vw, 44em) lets each caller size to its
own content naturally: the create-agent form still hits the 44em cap (same
rendered width as before, confirmed via screenshot), ConfirmDialog's
paragraph settles at its own intrinsic width with no extra prop, no second
CSS class, and no second hardcoded number to keep in sync with the first.
Table columns whose value only ever comes from a small closed set
(freshness, wanted, hive) get a <select> in their filter-row cell
instead of a free-text input, populated from the distinct values
present in the currently-loaded rows plus an "any" option, matched by
exact equality instead of substring. Free-text columns (name, the
agent's own status message, config PR, hive domain) are unchanged.
The log-store board reports rows ingested, disk size, free disk and errors —
every one of them a fact about VictoriaLogs itself. None of them can say
whether a particular unit or host is contributing, so aggregate ingestion
reads healthy on host-tier units while a whole tier ships nothing, and there
was no way to tell those apart from a dashboard.
Add four panels to that board rather than a second one. Every other board here
is per service — agents, authelia, openbao, forge, queue, the two stores — so
a second board about the same service would have made an operator guess which
of the two answers their question.
Nothing in the new panels names a unit: both breakdowns discover their rows
from the data, so a source that starts shipping appears without an edit, and
one that never existed is simply not there.
The ungrouped total is a control, not a summary. An empty breakdown renders
the same whether the query is malformed or the source genuinely never shipped;
with the total beside it, nonzero-and-empty is a broken query and
zero-and-empty is an empty store. Being on one board buys a second reading it
could not have alone: that total and `Log rows ingested` are the same quantity
measured by querying and from the store's own metrics, so the two disagreeing
means rows arrived that no query can reach — which is the failure this
pipeline actually had once.
The two breakdowns are bargauges rather than tables. A `stats` query returns
one frame per group, and a table panel renders one frame at a time behind a
series picker; bargauge consumes multi-series natively, for the same reason
the timeseries beside it always did. The alternative was a table plus
labelsToFields plus merge plus organize — three assumptions where this needs
none, in a spot I cannot render to check.
Series are named by their label rather than `rows{_SYSTEMD_UNIT="x.service"}`,
which also cleans up the timeseries legend. The datasource supports
legendFormat; its query editor's own placeholder is `{{label}}`.
These are the first panels to query the logs datasource at all; the other
eight boards are prometheus, including this one until now, which reads
VictoriaLogs' self-metrics out of VictoriaMetrics. So `renderDashboard` grows
a `@logsDatasourceUid@` substitution alongside the metrics one. The binding it
points at already existed, and its comment claiming a dashboard panel named it
only becomes true with this commit.
The uid is unchanged, so existing links and bookmarks still resolve; only the
title widens to match what the board now covers.
The query model was read out of the plugin in the store rather than guessed:
`queryType` is one of hits/instant/logs/stats/statsRange, and Stats/StatsRange
are the two that consume `expr`. The stream field names come from the module
that builds them — swarm-otel.nix's `_stream_fields=_HOSTNAME,_SYSTEMD_UNIT`.
The queries themselves are confirmed against the live store: mara ran the
by-unit one over seven days and it returned rows.
Refs #4084
`deliver` read the value out of the secret store and wrote it itself, as the
`hive-core` user, at 0600. The file lands in a directory owned by the agent,
so it arrived owned by `hive-core` — the agent's matrix daemon woke on it
appearing and could not read its own credential. `priv_client::write_agent_
matrix_token` already existed and already had two callers; this was the one
path that did not use it.
hive-priv now owns the filename too, so the name the daemon's path unit globs
for is decided in one place instead of being built identically in two.
That move exposed a disagreement worth fixing rather than routing around.
The secret store accepts `[A-Za-z0-9_-]` for an account name; hive-priv's
`validate_name_chars` accepts lowercase, digits and hyphen only. An account is
an attribute name in `hyperhive.matrixAccounts`, typed `attrsOf` with no
charset constraint, so `Ops_Relay9` is a key an operator can already have
written — and it would have read out of the store and then failed to land.
So hive-priv grows `validate_account_name` rather than widening the existing
one: an agent name is an `Ident` and lowercase by design, an account name is an
attrset key, and one validator serving two name domains is what let them drift.
The test that caught this came from `credential.rs`, which used to build the
path. It moves to hive-priv with both of its controls intact, because the
controls are the point — they assert which names must be ACCEPTED, and a
validator narrower than the store's passes every rejection case. A second
moved test pins the `matrix-token` prefix where the name is now built; the
old one would have kept passing while asserting a function that no longer
decided anything.
Refs #3726
`write_agent_dir_file` opened the final path with O_TRUNC and filled it, so
the file existed empty before it held anything. Several of these paths are
watched, and the kinds differ: `nix/agent-modules/matrix.nix` starts the
agent's matrix daemon on `PathExistsGlob = ".../matrix-token*"`, which fires
on the file *existing* — the O_CREAT moment, ahead of the content.
`nix/agent-modules/forge.nix` uses `PathChanged` and has no such window.
Write to a temp in the same directory, chown that, then rename it into
place. The chown stays ahead of the publish for the same reason the write
now does: the file must never be visible under its final name while still
root-owned.
The temp is dot-prefixed rather than suffixed, because `matrix-token-x.partial`
matches the daemon's own glob — a suffix would wake it on exactly the empty
file the rename exists to hide.
`write_state_file_nofollow` is deliberately unchanged. Its O_NOFOLLOW and
fchmod/fchown-on-the-fd properties are what make a root write into an
agent-owned directory safe, and its existing tests are the control on them;
the caller is the part that needed to change.
That does move the leaf validation, though: the helper now only ever sees the
temp name, which is a plain component whatever the caller passed. So
`ensure_plain_filename` is extracted — it was already duplicated in
`delete_agent_state_file` — and the caller's own name is checked with it.
Whether the empty-file window is reachable in practice is not measured;
`matrix.nix`'s documented skip condition is "missing" rather than "empty", so
a first provision could plausibly lose one. This makes the question moot
rather than answering it.
Refs #3726