docs/swarm/README.md gets the routes, the revision, the apply semantics
and the stopped-agent behaviour; the persistence doc points there from
the AGENTS.md entry. The swarm-controller, swarm-controller-api and
swarmctl READMEs and the repo map in AGENTS.md list the new bodies and
routes. docs/tools/swarmctl-cli.md is regenerated for
`swarmctl agent agents-md`, whose help text loses two passive phrasings
vale flags.
The agent's state/AGENTS.md stays the truth and its harness the only
writer. The harness mirrors the file into a new `agents-md` KV bucket,
and swarm-controller serves that copy, so a read works while the agent is
stopped. An edit is a unified diff plus the revision it was made against,
sent over the agent's existing control subject; the harness refuses a
stale base, applies the diff with `git apply --check` then `git apply` to
a temp copy, and writes the file only when both pass. Fixes#5519.
Files:
- swarm-queue-client/src/agents_md.rs, lib.rs: the bucket name and
subject, the mirror value, the `ApplyAgentsMd` control command and its
reply. `AgentControl` loses `Copy` because the new variant carries text.
- hive-agent/src/swarm_agents_md.rs, main.rs: the mirror task (start,
10 s re-read, immediate republish after an edit) and the apply.
- hive-agent/src/swarm_input.rs: the control loop answers both commands.
- hive-agent/src/claude_md_watch.rs: its AGENTS.md path is shared.
- hive-agent/Cargo.toml, Cargo.lock: sha1 for the git blob id (already in
the lock), tempfile moves from dev to runtime for the apply directory.
- swarm-controller/src/agents_md.rs, main.rs: the bucket reader and
GET/PUT /api/agents/{name}/agents-md; test AppState literals get the
new field.
- swarm-controller/src/agent_control.rs: the sink hands back raw reply
bytes, so each command decodes its own reply type.
- swarm-controller-api/src/agents_md.rs: the route bodies, shared with
swarmctl.
- swarmctl/src/agents_md.rs, main.rs: `swarmctl agent agents-md get|apply`.
- nix/host-modules/swarm-nats.nix, nix/module-eval/nats-authelia.nix: the
agent's publish grant on its own `$KV.agents-md.<agent>` key.
git needs no new package: agent containers enable programs.git, and the
harness unit's PATH includes /run/current-system/sw.
swarmctl decoded POST /api/agents and POST /api/agents/{name}/forge-token
into hand-written copies of the controller's private response structs.
Both now live in swarm-controller-api, so a field renamed on one side is a
compile error on the other. Wire format and OpenAPI schema are unchanged.
The request side stays mirrored: CreateAgentRequest carries inference
settings and an ArchetypeRef (swarm-queue-client types) and Permission
(hive-sh4re), and the permissions body is a list of Permission. Where
those types live is #5471's decision.
Files:
- swarm-controller-api/src/agents.rs, lib.rs: the two responses, with
round-trip tests whose literals come from the tests that move here.
- swarm-controller/src/main.rs: imports them; the struct definitions and
create_agent_warning_tests (now in the api crate) go.
- swarmctl/src/agent.rs: imports them; the mirrors and their decode tests
go, and the module doc says only the request is mirrored now.
- swarmctl/src/permissions.rs: its doc pointed at agent.rs for why it
mirrors; that reason no longer covers Permissions, so it gives its own.
- swarmctl/README.md, swarm-controller-api/README.md, CLAUDE.md: which
bodies are shared and which are mirrored.
Part of #5472
swarmctl hand-mirrored the controller's forge make-admin response and
the /api/schedules bodies. Both now import them from a new serde-only
crate, swarm-controller-api, the way hivectl and hive-c0re share
hive-host-sock. ToSchema sits behind its schema feature, which only
the controller turns on.
The wire format is unchanged: round-trip tests in the new crate pin
each type against the JSON the controller and swarmctl produced, and
the controller's OpenAPI document for these routes is byte-identical.
Files: swarm-controller-api/ is the new crate; swarm-controller and
swarmctl (src, Cargo.toml, Cargo.lock, root Cargo.toml) switch to it;
CLAUDE.md lists the crate and which swarmctl shapes are still local
mirrors; swarmctl/README.md says the forge and schedule bodies come
from the crate, so none of its lines still call them mirrored.
Part of #5472.
Permission::default_for and hive-sh4re's MANAGER_AGENT are gone: every
agent with no permissions key holds Permission::DEFAULT, ruth included.
The agent-name parameters that only fed default_for go with it
(permissions::effective, HiveCopy::before_read/after_read and their
callers).
swarm-controller seeds the grant ruth held by name: at start, a declared
ruth whose permissions are unset gets DEFAULT plus manage_root_agent
written into her key, once, logged. A set key is never touched. It is a
one-shot to delete once every swarm has passed it.
The swarm UI setup assistant opens the create form with
manage_root_agent checked, so a new ruth is created with the grant.
permissions_import no longer adds ruth to the import or splits her
default. Docs and help text no longer say ruth holds the grant by
default; swarmctl-cli.md regenerated.
mara on #5170: "dont split caps and perms. its all perms. / dont include
the now empty tool groups / nothing set -> default / also add perms to
agent creation screen".
- hive-sh4re: one `Permission` enum (messaging, meta, inbox, execution,
web_tools, manage_root_agent) replaces `ToolGroup` and `Capability`;
`lifecycle`/`forge` are gone. `Permission::default_for` is the default
for an agent the swarm sets nothing for.
- The record is `{ "permissions": [...] }`. Missing key, unset field or
no successful read yet = default; `[]` = none; a failed read after a
good one keeps the last value.
- hive-agent reads its own key every 10s and builds each turn's --tools,
--allowedTools and MCP config (rewritten before every turn) from it;
the subagent daemon reads it on each start/continue. Agents get a
read-only grant on their own key.
- hive-c0re restarts an agent only when its manage_root_agent changes.
The import now merges meta/capabilities.json and meta/tool-groups.json
into one list per agent and deletes both; the "import pending, don't
read" decision is `ImportOutcome::read_allowed`, table-tested.
- swarm-controller: `GET /api/permissions` (names + default) and an
optional `permissions` on `POST /api/agents`, stored before the job is
queued. swarm-ui's create-agent form gets a permissions field.
- Deleted: HIVE_TOOL_GROUPS and its forward to the subagent daemon,
tool_groups.rs, ruth's tool-group seed, the permissions rebuild job
(PermPayload/WritePermFile/perm_change), the dashboard permissions tab
and its stale-entries panel.
Part of #5170.
An agent's capabilities move from the hive's meta repo
(`meta/capabilities.json`, injected as `HIVE_CAPABILITIES`) into the
`capabilities` field of its key in a new `agent-permissions` KV bucket,
which swarm-controller alone writes.
- swarm-queue-client: `agent_permissions` (bucket, key, `AgentPermissions`,
direct-get read, the import subject and its wire types).
- swarm-controller: creates the bucket at start; `GET|PUT|DELETE
/api/agents/{name}/permissions` (deny_unknown_fields and unknown
capability ⇒ 422, duplicate ⇒ 400, agent must be declared by a hive);
answers each hive's one-shot import, writing a key only where there is
none and only for agents that hive's wanted state declares.
- swarmctl: `agent permissions get|set|clear`.
- swarm-nats-auth: a hive reads every key of the bucket and sends its
import on its own subject; the controller subscribes to the imports and
may answer them (`responses`).
- hive-c0re: reads each of its agents' keys every 30s into
/var/lib/hyperhive/swarm-agent-permissions.json and restarts an agent,
gracefully, when its capabilities change. Unset is the compiled-in
default (ruth: manage_root_agent; everyone else: none). A failed read
keeps the stored value. On first boot it seeds that file from
meta/capabilities.json, sends the grants to the controller, and deletes
the file once the controller answers.
Deleted: the capabilities.json reader/writer, `HIVE_CAPABILITIES` in the
rendered meta flake, ruth's capability seed, the capabilities half of the
permissions rebuild job (`PermPayload::Capabilities`/`Combined`), the
dashboard's capabilities table and `capabilities_changed` event, and the
now-unused `Capability::description`.
Part of #5170.
Swarm::http() built a reqwest client with no timeout. A token refresh runs
under the exclusive flock on the login cache (kept: authelia rotates the
refresh token, and a second refresher presenting the used one revokes the
login), so an authelia that accepted the refresh and never answered held
that lock forever and every other `swarmctl --swarm` invocation blocked
behind it in flock.
The client now has a 5 s connect timeout and a 30 s total timeout, the
pair swarm-controller's own clients of swarm services use. No caller
streams a response (every gateway call and every login POST reads one
whole body), so a total bound fits all of them. The lock is unchanged; it
is now held at most for one bounded request.
Closes#5308
`swarmctl login` (and any `--swarm` verb) on a controller host failed TLS
with `UnknownIssuer` against the swarm's own gateway: its leaf chains to
the store's `pki` root, which no system store holds, and the only way
through was `--ca-file`/`SWARMCTL_CA_FILE` by hand.
The host's swarmctl wrapper now sets `SSL_CERT_FILE` to the bundle the
swarm-controller daemon already trusts (`/run/swarm-controller-ca/
trust-bundle.pem`: system CAs plus the swarm's root), when the gateway is
self-signed. swarmctl's reqwest/rustls resolves roots the same way the
daemon's does, so this is the path the daemon uses to reach the same
gateway. `--set` rather than a default, since a nix shell exports its own
`SSL_CERT_FILE`.
Pointing `SWARMCTL_CA_FILE` at the bundle instead would not work (#5243):
`--ca-file` adds each certificate as a root and fails the whole client on
one it can't parse.
Hosts without the controller (an operator laptop) still need `--ca-file`.
Refs #5239Fixes#5243
`swarmctl login --swarm X` failed with "unexpected argument", and
`login --help` listed no `--swarm`: the flag was only accepted before the
verb. `--swarm` and `--ca-file` are now global, so they parse on either side
of the subcommand and show in every subcommand's help.
`--swarm` / `SWARMCTL_SWARM` accept a bare domain and prepend `https://`.
A value with a scheme is parsed as before, so `http://` is still refused.
On a swarm-controller host, the swarmctl wrapper sets
`SWARMCTL_LOGIN_DEFAULT_SWARM=https://<swarm.domain>` (`--set-default`).
Only `login` reads it, after `--swarm` and `SWARMCTL_SWARM`. The wrapper
does not set `SWARMCTL_SWARM`: that would send every socket verb (agent
create, schedule, inference, forge) through the gateway and make each need
a login. Other verbs resolve exactly as before.
No CA file is wired. The only CA material the module names is the
controller's assembled trust bundle (system CAs plus the hive anchor).
`--ca-file` hands every certificate in it to rustls-platform-verifier as
an extra root, and that fails the whole client on any one it cannot
parse, so pointing it at the system CAs is a risk nothing here can test.
The controller daemon trusts the same CA through `SSL_CERT_FILE`, which
skips unparsable certificates; using that for swarmctl is left open.
Machines that are not swarm-controller hosts (an operator laptop) get no
default; that case waits on an answer in the issue.
Fixes#5239
swarm-controller README, the swarmctl inference doc comment (and the
regenerated swarmctl-cli.md) and the hive-agent swarm_inference module doc
still listed only model, effort and subagent models.
Nothing capped how many subagents an agent runs at once; the operator
wants to set that per agent from swarm-ui (#5234, shape (b)).
AgentInference gains `max_subagents: Option<u32>`. An absent field
decodes as None, so every stored value still parses and means "no cap",
which is today's behaviour.
swarm-controller accepts 1..=32 on PUT. 0 would refuse every start; 32
is far more concurrent claude sessions than one agent container has
memory for, so anything above it is a typo, not a cap.
The subagent daemon already read the agent's agent-inference key for
subagent_models; it now reads it on every start and continue (not only
those naming a model) and refuses a new run once the cap's worth of
names is tracked, counted under the same lock as the name reservation.
Every tracked slot counts: running, starting, a goal run between turns,
and an interrupted one still exiting. A goal re-prompt never goes
through the reservation, so it is never refused. A failed read applies
no cap, as it falls back for models.
swarmctl's `inference set` does GET, merge, PUT; its mirror type now
carries the field so a set from the CLI keeps a cap set in swarm-ui
instead of dropping it, and `get` prints it.
Re-creating an agent mints its store identity the same way, so the
backfill verb and the `POST /api/agents/{name}/identity` route that only
it called go. The `MintAgentIdentity` job node stays: agent creation and
the renewal pass still queue it.
Part of #4765 (c).
The loopback listener served one connection at a time with no read
timeout, so a connection that never sent a request (a browser preconnect,
any local process) held up the real form_post until the 5-minute wait
ran out. Each connection now runs on its own task and the first final
answer wins. The callback body is capped at 16 KiB; over the cap it reads
as empty and can't complete the login.
A request the gateway answers 401 now triggers one refresh (under the
cache lock, skipped if another process already replaced the refused
token) and one retry; a second 401 says to run `swarmctl login`.
The login cache directory is a `Swarm` field so tests can point it at a
scratch directory. New tests: an idle connection does not block the real
callback; an oversized body is refused without ending the wait; two
concurrent refreshers refresh once and share the token; a refused token
is refreshed once and retried; a second 401 says to log in without a
further retry; with no `--swarm`, the verbs use the controller socket.
`swarmctl --swarm https://<swarm> login` runs the OAuth 2.0
authorization-code flow with PKCE against the swarm's authelia as a new
public `swarmctl` client: a pushed authorization request, the browser login
and explicit consent, and the code delivered by form_post to a loopback
port. The access and refresh tokens are cached at mode 0600 under
$XDG_STATE_HOME/swarmctl/ and refreshed under a file lock, since authelia
rotates the refresh token and revokes the login when a used one is
presented again. With --swarm (or SWARMCTL_SWARM), the agent, schedule and
forge verbs send their requests to https://<swarm>/api/ with the bearer
token instead of over the controller's unix socket, which stays the default.
authelia registers the client on every authelia host with a swarm domain:
`kind = "native"`, public, no minted secret, scopes offline_access and
authelia.bearer.authz, audience https://<swarm>/api/ (matched as a URL
prefix), redirect http://127.0.0.1/callback (any loopback port). Every
field is one authelia 4.39.20's validator requires for a public client
holding the bearer scope.
The swarm apex's /api/ answers a refused caller that sent an
Authorization header with the 401 itself instead of the login redirect,
so the CLI can tell "log in again" apart from an answer; a browser still
gets the redirect. The bearer token is not forwarded to the controller.
Refs #4409 (mara: c94764, c94782).
mara on #5034: "effort shares /models fate". The per-agent web UI no
longer sets effort: the `/effort` slash command, the effort picker,
`POST /api/effort`, `Bus::set_effort`, the `hyperhive-effort` writer,
`EFFORT_LEVELS`/`is_valid_effort` and the ACP effort-list payload are
gone, and `lib/modelEffort.ts` with them. The effort chip is
display-only and shows its source (`effort_source` in `/api/state`);
on ACP it stays hidden while the session offers no effort level.
The read side is unchanged: with no swarm value an agent runs an
existing `hyperhive-effort` file, else its agent.nix effort, else
medium. Existing files are not touched.
Refs #5034
The per-agent web UI no longer sets the model: the `/model` slash
command, the model picker, `POST /api/model`, `Bus::set_model` and the
`hyperhive-model` writer are gone. The model chip is display-only and
shows where the model comes from (`model_source` in `/api/state`). The
model is set on the swarm (swarm UI agents page, `swarmctl agent
inference set`).
The read side is unchanged: with no swarm value an agent runs its
agent.nix model, else an existing `hyperhive-model` file, else haiku.
Existing files are not touched.
With the picker gone, its model list goes too: `available_models` in
`/api/state`, the ACP model-list filter, and the harness's global
`HIVE_AVAILABLE_MODELS`. The subagent daemon keeps its own
`HIVE_AVAILABLE_MODELS` from mcp.nix, and the `availableModels`
assertion stays.
`/effort` (command, picker, route, file) is unchanged.
Refs #5034
`GET /api/agents/{name}/inference` answered `"configured": {}` both for
an agent with no key in `agent-inference` and for a stored value with no
fields, so a reader could not tell "the swarm sets nothing" from "a
setting with every field unset". It now reads the key as
`Option<AgentInference>`: an absent or deleted key is `null`, a stored
`{}` stays `{}`. The shared `schedules::read` helper is unchanged.
`swarmctl agent inference get` prints "no swarm setting; the agent runs
on its own model and effort" for `null`; `set` merges into an empty
value in that case, as before.
The agent itself still treats no key and `{}` the same.
Refs #5034
The header on `$SWARM.agent-state.<agent>` went out only when a bus event
changed it, and the core subject keeps nothing for a late subscriber. An
idle agent therefore said nothing to a reader that attached after its
last change: swarm-controller after a restart showed `effective: null`
for that agent until its next turn, which can be hours.
The publish loop now also wakes every 60 s and re-sends the current
header through the same `publish` path (clearing the dedup state first).
Transitions still go out as they happen. Same subject, same grant.
An agent whose queue connection is down now logs `swarm agent state:
publish skipped` once a minute even while idle, so the
`hh-swarm-agent-state-publish-skipped` alert also fires for idle
disconnected agents, not only busy ones.
Refs #5034
Each agent now reports the model and effort it runs on, and where each
comes from (`swarm`, `agent_nix`, `local_file` or `default`), in its
turn-state header on `$SWARM.agent-state.<agent>`: `effort`,
`model_source` and `effort_source` join `model`. The shared
`swarm_queue_client::agent_inference::{Effective, Source}` types spell
those fields for both ends.
The Bus keeps a source next to the model and effort. `Bus::new` takes it
from the local fallback chain (`harness_state::local_model` /
`local_effort`, which now return the source with the value), a `/model`
or `/effort` pick sets `local_file`, and the swarm poller sets `swarm`
or, on a clear, the local chain's source. A swarm value equal to the one
the agent already runs is now applied once, so the source moves to
`swarm` and the header is republished; before, `next()` skipped it as
unchanged.
swarm-controller subscribes to every agent's header with the reader
grant it already holds for the state relay, and keeps the latest
inference fields per agent in memory (`inference::Reported`).
`GET /api/agents/{name}/inference` now answers
`{configured, effective}`, `effective` being `null` until the agent's
first header with these fields since the controller started.
`swarmctl agent inference get` prints both; `set` reads `configured`.
Refs #5034
swarm-controller keeps each agent's inference settings in a new
`agent-inference` KV bucket, one key per agent, and is its only writer
(`GET`/`PUT`/`DELETE /api/agents/{name}/inference`; `swarmctl agent
inference get|set|clear`). Every field is optional. Each agent reads its
own key every 10 s through a direct-get grant on that key alone, and a
set `model` or `effort` replaces the Bus value, so the next turn runs on
it and the badge follows.
The swarm value is applied without persisting it: `Bus::set_model` and
`set_effort` are split into a persisting setter and a non-persisting
`apply_*`, and the poller uses the latter. Otherwise the swarm value
would overwrite `hyperhive-model`/`hyperhive-effort`, and clearing it, or
a restart, would no longer find the agent's own value. An unset field
changes nothing; a field cleared after being applied goes back to the
local value (`harness_state::local_model`/`local_effort`, the same
resolution `Bus::new` uses), read again at the clear.
`subagent_models` is validated, stored and shown by `get`; no agent
applies it yet.
Refs #5034
swarm-controller keeps every scheduled prompt (id, name, agents, body,
anchor, interval, paused) as one document under one key of a new
`schedule-definitions` KV bucket, so each change is a single
compare-and-set and a prompt exists for all of its agents or none. It
writes each agent's key of a new `agent-schedules` bucket as the prompts
naming that agent, without names or other agents, after every change and
on a one-minute reconcile pass. It is the only writer of both, behind
GET/POST /api/schedules and PATCH/DELETE /api/schedules/{id}.
`swarmctl schedule add|list|edit|pause|resume|rm` drives those routes over
the controller socket.
An agent's own queue credential is granted one more subject, the direct
get of its own key in `agent-schedules`. A KV watch is not granted: its
consumer create subject carries no key, so it would cover every agent's.
hive-agent's new schedule ticker reads that key every 10 seconds and fires
due slots as the same `schedule:<id>` todo hive-c0re's worker pushes. Fire
times are anchor + k*interval; the last fired slot per schedule id is kept
in the state db, so a body edit or list refresh never moves or refires a
slot, downtime fires the latest missed slot once, a restart refires
nothing, a one-shot fires once, and paused slots are skipped.
Refs #4914
authelia 4.39.20 exits at startup on `users: {}` ("users: non zero value
required"), and the first-boot unit seeded exactly that, so a swarm with
no users crash-looped authelia and answered 502 until `swarmctl user add`
ran.
The first-boot unit now writes one subject, `swarm.placeholder`, when
the users database is absent, empty, or exactly `users: {}`:
- `disabled: true` — authelia returns "user not found" for a disabled
user before any password check (file_user_provider.go,
CheckUserPassword).
- password: an argon2id digest with an all-zero key. It decodes (authelia
rejects a non-digest at startup) and no known password hashes to it.
- the `.` keeps it out of agent names (`[a-z0-9-]`), and `swarmctl user
add` refuses it as already existing. Neither writer removes users, and
both round-trip `disabled`.
A file with any user in it is never touched.
The docs that described the crash-loop (sso.md, gateway.md, setup.md,
the sso-unavailable error page) now describe the placeholder; the
writers' load_store docs and the seed fixtures follow. module-eval
nats-authelia asserts the seed branch.
swarm/README.md opens with the swarm and its control plane; hive identity
and the directory follow as the substrate. Upgrade notes move into a
<details> block, the per-agent queue publishing detail into another, and
the one-paragraph pointer sections collapse into a link list.
Fact fixes, checked against origin/main:
- an empty swarm.hives fails eval (swarm.nix:341-354); it does not mean
"not in a swarm"
- swarm.domain is required with a hive (hive-network.nix:156,188), hiveName
with a hive, store or homeserver (hyperhive.nix:161-166)
- the matrix container trusts the hive's trust-bundle.pem at runtime under
self-signed certs (hive-matrix.nix:1046-1052, lib/hive-ca-trust.nix:76-85)
- singleHostSwarm also defaults the controller, localHostsEntry, the nats
callout keys and the bao bootstrap token path (local-defaults.nix:72-129)
- swarm-controller serves far more than /health: roster, wanted state, job
graph, agent creation and credential mints (main.rs:2874-2899)
- swarmctl user add needs --email for the forge account and refuses an
existing user (setup.md:67-71, swarmctl/src/main.rs:425-430); document
agent mint-identity and mint-forge-token
- agent creation also mints store identity, forge token and matrix
account, and declares the agent paused (main.rs:1822-1920, 247-248)
Refs #3902
swarm-controller's POST /api/agents now refuses (409) a name the swarm
has already placed on a different hive: a non-Destroyed declaration in
that hive's wanted state, or a SetAgentWanted node still queued for it.
The same name on the same hive is that agent being re-created and goes
through. A wanted state that cannot be read refuses (503/500) instead of
reading as "placed nowhere". Creations are serialised from that read to
the graph insert so two concurrent creations of one name cannot both
pass.
Hive-level creation is removed: hivectl `agent create` / `request-create`,
HostRequest::Spawn / RequestSpawn, the dashboard POST /api/request-spawn
route, and ApprovalKind::Spawn with its approve/resolve arms and the
approval-carrying `templates::spawn`. The swarm path (deploy request or
wanted-state sweep -> queue_first_deploy -> templates::first_deploy) used
none of them. Old `spawn` approval rows are skipped by collect_lenient,
as `init_config` rows were in a3b672d1.
policy.rs's comment on agent_object_name stated swarm-wide name
uniqueness as a fact; it now says where it is enforced and what that
check cannot see.
Refs #4396
authelia's file backend has no self-service reset (no SMTP notifier),
so the only way a human account got a new password after the old one
was forgotten was hand-editing users.yml as root. `user add` already
hashes a password into the file; this verb does the same for an
existing user instead of refusing on the name.
Mirrors `user add`'s UX exactly: no password flag, authelia generates
and hashes it (never crosses argv), and it's printed once and never
stored. Refuses on an unknown user before ever invoking authelia. Same
publish path as add/update, so the same atomic write and no-restart
(authelia watches the file) behaviour apply.
Split the digest-replacement into users::reset_password so it's
testable without a command line or a running authelia, same pattern
as apply_update.
`swarm_queue_client::agent_token::format_agent_token` / `parse_agent_token`
are the spelling an agent presents its own queue secret in,
`swarm-agent.<agent>.<secret>`, and the one the auth-callout responder
reads back. The prefix is what separates it from an OIDC access token,
which may itself contain `.`. Parsing distinguishes "not an agent token"
(no prefix) from "a malformed one"; the error names the problem and never
the value. The module is store-free, so the agent formats its token
without linking the secret-store client.
`swarm_secret_client::queue::AgentCredential` loses `hive`: an agent's
identity is not tied to a hive, and nothing reads the field. Objects
already in the store carry it and still decode, since unknown fields are
ignored; a test parses one. The controller stops writing it.
With the credential no longer naming a hive, and the agent's policy
naming none since #4762, nothing in the mint consumes one. `hive` goes
from `mint_and_verify`, from the `MintAgentIdentity` node, and from
`POST /api/agents/{name}/identity`, which now takes no body and no longer
checks a hive against the roster; a caller that still sends one is not
refused, the body is ignored. `swarmctl agent mint-identity` loses
`--hive`, so passing it is now a usage error.
setup.md said Swarm SSO creates the operator's forge account, which was
not true until the previous commits. It now says how: sign in to the
forge once through authelia, then `swarmctl forge make-admin <you>`.
sso.md says what that first login does and why ACCOUNT_LINKING is
`login`. README, hivectl.md and forge.md drop `hivectl forge
create-user`, and the swarmctl README gains `forge make-admin`.
Refs #3782
Calls POST /api/forge/users/{name}/admin and prints what it found. It
fails with the controller's message when the user has not logged in via
SSO yet, and when the name is an agent's.
Refs #3782
forge-token.nix fetches swarm/agents/<agent>/forge-token under the
agent's own store identity into /run/hive-agent-forge-token/token, and
re-fetches on a timer so a rotation lands. hive-forge, the git
credential helper, hive-forge-notify, forge-avatar-sync and the web UI
read that file first and fall back to <state>/forge-token.
tea-login is deleted: it copied the token into ~/.config/tea, which
docs/swarm/credentials.md forbids for a store secret. hive-forge covers
the same verbs. swarmctl gains agent mint-forge-token.
Refs #3782
`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.
Rewrites every write-good.Passive hit in the hive-forge clap help text
into terse, imperative, active voice (meaning unchanged) and drops
clap-markdown's own fixed footer ('This document was generated
automatically by...') via MarkdownOptions::show_footer(false), since
that string isn't ours to reword and vale flagged it too.
docs/tools/{hivectl,swarmctl,forge}-cli.md are generated from each
crate's clap tree (see hive-forge/src/main.rs's MarkdownDocs verb) —
regenerated here from the fixed source, not hand-edited.
Refs #4549
`swarmctl agent create <name> --hive <hive>` POSTs `/api/agents` to
swarm-controller over the daemon's unix socket and prints the queued
job's node id.
It deliberately does not wait. The endpoint queues a DAG whose last node
*publishes* a deploy message; the hive's `hive-c0re` then converges on
its own clock, out of the controller's sight. So even a fully settled
graph would not mean the agent is up, and there is nothing this CLI
could wait for that would let it claim otherwise. Printing the id is
exactly what the response says and all of what it says.
Transport is a bare hyper HTTP/1.1 client handshaked onto a tokio
`UnixStream` via `hyper_util::rt::TokioIo` — the same crate family
`hivectl/src/watch.rs` and `hive-agent/src/web_ui/proxy.rs` already use,
all of it already workspace-pinned. The request/response shapes are a
local mirror rather than a shared crate: the controller's own types are
private to its binary and this crate does not link it, the same
separation `hivectl` keeps from `hive-c0re`.
Errors are reduced to one actionable line — the controller answers
RFC 9457 problem+json, so an unknown `--hive` reaches the operator as
the roster of hives that would have worked rather than a body dump.
Response `warnings` are printed when non-empty.
The nix module wraps the binary with `SWARM_CONTROLLER_SOCKET`, read
from the same `socketPath` the daemon binds.
Refs #4399
Per #4128 (mara: allow-everywhere false positives go in a central
list, otherwise fix in source). Testing surfaced better fixes than
the plan posted on the issue:
- 5x Microsoft.Contractions 'that is' idiom false positives: adding
the missing comma ("that is, ...") both reads better and satisfies
the rule's own negative-lookahead, so no suppression is needed at
all. Fixed in docs/integrations/forge.md, docs/tools/forge.md,
docs/tools/hivectl.md, docs/web-ui/dashboard.md, and
swarmctl-cli.md's generated source (swarmctl/src/main.rs, doc
comment regenerated via markdown-docs).
- persistence.md's 'is not' matching inside 'is nothing': reworded to
'there'\''s nothing' rather than add any exception -- dodges the trap
and is a genuine contraction besides.
- ca.md's 'it is' matching inside the already-correct 'it isn'\''t':
tried a central .vale.ini TokenIgnores entry first per the
allow-everywhere framing, but testing against the real file (not
just a synthetic snippet) found it silently fails to suppress
whenever markdown emphasis syntax appears earlier in the same
file -- an offset-drift bug in how Vale applies TokenIgnores, not
a config mistake. Reworded to "it'\''s not" instead, same fix
shape as persistence.md.
- config.md's 3 genuine Microsoft.Avoid 'backend' exceptions (already
flagged and accepted on #4139 -- an actually-pluggable LLM API
provider, matching the nix option's own name, not one internal
system to name): scoped inline vale suppression around just that
section, since this one really is context-specific rather than a
rule bug.
Verified: fresh 'vale docs/ --minAlertLevel=error' is 0 errors AND
0 warnings (was 10 errors). nix fmt 0 changed beyond the edits
themselves. pre-push lints (tracker-tag/comment-block/doc-pointer)
clean. cargo clippy -p swarmctl -- -D warnings clean. Diffed the
regenerated swarmctl-cli.md against the old copy to confirm only
the intended line moved.
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.
Fixes the "obvious ones first" slice of #4042 (mara: do the obvious
ones first) -- 81 hits across write-good.So, write-good.ThereIs, and
write-good.Weasel, all in docs/. Each is a genuine sentence rewrite
(lead with the real subject instead of "There is/are", drop a
sentence-initial "So ", replace a vague intensifier), not a blind
regex substitution -- read every hit in its real file context before
touching it.
3 of the 81 hits were in CI-generated CLI docs (docs/tools/{hivectl,
swarmctl,forge}-cli.md) -- fixed at the clap #[arg(...)]/doc-comment
source in hivectl/src/cli.rs, swarmctl/src/main.rs, and
hive-forge/src/verbs/repo_add_collaborator.rs, then regenerated via
each crate's `markdown-docs` subcommand so CI's freshness check stays
green.
Verified: fresh vale re-run shows 0 remaining So/ThereIs/Weasel hits
and no new hits introduced (983->982, exactly the one incidental fix
this pass also picked up at docs/scheduler/observability.md:48).
cargo fmt --check and clippy clean on the three touched crates.
Remaining write-good backlog (Passive: 726, TooWordy: 207) is
judgment-heavy and left for a follow-up slice of #4042, not bulk-
rewritten here.
docs/tools/swarmctl-cli.md and docs/tools/hivectl-cli.md are generated
(nix/checks.nix's swarmctl-docs/hivectl-docs freshness checks diff a
fresh 'markdown-docs' run against the committed copy). The earlier
Contractions/Foreign fixes in this branch edited the generated
markdown directly instead of the clap #[arg(...)]/doc-comment strings
in swarmctl/src/main.rs and hivectl/src/cli.rs -- so CI's freshness
check would regenerate the docs from unfixed source and silently
undo the fix (found by argus's review).
Applied the exact same 13 wording changes to source (5 in
swarmctl/src/main.rs, 8 in hivectl/src/cli.rs) that the earlier commits
already made to the generated .md, matched 1:1 against
'git diff origin/main HEAD -- docs/tools/{swarmctl,hivectl}-cli.md'
rather than guessed. Regenerated both docs from the now-fixed source
and confirmed byte-identical to what's already committed (both
'diff <(binary markdown-docs) docs/tools/X-cli.md' exit 0) -- source
and generated output are back in sync.
cargo clippy -p swarmctl -p hivectl --all-targets -- -D warnings and
scripts/check-doc-refs.sh both clean.
Pure `nix fmt` output from the commit before this one — no hand edits.
203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs.
Reproduce with `nix develop -c nix fmt` on the parent commit; the result
should be byte-identical to this tree.
None of the 13 `.prettierignore` entries appears here — verified by
intersecting the changed-file list against the ignore file, with a
control proving the intersection finds a match when one exists.
Its last call sites were the JSON store's to_string_pretty/from_str,
which this branch replaced with serde_norway. Nothing in swarmctl/src
names it any more.
Caught by argus, and it is the second time: the same reviewer found a
dead reqwest in swarm-controller after a move earlier this week. No lint
in this workspace sees an unused dependency, so both were green locally
and in CI -- a move has two sides and my gate only looked at one. My
local gate now carries an advisory check for it.
Six places asserted the old design as fact, and none of them mention the
change by name -- the class of doc breakage that is found by asking what
a diff made untrue, not by grepping for a feature:
- swarmctl/README.md and swarm-authelia-bridge/README.md both described
their own private canonical store. The bridge's "known limitation"
section described the seam as unsolved; it is what this fixes, so it
becomes what both writers must uphold instead.
- docs/swarm/{sso,ui,secrets}.md described a rendered artifact.
- The repo CLAUDE.md entry for swarmctl said the same.
- docs/tools/swarmctl-cli.md is regenerated (CI diffs it against the
clap tree), picking up the removed --store flag.
Operator-facing where it is read: the hand-editing consequence (values
survive a rewrite, comments do not) is stated in sso.md, where an
operator is being told to edit the file, rather than only in a module doc.
The second half of making one file canonical. swarmctl kept its own
private JSON store and rendered users.yml from it, against the same
physical file the bridge wrote -- the seam that made `swarm agent create`
refuse to run.
- users.yml is read before it is written, so users another writer added
are loaded rather than treated as a file to refuse or clobber. The
overwrite guard and the seed check go with the second store: they
existed to police two stores that could disagree.
- serde_norway replaces the hand-rolled emitter. Unknown top-level and
per-user keys round-trip through `extra`, so two writers cannot delete
each other's fields.
- The synthetic email moves from render time to the write path and is
stored. With the file as the store, "rendered but not persisted" has
nowhere left to live, and mara ruled the stored address correct.
- `--store` is removed rather than deprecated: a flag whose only
remaining effect is nothing reads as accepted and does nothing.
Three tests asserted the emitter's exact bytes, and one asserted the
guard. Rewritten rather than deleted -- as round-trips for the former,
and inverted for the latter, since "a populated file is READ" is the
behaviour this change is for and deleting its test would leave it
unpinned.
`swarm agent create` failed with "no user store at
swarm-authelia-bridge-users.json but users.yml already holds users" on
any hive that had users. The guard was correct; what was wrong is that
two files both claimed to be canonical for one physical file.
The bridge kept a private users.json and rendered users.yml from it,
while swarmctl kept its own pair against the same users.yml. A writer
whose own JSON was absent could not tell "nothing here yet" from
"someone else's users", so it refused to write at all.
users.yml becomes the store: read before write, through serde_norway
rather than a hand-rolled emitter. Unknown top-level and per-user keys
round-trip through `extra`, or whichever process writes second would
silently delete what the first added. Validation stays on the write path
-- a bare to_string(&store) serialises perfectly and drops the "no
control character ever reaches this file" guarantee silently.
The seed constant goes with the guard: nothing writes a seed now, an
absent file is an empty store, and left as a pub constant it read as if
the seed dance were still load-bearing.