`swarm/agents/<agent>/bao-mtls` did not exist, and neither did any
per-agent identity at the secret store: `policy::agent_object_name`,
`render_agent` and `render_agent_with_queue` had been written and never
called outside their own tests. An agent's only "per-agent" secret today
is read under the HIVE's certificate, through a wide grant on
`swarm/agents/*` — so "per-agent" was presentational.
The swarm now mints the certificate, so no hive ever needs the capability
to mint one. `swarm-controller` is the service that does it: it already
logs in to the store, and its existing grant already covers exactly the
three objects written here (`create/update` on
`secret/data/swarm/agents/*`, `sys/policies/acl/hive-*` and
`auth/cert/certs/hive-*`). No new bao grant, and nothing co-located — a
cert-auth role pins its authority by value, per role, so the controller
issues from its own CA on its own host and pins that CA in the role it
writes. No existing role changes.
The mint node does not report success on a write. After publishing it
connects again, with the leaf it just issued and under the role it just
wrote, and reads the path back — so the policy, the role, the common name
and the leaf are exercised in production on every agent creation. A
certificate this code mints that the role this code writes will not accept
turns the job node red at creation time instead of surfacing later as an
agent container that cannot start.
`TriggerDeploy` gains an `after_any` edge on the mint, not `after_ok`: a
hive cannot pass down a certificate the swarm has not published, but a
host with no authority configured must still create agents exactly as it
does today.
The private key is generated in memory and never written to disk on the
controller — `SecretStore::connect_with_identity` takes the PEM the minter
is already holding, so nothing is written out purely to be logged in with.
Refs #4137
`services.hyperhive.agent.matrix.enable` was a second source of truth for
a fact the account set already carried: after ①-③ the hive-internal
`main` account is an ordinary `matrixAccounts` entry, so "does this agent
have matrix" and "does this agent have an account" were the same question
asked twice, with the boolean able to disagree.
The option is gone and a non-empty `matrixAccounts` now gates the daemon
unit, its token path-watcher and the injected `extraMcpServers.matrix`
entry.
That is only a real condition because `matrixAccounts.main` is itself
gated: it is declared when `matrix.url != null`, never unconditionally. A
`main` with no homeserver is an account the daemon can never log in as,
so declaring one always would have made the signal trivially true and
turned matrix on for every agent in every hive. With the URL gate, the
empty set is reachable exactly for an agent the hive gave no homeserver
and whose operator declared no account of its own — the state the old
`enable = false` expressed.
Assertions: "extras require enable" is deleted, having become the
definition of the thing it checked (an external-only account with its own
homeserver is now rendered rather than rejected). `main.tokenFile` stays
pinned, re-guarded on `? main` instead of on the flag, since `main` is
absent whenever the URL is null and an unguarded index would throw there.
Both spellings of the option get `mkRemovedOptionModule`, following
../host-modules/deploy.nix's registrationTokenFile pair rather than a
silent delete: the definition whose meaning changes is `false`, and left
undeclared it would be ignored and hand the agent the tools its operator
turned off. Failing the eval with the replacement spelling is the only
outcome that cannot.
module-eval gains the three arms — URL, nothing, external-only — with the
middle one carrying why it exists: it is the only thing in the suite that
would notice `main` becoming unconditional again.
Refs #4475
mara: "the subagent should already stamp the agent on the metric. so i want
to see count of distinct subagent names per agent name over time" — it
does. New timeseries panel-46 on the subagents tab: one line per parent
agent, counting distinct subagent label values with a turn in the trailing
$__rate_interval at each point. Rolling, not cumulative — can fall as well
as rise, same shape as any other rate()-derived panel here.
Placed at y:15, below where #4491's panel-45 (stat tile, y:11-15) sits once
merged, and numbered panel-46 rather than reusing panel-45 — argus caught
that this PR and #4491 both independently added a panel-45 in the same grid
slot from a shared unmerged base, which would have collided on whichever
merged second.
Narrower than a true concurrency gauge (subagents open at this exact
instant) — that needs hive-subagent-mcp to publish its own metric, which
nothing does today; noted as a separate, harder ask on #4488.
`matrixAccounts` is meant to be the agent's full account list, but the
hive-internal `main` account was outside it: the nix module emitted only
the extras and `hive-matrix-daemon` prepended a `main` it synthesized
from the per-agent paths, with the option schema forbidding the name
outright.
nix/agent-modules/matrix.nix now declares `main` itself, as an ordinary
entry under `matrix.enable`, from the state-dir paths the module already
used for its token path-watcher (now a shared `stateDir` binding) plus
`matrix.url`. The whole set, `main` included, is serialized to
HIVE_MATRIX_ACCOUNTS.
accounts::configured therefore synthesizes `main` only when the parsed
list carries none, and otherwise takes the declared one verbatim —
hoisting it to index 0, since the daemon reads index 0 as the primary
and nix serializes an attrset, so `main` sorts wherever its key falls.
Declared xor synthesized: an agent whose harness predates this entry
keeps working, a current one gets its own, and there is no arrangement
where `main` is duplicated or missing.
The reserved-name assertion is replaced rather than dropped: the name
must now be legal (the module uses it), but `main`'s tokenFile stays
pinned to `<state>/matrix-token`, since hive-c0re provisions the
hive-internal token there and nowhere else — a retarget would evaluate
fine and then never restore. The other two fields are mkDefault and free
to override.
Refs #4475
Bargauge panels display series in the order the datasource returns them —
Grafana has no native sort for that (long-standing upstream gap, grafana#17245).
The reliable fix is sorting at the PromQL level with sort_desc() on an
instant vector, which forge.json's 'Issues by label' panel already does.
Applies the same pattern to the other 14 multi-series bargauge panels across
agents.json, claude-usage.json, and authelia.json (the latter two also
needed instant: true added, since sort_desc only works on instant vectors).
logstore.json's two VictoriaLogs-backed bargauge panels use LogsQL, not
PromQL, and are left out of this pass — LogsQL's sort-after-stats has a
documented ordering bug in some versions; needs its own verification.
Operator ruling: repair belongs on the write path, not the read path.
read() still drops unrecognised names from what it returns (with a
warn!) so a stale name is never honoured, but it no longer writes the
pruned form back — the file heals the next time set_caps()/write() run,
which prune anyway since they're fed a map read() already filtered.
Refs #4474
read() and write() now prune any string in capabilities.json that isn't
a recognised hive_sh4re::permissions::Capability, warn!ing per dropped
entry (naming the agent) and healing the on-disk file so the junk
doesn't survive forever. set_caps() filters incoming names the same
way before ever writing them, so a typo'd or stale grant is dropped
with a warning instead of looking like it took effect.
Refs #4474
mara: "show subagent count in time range" — the existing 'Subagent turns'
tile answers volume (how many turns), not this (how many differently-named
subagents ran at all). New panel-45 counts subagent label values with at
least one turn in the selected range.
meta.rs writes each agent's flake, and it still named the pre-move
`hyperhive.*` paths — so every agent rebuild would print a rename
deprecation warning about a line no human wrote and no operator could fix.
A warning nobody can act on trains everyone to ignore the ones that matter,
which is the whole value of the alias shims.
Repoints the FORWARDED_VAR_OPTIONS table and every other emitted option
assignment (otel.*, docs.source, claudeCodePath, github.enable, user.name,
claudeMemoryMaxBytes) to `services.hyperhive.agent.*`, with the test
expectations that pin the rendered text. The flake input named `hyperhive`
(`hyperhive.url`, `hyperhive.inputs.nixpkgs.follows`,
`hyperhive.nixosConfigurations.*`), hive-tier `services.hyperhive.*` paths,
and the `@hyperhive.local` git identity share the word and are untouched.
Also repoints the same option paths where they appear in comments, rustdoc
and runtime message strings across the other crates — a refusal message
naming `hyperhive.allowedRecipients` sends an operator to a path that will
stop existing. Prose under docs/ is deliberately not in this commit.
Refs #4473
Every per-agent harness option lived at the top-level `hyperhive.*` while
the host tier has always been `services.hyperhive.*`. Move all 52 agent-tier
option leaves (33 top-level names across 16 modules) to
`services.hyperhive.agent.*`, repoint every read, and keep existing agent
configs evaluating through one `mkRenamedOptionModule` per old leaf path in
the new nix/agent-modules/renamed-options.nix.
The shims are per leaf rather than per namespace: `user`, `mcp`, `otel`,
`queue`, `docs`, `forge`, `frontend`, `github`, `gui`, `logs`, `matrix` and
`cargo` are plain attrsets of declarations, not submodule-typed options, so
a parent-path rename would not reach their children. Three read-only
options (`frontend.mergedDist`, `queue.clientIdFile`,
`queue.clientSecretFile`) deliberately get no shim — a rename contributes a
definition, which a read-only option refuses; the exclusions are commented
in place.
Refs #4473
Same slice, same fact: socketPath lost its pre-rename path, so only three
old paths are defined in the fixture now, not four. Missed this sentence
while fixing the count three lines below it.
readOnly means socketPath can only ever hold the default, so the fixture's
old custom value can no longer reach the unit and the check.module-eval
assertion for it was failing. Assert the default instead of deleting the
check -- it still catches the env var vanishing or being misspelled, which
deleting it would not.
Also corrects the block's stale 'FOUR movers' count: socketPath lost its
pre-rename shim in the same slice, so only three of the asserted values
are still rename-shim movers.
readOnly makes a bad socketPath inexpressible rather than diagnosed
after the fact. Since readOnly rejects any definition including one
arriving through a rename shim, drop the deploy.nix rename entry for
it and the fixture line exercising it, and update the controllerOldPath
comment's shim count from seven to six.
Refs #4208
The socket is 0666 (nginx needs to connect(2) as a different user), so its
directory is the only thing bounding what the gateway's nginx can reach —
the crate's README already says so. A unit test pins DEFAULT_SOCKET, but
that only catches a developer editing the const; an operator setting the
documented socketPath option to a shared dir (notably /run/hyperhive,
which holds the host admin socket, or /run/hive) went unenforced.
Denylist rather than the general "not any other module's socket dir"
form — narrower, but it's what the README names and lands without pulling
in other modules' constants.
Refs #4208
treefmt normalises markdown emphasis to underscores; the file was
written with asterisks and never run through `nix fmt`. Also ends the
sentence before the scoping note with a full stop rather than a colon,
since a paragraph now sits between it and the example block.
Refs #4460
A scratch report written inside the worktree was swept in by a
`git add -A`. It is not part of the change, and its contents trip the
tracker-reference lint.
Refs #4460
The worked example keyed a stream selector on `_HOSTNAME`, which the
collector config warns against: a hostname is a config value two
machines can share, so keying on it merges their streams. Use
`_MACHINE_ID`, which is the field that actually distinguishes them.
Refs #4460
`swarm-logs --help` documents the CLI surface but not the LogsQL
constructs an agent needs to build a bounded query with it: no
time predicate, and no way to know a bare word matches across the
whole swarm rather than one hive. Add a skill teaching `_time:`
predicates, hive/service scoping via the collector's stream
fields, message vs json format, distinguishing an empty result
from a refused request, and running a control query before
trusting an empty one.
Refs #4460
First slice of the move off the hand-rolled swarm services sub-CA: the
store gains the three objects the eventual minter needs, and nothing
else.
- a `pki` secrets mount, enabled ask-first off the same `bao secrets
list` snapshot the kv-v2 mount beside it uses, so a rebuild that finds
it mounted does nothing;
- a `swarm-services` role on it, `allowed_domains` read straight out of
`swarm.serviceDomains` — the same swarm-tier list swarm-ca.nix
name-constrains its sub-CA to and hive-tls.nix carries as the leaf's
SANs — with subdomains, localhost, globs and IP SANs all off;
- a `swarm-services-issuer` policy granting `update` on
`pki/issue/swarm-services`.
Inert: the mount has no issuer generated into it, the role therefore
cannot issue, and no login role attaches the policy, so no token in the
swarm carries it. No consumer changes; swarm-ca.nix still mints the
services sub-CA exactly as before. Reversible with
`bao secrets disable pki`.
The cert-auth role that attaches the policy waits for the leaf carrying
its CN, which glue-bao-tls.nix mints — a later step.
Refs #4256
Line 26's list named two identity kinds; mara flagged it as
should-be-three and the third went unnamed. A hive holds its own mTLS
leaf (glue-bao-tls.nix:130-131, CN = hiveName from :49) and logs in
with it (glue-queue-agent-credential.nix:171, bao login
-method=cert); swarm-bao.nix mints matching cert-auth roles for it
alongside swarm-controller (:1049-1052) and swarm-secret-publisher
(:1101-1104), and swarm-controller/src/read_policy.rs:184-188 writes
the per-hive role dynamically. The table further down this page
already lists a hive as a reader (swarm/hives/<hive>/matrix/appservice-token),
so the line-26 list was inconsistent with the rest of the page.
Refs #4468
A typo'd or invented --scope matched no label at all, and because
nothing carries a label in a scope that does not exist, every open
item came back reported as missing it -- the failure direction reads
as a finding instead of an error.
Validate --scope the same way list --label already validates
--label: reuse labels::repo_labels (the paginated label fetch) and
error with the bad scope plus the available ones, using the same
with_suggestions near-miss helper list --label's message uses.
The scope-parameter addition pushed the doc comment on token_request to 39
lines. Moved the HTTP-Basic incident story and the audience/scope rationale
to the crate README's new "Token request shape" section (docs/ is markdown,
exempt from the lint); the code comment keeps the pointer plus the one-line
summary of the invariant. Refs #4464
`swarm-logs query` got a bare nginx 401 from the swarm log store on every
query. The agent OIDC client is registered for `authelia.bearer.authz`
(`swarm-authelia.nix`'s `agentClients` sets `bearerAuthz`), but registration
is not issuance: the token request asked for no scope, so the token came back
carrying none, and authelia's `/api/authz/auth-request` refuses that exactly
as it refuses an unauthenticated caller.
The same failure is already recorded in `swarm-otel.nix` against the
collector's client, on the same scope string — prometheus asks for no scopes
unless told to, and every scrape was refused at introspection. This is that
bug one layer down, so it gets the same shape of fix.
`scope` becomes an opt-in parameter alongside `audience`, not a hardcoded
value or a config field: the two travel together (registered ≠ requested
applies to both) and only the destination decides whether either is needed.
`None` keeps every other caller byte-identical — the NATS connect callback,
`auth.rs`'s bridge client and the OTLP push client all pass it.
Refs #4464
The committed doc was stale (missing help text under --limit/--format
that the binary's clap-markdown output now includes). Regenerated with
the documented command. While at it: docs/tools/swarm-logs-cli.md was
never added to .prettierignore alongside its three siblings, so nix fmt
silently reformatted it back to a stale layout right after
regeneration — the exact failure mode .prettierignore's own comment
warns about. Added it so the *-docs-fresh check in nix/checks.nix stays
satisfiable.
An agent can reach VictoriaLogs only through the gateway, and since the
machine query route landed the way to read it has been to hand-roll a
client_credentials token request and a curl, per query. This is the CLI
that closes that: `swarm-logs query '<LogsQL>'`, matched log lines on
stdout, so the answer pipes into grep like any other command's.
Built to the plan posted on the tracker thread: own crate, own
docs/tools reference generated off the clap tree, `query` as the one
verb, and the JSON error body surfaced on a non-200 rather than
swallowed. No `tail`: streaming is a different endpoint with a different
response shape, and folding it in here would be a fatter scope than the
ask.
Minting the token is NOT implemented here — swarm-queue-client already
owns the client_credentials request, its error type and its CA handling,
and a token-endpoint fix has to be findable in one place. What this crate
adds is the agent-shaped half: the client id arrives as a *file* beside
the secret, so nothing outside nix/agent-modules/queue.nix spells
`hive-<name>-agent` twice. That is the same problem hive-agent's
swarm_queue module solves, and swarm-logs/src/auth.rs is its `decide`
restated over this binary's inputs.
⚠️ The plan named one thing to verify empirically before calling the auth
settled: whether authelia's bearer policy for the logs vhost accepts the
agent client's audience. Measured from inside a container: it does not.
The client minted a token fine but with `aud: []` and `scp: []`, asking
for the logs URL as an audience answered `invalid_target`, and presenting
the audience-less token to the gateway answered a bare 401. So
swarm-authelia.nix's agentClients gains `authelia.bearer.authz` and the
query URL as a second audience — authelia authorises a bearer token by
the URL being requested, and that URL is now one binding read by three
places rather than three spellings of one address.
The URL reaches an agent the same way its queue coordinates do: computed
on the host (a container cannot derive a gateway address), forwarded by
hive_c0re::meta into the container's option set, and consumed by a new
agent module that installs the binary *wrapped* with its coordinates —
the shape swarm-controller.nix installs swarmctl in. Gated on the queue
credential as well as on the URL: a binary that can only answer 401 is
worse than no binary, because an agent reads a 401 as "no logs", which is
the exact confusion the store's machine route was added to end.
The rewrite in #4445 flattened this section's new-functionality-vs-
existing-functionality split into unconditional rules, because it had
been anchored to the current-state table that PR deleted. Restore the
split on its own terms, per #4445's own description of the rule: new
functionality has to match the target shape immediately; existing
functionality can move step by step, provided each step moves toward
the target shape.
Refs #4445
Two lines still framed the invariant as one file total: line 19 ('plus
exactly one file') and line 21 ('the only credential on disk') implied
a single mTLS cert page-wide, contradicting the earlier fix that made
line 26 say 'every host has at least one'. Agents and swarm-level
services each need their own identity certificate, so a host running
several holds several. Restate both as per-identity, keep the target
shape (store + certs on disk, nothing else) unchanged.
Also relax the touching-a-credential rule: moving a secret into bao
without resolving its renewal strategy in the same change is
acceptable while the migration to this shape is in progress, provided
a follow-up issue is opened to settle renewal.
Refs #4445
The citation for the nix option description was 529-531, but the quoted
phrase 'A path, never a value.' appears on line 533. The correct range
529-533 includes both the 'operator places by hand' claim and the
'path, never a value' assertion that the doc attributes to that file.
Refs #4445
Drop the current-state table and the prose describing how the swarm
currently breaks the target (unrotated secrets persisted to disk outside
the store, no agent store identity, the appservice token's second
minter). Scope the path-never-value rule to the mTLS client certificate
— the one credential a nix option can carry, now that everything else
goes through bao. Trim the migration checklist to unconditional rules
for any credential-touching change, since there's no current-state table
left for the old existing-functionality carve-outs to reference.
Refs #4445
The target section described the target as a shorter list of secret files —
the intro promised "what changes is how many paths there are", and the
per-secret contract pointed at the current-state table's file-shaped columns.
That is today's shape with fewer rows, not the target.
Rewrite it around the invariant: no secret the store holds is ever written to
disk. The mTLS client certificate is the sole credential on disk, and the
section now says why — it authenticates the puller to the store, so it is the
one credential that cannot be fetched from it. Per secret the target still
states minter, reader and renewal, but the reader is now a process pulling a
store path at runtime under its own certificate, not a path being read off
disk.
The current-state section and all of its citations are untouched: the current
state genuinely is files, and that contrast is what the page is for.
Two vale rewrites in c33cc441 changed meaning, not just wording:
- "on track to become" asserted a trajectory the page's own inventory
contradicts (no bao identity anywhere, all renewals NONE, all
secrets plaintext). Restored intent: "what it should become".
- "most often misread" claimed an observed reader frequency nobody
measured. Dropped the claim about readers entirely; states the fact
directly instead.
Both stay vale-clean; no suppressions.
Fixes the Microsoft.Contractions/write-good.ThereIs/alex.Condescending/
Microsoft.Auto errors vale flagged in CI (prose-lint-errors, required):
contractions throughout, no sentences opening with 'There is', dropped
the 'auto-mints' hyphenation and the 'easily' condescension flag, and
renamed the credentials table's 'what it is' header to 'kind' (also
flagged once styles were resynced). No technical claim or file:line
citation changed. Same fix applied to the one new line in
docs/README.md's pointer to this page.
Audited all 22 file:line citations against source on this head, per
argus's PR review. Fixed:
- matrix_account.rs writer cell pointed at path-computation line 163;
the actual store.write() call is at 189-197 (now anchored to the
put_matrix_account function name too).
- swarm-secret-publisher.nix appservice-token minter cited the
"put on every run" comment (280-286) instead of the mint itself
(271-278).
- glue-matrix-bao-token.nix persisted-token cell cited the
registration-restamp comment (186-188) instead of the printf+chmod
(180-181).
- glue-queue-agent-credential.nix persisted-files cell missed the
secret's chmod 0600 at 219, citing 224-231; corrected to 218-224.
- swarm-otel.nix persisted-file cell cited 763-767, one line short of
the chmod 0400 at 768; corrected to 764-768.
- hive-priv/src/main.rs:368,413 (forge/github token cell) landed in
unrelated DestroyContainer/ListContainers/WriteAgentGithubToken match
arms; replaced with the write_forge_token/write_github_token function
names as a greppable, edit-resistant anchor.
- swarm-bao.nix:527-528 (the "operator places by hand" quote) missed
the quoted text, which starts at 529; corrected to 529-531.
15 of 22 already resolved exactly; no claim lacked supporting code, so
nothing was removed. 22 of 22 citations checked.
The swarm's credential docs say where every file lives. They do not say
whether it should be a file at all, so a discussion about direction has
had nothing to point at and each one re-derived the same table.
This page carries that table with the three columns the target contract
is written in — minter, reader, renewal — plus the column the target is
really about: whether the value is persisted outside the store. Stating
it flatly is the point. All four stored families are plaintext files on
disk, the appservice token twice; every renewal cell reads NONE; no agent
container holds a store identity at all, so the hive reads on its behalf
and writes a file in; and the appservice token has a second, uncoordinated
local mint that can diverge from the published one.
The target section is marked as a target throughout, because its first
line is the one most easily misread as fact: every host needing a store
mTLS certificate is where this is going, while today only the store's own
host auto-mints and swarm-bao.nix calls it the credential an operator
places by hand everywhere else.
The progressive-enhancement rule is stated as a table of questions a
reviewer applies to a pull request rather than as prose, since a rule
nobody can check is a preference. New functionality matches the target
immediately; existing functionality moves stepwise, and the questions
distinguish a step from churn.
Indexed from the docs root and the swarm README. It supersedes
secrets.md when the migration completes — at which point that file is
deleted and this one moves into its place.
A unicast DHCP renewal reply currently reaches dhcpcd only by matching
the firewall's ESTABLISHED,RELATED conntrack rule against the outbound
request. When that conntrack entry has already expired the reply is
dropped silently, with no log line anywhere. The client's broadcast
paths (DISCOVER, rebind) bypass netfilter entirely via a raw BPF
socket and never depend on this state — only the unicast renewal path
does.
This removes that dependency by accepting DHCP client traffic
unconditionally, gated on the firewall being enabled at all. It does
not identify or claim to fix the cause of any particular observed
renewal failure.
Refs #3389
The origin-membership check used `case " $remotes " in *" origin "*)`,
padding $remotes (git remote's newline-separated output) with spaces.
That only puts literal spaces around the first and last entries, so with
more than one remote the case never matches, and the loop's
`[ "$r" = "origin" ] && continue` then discards origin permanently.
Result: a tree with origin plus any second remote hard-failed even
though origin resolved fine (argus's repro: aaa-dummy + origin).
Replace with a real membership test: grep -qx over one entry per line.
Hardcoding origin/main broke in any checkout whose remote isn't named
origin (e.g. an agent worktree, whose remote is renamed to forge). Try
BASE_REF if set, else every configured remote in a stable order
(origin first to match CI, then the rest alphabetically), fetching each
remote's copy of the base branch if not already present. If nothing
resolves, exit 1 with a clear message rather than falling back to any
guessed range - an unresolvable base must never read as a clean pass.
Refs #4449
check-attribution-trailers.sh used `|| true` on `git log`'s exit status,
so a hard failure (bad range, unborn HEAD) and an empty-but-successful
range were indistinguishable — both fell through to the same
`-z "$commits"` exit-0 path. Capture the status via the `if` guard
(exempt from set -e on purpose) and exit 1 on a real git log failure.
check-issue-refs.sh piped `git ls-files | xargs grep | grep -v
lint:allow`, then swallowed the final exit code with `|| true`. Worse:
xargs itself collapses grep's exit 1 (no match) and exit 2+ (real error,
e.g. an unreadable file) into the same xargs(1) status (123 either way),
so even capturing that status can't tell them apart. Switched to `git
grep`, which runs once over the tracked set and hands back its own exit
status untouched (0 matched / 1 no match / 2+ error) — then branch on
that status explicitly for both the scan and the lint:allow filter step.
Refs #4439, #4442
check-attribution-trailers.sh: the origin/main fallback now fails
loudly if it cannot fetch/resolve origin/main, instead of silently
substituting HEAD~10 as the diff base.
check-comment-blocks.sh and check-issue-refs.sh: assert the tracked
file-list search matched at least one file before treating an empty
hit-set as clean, mirroring the existing assertion in
check-doc-refs.sh.
Refs 4442, fixes 4439
Reworded the two hits scripts/check-issue-refs.sh found — a bare hash-
number tag in .forgejo/workflows/ci.yml's push-trigger comment and a
hyperhive#4345 tag in matrix_account.rs's doc comment — into prose that
stands on its own, per the lint's own rule. Neither carried semantic
weight beyond what the prose already says once reworded.
Refs #4345
homeserver_or_configured_default read DEFAULT_HOMESERVER_ENV internally,
so its three unit tests raced each other by set_var/remove_var-ing the
same process env var with no synchronization under cargo test's default
parallelism (argus, PR #4443 review).
Take the default as a plain parameter instead of reading the env var
inside the function. The one env read moves to a new
configured_default_homeserver() helper, called once at the edge
(main.rs's startup diagnostic); homeserver_or_configured_default itself
is now pure and its tests need no env mutation at all.
Refs #4345