The topology doc keeps its filename and its second half (manager
special-casing, harness unit shape) — both are cross-referenced from
other pages and neither is about the parent field. Its first half is
rewritten: what topology.json is now, and a table of what the removal
took with it, so a reader who finds `<parent>` or `set-parent` in an old
issue thread learns it went away rather than moved.
The dashboard's tree-rendering section is marked dormant rather than
deleted: the walk is still in swarm.js and retiring it is the frontend
owner's call.
--run <id> against a run that was itself PR-triggered has the identical
defect #4632 fixed for --pr: a workflow_dispatch run writes no commit
status, so it can't clear the red (pull_request) check on that PR's sha
no matter how the dispatched run turns out.
resolve_run already distinguishes this case -- it recognizes a run's
prettyref as a PR pseudo-ref (#<n>) via pr_number_from_run_ref, then
used to call branch_for_pr to keep going. It now bails with the same
pr_refusal_message --pr uses instead, before ever building a dispatch
request. branch_for_pr has no other caller (--pr refuses before
touching it too, since #4632), so it's removed rather than left dead.
Push (non-PR-triggered) and --branch are untouched.
Regenerated docs/tools/forge-cli.md from clap help; corrected
docs/tools/forge.md's claim that --run always works.
The doc comment still described the old (never-shipped) behavior --
exactly the lie this whole PR exists to remove, just in --help instead
of the module doc or forge.md. Say what the flag actually does: refuse,
because a workflow_dispatch run writes no commit status and can't clear
a red (pull_request) check.
Regenerated docs/tools/forge-cli.md (derived from clap help via
'hive-forge markdown-docs').
A workflow_dispatch run writes no commit status, so ci-rerun --pr could
never clear the red (pull_request) check it claimed to be re-running for
- it dispatched a fresh run and printed a success message regardless,
even though the check stays red no matter how that run turns out.
--pr now refuses up front, before dispatching, naming the mechanism and
the working alternative (re-run from the web UI). --run and --branch are
unchanged: --run's own PR-pseudo-ref resolution and --branch's direct
dispatch are both untouched.
Fixes the exit-code/honesty defect from #4613; the workflow_dispatch vs.
pull_request event-type question (whether to close+reopen the PR to fire
a real pull_request event) is a separate, parked decision.
`vale --minAlertLevel=error docs` is a CI gate and origin/main passes it
with zero findings, so the twelve this branch added were a red build, not
a backlog: eight Microsoft.Contractions, one write-good.So, two
write-good.Passive, across the new credential-matrix row, the backfill
runbook and the mint-identity help text.
Contractions and the sentence that started with "So" are mechanical.
The two passive hits are rewrites: "`--hive` is required" becomes
"`--hive` has no default", which is the actual claim -- the flag has no
value to fall back on -- and the help text's "the agent's queue secret is
left exactly as it is" becomes "it leaves an existing queue secret
exactly as it stands", which also names who does the leaving.
docs/tools/swarmctl-cli.md is regenerated, not hand-edited; the wording
lives in swarmctl's clap doc comments.
Agent creation at swarm level is event-driven and nothing sweeps for
agents missing a credential, so an agent created before a credential
joined the mint never receives one -- nothing comes back around to it.
Without a way to re-run the mint by hand, the only route to giving an
existing agent its queue credential would be to delete and recreate the
agent.
POST /api/agents/{name}/identity enqueues the same MintAgentIdentity
node POST /api/agents declares, rather than writing inline: a second
code path that mints an identity is a second place for the four strings
that have to agree to disagree. swarmctl agent mint-identity is the
operator end, the same POST-and-print-the-node-id shape agent create
already has.
--hive is required on both ends. Neither the CLI nor the controller
keeps a roster of which agent runs where, and the credentials this mints
name a hive, so a default would be a guess that hands an agent subjects
on a hive it does not run on.
Documents the backfill as a runbook step, and fills in the renewal cell
the credential matrix requires for the new row.
swarm-logs covers every host-tier unit this tool could reach, so the
second, capability-gated path into host journald earns nothing and is
removed outright rather than disabled behind a flag.
Removed end to end: the MCP tool definition + handler, the
GetHostJournal/HostJournal wire variants, hive-c0re's
dispatch_host_journal handler, the ReadHostJournal capability, and the
harness-side capability->--allowedTools gate. get_host_journal was the
only capability that mapped to an MCP tool, so allowed_capability_tools
could only ever return an empty vec; it goes too rather than linger as a
function that provably does nothing.
capabilities::has_cap/caps_for stay: #4624 gave ManageRootAgent's
bind-mount enforcement (hive-c0re/src/lifecycle/host_config.rs) a
second caller of has_cap, so they're no longer callerless once this
lands on top of it.
hive-sh4re's journal module (JournalPriority) had no consumer outside
this tool and is deleted.
An existing capabilities.json still naming read_host_journal does not
error: capabilities::prune_unknown drops unrecognised names with a
warn!, and an agent left with no capabilities has its entry removed. No
migration step is needed.
Untouched: hive-c0re/src/dashboard/journal.rs's
read_host_journal_response, which matches the name but is the private
helper behind the operator-only GET /api/journal-host dashboard route
and carries no capability check.
Rewrite two bare issue references (#4472, #4477) as self-standing
prose per check-issue-refs.sh's requirement — no markdown exemption,
hash-number tags are dead weight to a public forge-mirror reader.
Apply iris's vale fix to docs/agent-lifecycle/approvals.md (passive
voice, two contractions, one auto- hyphenation).
`roles.json` had a seeding path: `topology::reconcile_roles` granted
`can_manage_top_level_agents` to MANAGER_NAME on every meta sync, and that
role is what put every agent's state/config dir, `/applied` and `/meta` into
the root agent's nspawn binds. Collapsing the role into the capability left
the capability store with no equivalent, so the root agent would have lost
its recovery mounts at its next container rebuild — silently, and only then,
since nspawn bakes bind flags at container start.
Seed it in `ensure_root_agent`, before the create/start branch so a hive
whose root container already exists still gets the grant. Seeded once rather
than re-ensured every boot: the role kept an empty-list tombstone so a revoke
stuck, and the capability store deletes an emptied entry instead, so "no
entry for the manager" cannot tell a fresh hive from a deliberate revoke.
File existence can — every grant and revoke writes capabilities.json, even
when the result is `{}` — so the seed fires only while the store has never
been written, and is inert forever after.
The target is `lifecycle::MANAGER_NAME`, the same const the deleted role seed
used and the one definition of "the root agent" in the tree; no name literal
at the seeding site. The written string comes from
`Capability::ManageRootAgent` via `IntoStaticStr` rather than being spelled
out, so it cannot drift into a name `prune_unknown` would drop.
The hive had two spellings of "this agent may act on agents that aren't
its children": the `ManageRootAgent` capability, which nothing checked,
and a `can_manage_top_level_agents` role in a third meta store,
`roles.json`, which owned the real grant — the bind mounts that put
another agent's state (rw) and config (ro) inside the holder's
container. The two drifted independently, and with the parent/child
hierarchy removed the role's set (`parent.is_none()`) silently became
every agent while nothing said so.
Collapse them. The mount grant now hangs off
`Capability::ManageRootAgent`, looked up through the one capability
path that already exists (`capabilities::has_cap` over
`capabilities.json`) rather than a second mechanism. `roles.json` and
everything that read, wrote or reconciled it is gone, along with its
`meta.rs` staging and commit-label wiring; nothing in the tree reads
that file any more.
The enum variant keeps its name deliberately. Renaming it would turn
every `manage_root_agent` already stored in `capabilities.json` into an
unrecognised name that `prune_unknown` drops without asking. Its
meaning, not its spelling, is what changed: "may manage any agent". The
doc comment and the description string now say that.
`top_level_agents()`/`top_level_agents_in()` are replaced by
`all_agents()`/`all_agents_in()`. Under "manage any agent" the mounted
set is every agent by definition, so the code states it instead of
deriving it from a predicate that no longer discriminates — and the
call-site comment explains that, because it otherwise reads as a
widening. The holder is no longer bound as its own virtual child: that
reproduced the own-state and own-config mounts exactly, so dropping it
loses nothing.
Review response on #4620: not having SSO is not a supported
deployment, so the type should not permit it, and the docs paragraph
explaining why SSO is always present is redundant once the type says
so.
- swarm.authelia.url drops types.nullOr.
- Every consumer's null-arm is gone: two option defaults
(swarm-controller's and swarm's own statusPublish.tokenEndpoint)
that produced an empty/null placeholder when the URL was null now
unconditionally compute the real derived URL. Five now-dead
"assertion = ... != null" guards (swarm-authelia's bridge,
swarm-grafana, swarm-otel, swarm-nats, hive-forge, hive-matrix) are
removed as unreachable — in every case the same URL was already
interpolated unconditionally a few lines below the guard.
- grafanaNoSso, the module-eval fixture whose sole purpose was
exercising the now-unsupported no-IdP refusal, is removed along
with its dedicated test case; swarm.authelia.url = null is a type
error now, not a value that reaches that assertion.
- docs/swarm/services.md: cut the clause about setting the option to
null and the sentence explaining why the URL is co-location-
independent — both redundant now that the type enforces it.
`swarm.authelia.url` defaulted to `https://<domain>` only when this host
ran the container, and to `null` otherwise — so the address a client is
given was a statement about co-location rather than about the swarm. A
swarm has one SSO provider; every hive addresses the same name and
resolution decides which address that reaches, exactly as
`swarm.otel.domain` already works.
The option stays nullable: "this swarm has no IdP" is still expressible,
it is just now something an operator states rather than something not
running the container produces. The Grafana fixture that exercised the
no-IdP refusal says it explicitly.
Closes#4536
mara's ruling on this PR: the principle that landed for the role path is
general, so the no-role path moves the task out of the system prompt as
well. `system_prompt_and_trigger`'s no-role branch still passed
`prompt_file` straight through as `--append-system-prompt-file` —
byte-identical to main's pre-PR behaviour, and the exact bug this PR
exists to fix. Since no agent ships roles yet, every real dispatch takes
this path, so the standing-system-prompt bug (a task re-asserting itself
as an instruction on turn 2/3/N under continue/goal) was still live for
100% of usage; only the unused role path had actually been fixed.
`compose_prompt` now always folds the task into `trigger`, role or not.
Without a role there is no role text to hold a system-prompt file open
for, so none exists at all — `--append-system-prompt-file` is omitted
from the spawn entirely, not pointed at anything task-shaped.
Replaces `without_a_role_the_task_file_is_what_claude_is_pointed_at`
(which pinned the bug as "unchanged") with a test asserting the task is
absent from what reaches `--append-system-prompt-file` and present in
the trigger. Role-path tests are untouched in behavior; only mechanical
fallout from `compose_prompt`'s `Option<PathBuf>` return and `&str`
trigger params (clippy needless_pass_by_value once both branches only
borrowed it).
Blast radius: this changes behaviour for every existing dispatch, since
prompt_file's content has always gone into the system-prompt file before
this fix.
A `start` may now name a role: `role: "reviewer"` loads the spawning
agent's own `subagent_roles/reviewer.md` and renders it, alone, into one
per-session file that `--append-system-prompt-file` points at. The role is
the system prompt; the task is the turn, never the other way round — a
task baked into the system prompt would re-assert itself as an
instruction on every later turn of a continued session, not just the one
it was written for. The task instructions (`prompt_file`) are read and
folded ahead of the turn's own prompt instead, the same channel that
carries them to the subagent without a role.
The argument is optional, so every existing call is unchanged — pinned by
a test that a pre-role payload still deserializes with `role` absent from
the schema's required set, and another that the no-role path reaches
claude with the caller's own file, unrendered, and the trigger untouched.
With a role, one test pins the system-prompt file to the role's text and
nothing of the task, and another pins the task still reaching the
subagent as the turn's prompt.
A role name with no file fails the call, before the session name is even
reserved, and the error lists the roles the directory does hold. No agent
ships roles yet, so named-but-missing is the ordinary first-run state; a
fallback there would spawn a subagent under a prompt missing every clause
the role existed to carry. An empty file and a name that is not a plain
identifier refuse the same way.
The topology predicate `is_descendant_of` gated the four schedule-
managing verbs: a caller could only name a schedule owned by an agent at
or below itself in `topology.json`. Those gates now permit any requester,
so the predicate, its pure `_in` form and the `schedule_authorized`
wrapper built on it are gone rather than left returning a constant. The
other two wrappers went earlier with the verbs they served —
`require_descendant` with the lifecycle MCP verbs in 87970a8c, and
`resolve_agent_state_target` with `get_loose_ends`'s agent parameter.
`require_group(agent, "scheduling", ...)` is untouched and still fires at
dispatch for every one of the five scheduling verbs, so holding the tool
group remains the gate; what goes is the ownership restriction layered on
top of it.
The three schedule-mutating verbs keep their row lookup as a plain
existence check, so a caller naming a schedule that does not exist still
gets `not found` rather than a message from deeper in the cancel path.
`list_schedules` stops filtering per row: it would only have hidden rows
the requester may act on anyway.
Error messages, tool descriptions and docs that described the subtree
relation are reworded — a refusal message naming a topology that no
longer decides anything is worse than none.
The six `is_descendant_of_in` unit tests go with the function they test;
the permit behaviour they leave unasserted is picked up by the next
commit.
Refs #4472
bao's metrics were scraped by the SWARM collector over loopback, via a
`swarm.otel.scrapeTargets.bao` entry gated on `deploy.swarm-otel.enable`
— "does the swarm's collector run on THIS host". It had to be: loopback
only reaches a reader that landed on the same host.
What that rendered everywhere else was nothing at all. Off that host the
metrics listener was not emitted, so the store's metrics reached the
store nowhere, and a host with no entry is indistinguishable from a host
nobody asked to scrape.
Moves the scrape into the collector this container already runs, per
mara on #4537: "move the existing scraper to the local collector". The
container shares the host netns (privateNetwork = false), so the scrape
still dials 127.0.0.1 — the listener keeps its address, its
`metrics_only` narrowing and its loopback-only bind, and the API
listener's `tls_require_and_verify_client_cert` is untouched.
The listener and its `prometheus_retention_time` lose their gate: the
reader ships with the store now, so there is no host where the endpoint
has none. The metrics pipeline reuses the logs pipeline's `resource`
processor and `otlphttp` exporter, so both signals carry the same
`service.name` and leave by the one hop.
Logs are unaffected: `journaldUnits` and --link-journal=host stay until
every sibling swarm container has a collector of its own.
The module-eval absence arm "a store with no collector beside it serves
no metrics" is inverted rather than dropped — the condition it asserted
is the bug. Three cases join it: the job is in swarm-bao AND gone from
swarm-otel (a move, not a copy), the scrape target and listener are both
pinned to loopback, and the metrics pipeline shares its exporter with
the logs one.
Both agent-facing tools go away end to end, with no replacement. This is
an intentional capability removal: agents can no longer enumerate their
own subtree, and can no longer queue a meta-flake input bump.
The system prompt and docs/tools/lifecycle.md land in this same commit
on purpose. A tool named in the prompt but absent from the server makes
agents confidently call something that doesn't exist, and the failure
then surfaces far from its cause.
Removed:
- MCP registrations and bodies (hive-agent-mcp), plus the now-unused
UpdateMetaInputsArgs.
- Wire variants Request::ListDescendants,
Request::RequestUpdateMetaInputs and Response::Containers, plus
ContainerInfo, whose only consumer was that response.
- hive-c0re's handle_list_descendants (its whole module) and
handle_request_update_meta_inputs, the two dispatch arms, and the
require_group(agent, "approvals", ...) gate on the meta-inputs verb.
- The stream_enrich emoji entry and argument formatter.
- docs/tools/lifecycle.md (both tools it documented are gone), its two
referrers, the tool-group tables and the agent-hierarchy prose.
Tool groups are kept, deliberately. ToolGroup::Lifecycle listed exactly
one tool and now lists none — it is vestigial, but the variant stays so
existing meta/capabilities.json grants still parse; retiring it is a
separate decision. ToolGroup::Approvals also listed exactly one tool,
but the group is NOT dead: check_can_cancel_approval still gates
cancel_loose_end's approval-cancel arm on it server-side.
ApprovalKind::UpdateMetaInputs stays too. Nothing in production code
produces it any more, but pre-existing approval rows may still carry it,
and the operator's own path to a meta update is unaffected — the
dashboard's POST /api/meta-update inserts the meta_update job directly,
bypassing approvals entirely.
The two format_ack tests in hive-agent-mcp that named
request_update_meta_inputs were only using it as a label string while
exercising the generic OkWarn/Ok renderer, so they are retargeted to a
surviving tool rather than deleted.
Note hive-c0re's priv_client::list_containers is a different thing (the
host-side privileged container listing behind hive-priv) and is
untouched.
Closes#4591
A swarm runs one homeserver and every hive on it logged in as the same
`@hive:` localpart, holding the same access token out of one swarm-wide
store path. That is one matrix identity for N hives: the homeserver
cannot attribute an action to the hive that took it, and revoking one
hive's standing revokes every hive's.
Three changes, and the third is the one that makes the other two real:
- **The localpart carries the hive's name** (`hive-<hive>`), derived in
one place, `swarm_secret_client::matrix::hive_localpart`.
`hive-matrix.nix` renders the same string as the appservice
registration's `sender_localpart`, so the shared account stops being
created rather than merely stops being used.
- **The store path is templated by hive**, not a constant. The
"a swarm runs one homeserver, so this is a constant rather than a
parameter" rationale went with it; it stopped holding the moment two
hives shared the homeserver it describes.
- **The path moved out from under the grant every hive has.** It sat at
`swarm/services/matrix/sender-token`, inside the
`secret/data/swarm/services/*` read stanza `policy::render` gives every
hive. It now sits under that hive's own stanza,
`secret/data/swarm/hives/<hive>/*`, which interpolates the reader's
name — so a hive reads its own token and is refused another's. The
policy renderer itself is unchanged: narrowing the `services/*` grant
would break the OIDC-secret read it exists for, and moving the
credential is what this needed instead. A policy test walks the
rendered stanzas and asserts none of hive alpha's covers hive beta's
sender token, so a later stanza that widened it fails here.
`swarm-matrix-ctl` takes a new required `MATRIX_MINT_HIVE` and writes
that hive's path; its store grant in `swarm-bao.nix` follows, scoped to
one hive's leaf via the new `deploy.bao.matrixCtlHiveName` (defaulting to
this host's `hiveName`) rather than a `hives/*` wildcard, which would
hand the matrix container every hive's token back.
Migration: no outage at deploy. `ensure_hive_user` short-circuits on the
local token file, so a hive keeps running on what it has; with no such
file it reads the new per-hive path, finds nothing, and falls through to
the existing register-or-appservice-login ladder against its own
localpart — which needs only the per-hive `as_token` on local disk. The
old shared object is read by nothing afterwards. Rooms do not follow the
identity, and that is the one operator step; both ways out are written
into `docs/integrations/matrix.md`.
No admin standing is granted to the per-hive accounts: `admin_execute`
stays empty and the assertion pinning it is untouched.
Three hits the recent write-good.Passive=error flip (hyperhive#4546) now
catches on this branch's content:
- "stored password is gone" -> "has lost its stored password"
- "the ordinary appservice re-login above is unaffected" -> named the
actual actor (the admin-sender limitation) instead of leaving it
passive
- "Federation is enabled" -> "tuwunel enables federation" (tuwunel is
already named one clause earlier in the same paragraph)
No suppressions, no content dropped.
Both spots hedged the swarm-level framing with a temporal qualifier
("until then", "today") that read as future work awaiting a fix, on
top of the framing itself. Drops the qualifiers; the operations are
swarm-level and rehomed there now, not pending anything from the hive.
Renames `swarm-matrix-minter` and reshapes it around subcommands. Minting
is now `swarm-matrix-ctl mint`.
Running rust inside `containers.hive-matrix` is not free: it needs its own
store identity, its own cert role and its own bind mounts, and every one of
those is per-*container*, not per-task. A second single-purpose crate would
have had to duplicate that plumbing to add one action, so the next thing
that has to run in there should be a verb here rather than a new crate.
The old name guaranteed the opposite.
`main.rs` is clap dispatch; the minting logic moves to `mint.rs` unchanged.
A bare invocation is refused: `mint` writes a credential, so "no verb"
defaulting to it would make a typo in the unit mint rather than fail.
The environment prefix moves with it, `MATRIX_MINTER_*` → `MATRIX_MINT_*`.
Scoped to the verb and not to the binary, because a binary-scoped prefix is
one the next verb has to share or widen, and a widened one never narrows
again. A test asserts every variable carries the verb's prefix.
The principal renames too. The cert role, bao policy, granting unit, leaf
filename and `certAuthCns` entry all have to spell one string the same way,
so leaving them as `swarm-matrix-minter` would have rebuilt the naming
split this branch exists to remove. Renaming the nix options alongside is
free here: every one of them is introduced by this PR and has never been
released, so no operator config names them yet.
`ExecStart` now names the verb, which is a contract between a nix string
and a clap enum that fails at deploy time with no local signal. Both ends
assert it: `mint_is_spelled_the_way_the_unit_invokes_it` in the crate, and
a new module-eval arm reading the rendered `ExecStart`.
docs/getting-started/setup.md drops the sender token from its "live on the
host" list: setup does not touch this credential, so a setup guide has no
reason to name it.
The store path and every identifier around it called this an admin
token. It is not one: of ~15 hive-c0re call sites only two need
homeserver admin, and the homeserver no longer promotes the account at
boot, so the name overstated both what the credential is and what it may
do.
Renaming it to the account was not enough either. "The `@hive:` token"
reads as the token of a hive user, and no such user is provisioned —
`@hive:<server_name>` is the appservice registration's own
`sender_localpart`, an account the homeserver creates for itself when it
loads the registration.
So it is the **sender token**: the matrix appservice sender account's
access token, at `swarm/services/matrix/sender-token`. The name says
what it authenticates as rather than what it may do, which is the part
that was wrong.
The path has one constructor, and the bao grant, the grant assertion and
three unit tests pin its literal independently — so a half-finished
rename fails a check rather than leaving the minter and its readers
disagreeing at runtime. `tracing` messages are renamed with the code, so
the journal reads the way the source does.
The host-side file keeps its name (`matrix/access-token`): it carried no
admin framing, and renaming it would orphan the file on every deployed
hive for nothing.
`docs/tools/hivectl-cli.md` is regenerated from the clap tree.
Three leftovers from the rename, plus the gating prose job.
`nix/packages/default.nix` still described the minter as the "matrix
admin credential's minter", and shipped that claim in the package's
`meta.description` — a PR-visible string.
`promote_user_to_admin`'s doc comment pointed at the `admin_execute`
entry in `hive-matrix.nix` as the thing that bootstraps the first
admin. That entry is gone, so the comment referenced nothing. It now
records that the account is ordinary, that the call therefore has no
working sender, and that rehoming at swarm level is the fix rather than
re-granting. `reset_user_password` gained the matching warning; it had
none.
The prose fixes clear all 8 `CI / prose lint (vale, errors)` failures,
all of which were in docs this branch touches. No vale config change,
no exception, no carve-out: contractions, one recast sentence, one
de-hyphenation and one dropped "simply".
Tense: four docs described system behaviour in the future ("will
refuse", "will fetch and trust"). Reference docs get read from the
other side of the change, so they say what the system does.
`swarm/services/matrix/sender-token` has a real minter
(`swarm-matrix-minter`, in the `hive-matrix` container) and two
readers now: the minter's own pre-flight read-back before it mints,
and hive-c0re's `stored_sender_token()` under the hive's own
certificate. The credentials table catalogs minter/reader/renewal for
every credential path; this one was missing a row.
Refs #4345
A swarm runs one homeserver and a homeserver has one appservice sender
account, so "mint it once" is a property of the thing being minted
rather than something a lock has to enforce. That is what makes this
account the one to move first: no trigger route, no controller change
and no agent list — a boot-time oneshot beside tuwunel is the whole
mechanism.
`swarm-matrix-minter` runs inside `containers.hive-matrix`, which
already holds the appservice token: the rendered registration is bound
in read-only because that is how tuwunel is handed it. What the
container lacked was an identity of its own, so this adds one — a leaf
from the store's CA with a grant of exactly one path, not the hive's
leaf, which reads every secret in the store.
Both ends of the credential ship here. The minter reads the path it
publishes to before it touches the homeserver, and returning on a
non-empty read IS the "only once"; `hive-c0re`'s `ensure_hive_user`
reads the same path, authenticating with the hive name already in
`HYPERHIVE_HIVE_NAME`. The existing mint-then-`M_USER_IN_USE`-login
ladder stays as the fallback for a store that is empty, unconfigured or
unreachable, which is every swarm deployed before this — so nothing
needs backfilling and nothing breaks if the rest of the sequence never
lands.
The credential is not an admin credential, and is not named like one.
It is the access token of the appservice registration's own
`sender_localpart` — `@hive:<server_name>`, an account the homeserver
creates for itself when it loads the registration. The store path is
`swarm/services/matrix/sender-token`, the host path is
`matrix/access-token`, and the homeserver no longer runs an
`admin_execute` promotion for that account at boot. Everything the hive
provisions with it — the Space, the chat room, their hierarchy and join
rules, the invites — rides on being the creator of those rooms at power
level 100, not on homeserver admin; there is no Synapse admin API here
to need, tuwunel has none.
Two operations do need an admin *sender* and therefore stop working:
`hivectl matrix promote-user` and `hivectl matrix reset-password`, both
`!admin …` messages into `#admins:<server>`, plus the password-reset
recovery path that an agent with a lost password file falls back to.
They are swarm-level operations and are left failing loudly rather than
served by an over-privileged token every other call site would also
carry. The sweep's own admin-rights check and self-repair go with them:
an account that is deliberately not an admin has nothing to check.
`ephemeral = false` stays, and hive root can still read the container's
filesystem. Accepted: what this buys is identity separation — no hive
*process* holds or reads the appservice token — not physical isolation.
Refs #4345
2 real active-voice rewrites (per mara: main picked these up via a
rebase+merge that landed after write-good.Passive went error-gate,
never checked against the current rule):
- "The datasource is told instead" -> nix/host-modules/swarm-grafana.nix
tells the datasource instead.
- "`value` is compared with `===`" -> the builder compares `value`
with `===`.
No suppressions, no content dropped.
The comment moved from swarm-grafana.nix into observability.md by the
previous commit became visible to vale (it doesn't lint nix comments).
Reword six flagged spots (contractions, a sentence-initial 'So', a
period outside quotes) without dropping any fact.
The 41-line comment on logLevelRules tripped the comment-block lint
(30-line max). Move the detailed walkthrough (why each rule shape is
what it is, the three silent-failure modes, the query a level button
emits) into docs/scheduler/observability.md's existing 'Log severity'
section, which already covered the mapping at a higher level. The nix
comment now carries the short why/contract and points at the doc for
the full detail — no information dropped, just relocated.
Grafana's log-level buttons filter on a field called `level`; no row in
the store has one. The store's name for it is `severity_text`, chosen by
VictoriaLogs' OTLP ingester rather than by us — v1.52.0's
`app/vlinsert/opentelemetry/pb.go` writes it unconditionally and the
ingest parameters have no `_level_field` to rename it with. So the
mapping is made on the reader: the VictoriaLogs datasource, which was
provisioned with no `jsonData` at all, now carries a `logLevelRules`
entry per severity the journald parser can emit.
`logLevelRules` is the datasource plugin's only level-related jsonData
key — there is no field-name setting and no OpenTelemetry preset to
switch on. It is read off `instanceSettings.jsonData` in the plugin's
`datasource.ts` and typed in its `configuration/LogLevelRules/types.ts`,
both recovered from the sourcemap shipped in the pinned artifact
(`grafanaPlugins.victoriametrics-logs-datasource` 0.26.3). Each enabled
rule appends an `OR severity_text:="INFO"`-shaped term to the query a
level button emits, next to the `level:…` term that matches nothing.
A wrong rule here fails silently: Grafana provisions unknown jsonData
without complaint and the buttons go on returning zero rows. The three
ways to get one wrong — a non-literal `enabled`, a non-canonical `level`
spelling, a value that is not the stored text — are recorded at the
binding, and a module-eval arm pins them along with the real failure
mode, a severity added to `nix/journald-severity.nix` and not here.
`Unspecified` is left unmapped on purpose: it is the store's own
rendering of an absent severity and the thing the logs dashboard's "no
severity" panel counts.
Refs #4560
Both blockers are done — #4548 (353 hits in hand-written docs) and
#4549 (46 hits in generated CLI reference docs, hivectl/swarmctl/hive-forge's
own clap help text) — and the previous commit suppresses the reviewed
false-positive remainder. vale --minAlertLevel=error docs now returns
0 errors, matching CI's existing prose-lint-errors job.
Closes#4546.
133 hits across 38 files, all previously classified during #4548's sweep
and deliberately left un-rewritten (predicate-adjective state/necessity
description, design-intent idiom, structural/type-description idiom,
no-single-actor topology claim, parallel-triple exception, vale
substring-match artifact — see hyperhive#4548's per-PR bodies for the
per-hit reasoning).
Wraps each one in a scoped <!-- vale write-good.Passive = NO/YES -->
pair (the supported mechanism — TokenIgnores has a known offset-drift
bug) rather than a blanket per-file or per-rule silence, so a *new*
passive-voice hit anywhere in these files still fails once the rule
gates CI (next commit). Table/list false positives (docs/swarm/credentials.md's
renewal-table cells) wrap the whole block, not each cell.
Part of #4546.
4 genuine passive-voice rewrites (actor named — you, the swarm, nix);
3 false positives left untouched — a design-constraint description
with no single correct actor across both provisioning modes ('is
issued with pathlen:1'), a governance/definitional statement ('who is
allowed to decide'), and the already-established predicate-adjective
classification ('is misconfigured').