The ACP agent was spawned with only `kill_on_drop`, which SIGKILLs the
agent's own pid. Commands the agent had started (a `nix build`, a test
run) were reparented and kept running after the agent was killed: when a
subagent run ends after `interrupt`, when an agent ignores
`session/cancel` past the grace, and when the harness drops the runtime.
The agent now leads its own process group (`process_group(0)`), and
dropping its `Connection` SIGKILLs that group. The soft path is unchanged:
`interrupt` / `Canceller::cancel` still sends `session/cancel`.
Refs #4999. The claude half needs hive-claude 0.1.3, which is not on
crates.io yet.
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
advance_applied_main refused every rev that does not descend from
applied/main, including an ancestor of it. Ancestors arrive in normal
operation: swarm-controller queues each merge's TriggerDeploy as an
independent node with no resource or edge between them and spawns each
runnable node on its own task, so two merges of one agent can publish in
either order; the meta-update cascade fast-forwards applied/main to the
forge's main on its own; and the webhook handler does not dedup a
redelivered merge event. Since a refusal now logs at ERROR and fires the
`hh-config-rev-refused` alert, each of those would page as a refusal.
An ancestor is now RevAdvance::Superseded, logged at info, nothing queued.
Only a rev that has diverged (neither descendant nor ancestor) is refused.
Refs #4851
A deploy request whose rev hive-c0re refuses (fetch failed, or the rev
does not descend from applied/main) no longer tries to comment on the PR
that merged it. Off the forge host that attempt always failed with "forge
core token absent". The refusal now logs at ERROR, message unchanged, and
the new `hh-config-rev-refused` Grafana rule fires on it.
Deleted with the posting: actions::post_rev_deploy_failure_to_pr, the
whole forge/pr_comment.rs (config_repo, merged_pr_for_commit,
post_pr_comment), and ReaderToken::value, whose only caller was
merged_pr_for_commit.
Refs #4851
The issue-comments endpoint takes no page or limit, so `comments
--since` cut its window client-side from a whole-list response. The
leftover over-fetch-by-one request (`.page_size(limit + 1)`) was a
no-op and the `--limit` clamp to MAX_LIMIT (49) guarded a truncation
check that no longer needs one.
Drop both: `--since T --limit N` now returns N comments. `comments
--json` still emits `since_limit_clamped`, always `false`. `timeline`
is untouched; its endpoint pages and its clamp stays.
The forge's issue-comments endpoint takes no page or limit and always
returns the whole thread. #5055 wrapped view's fetch in all_pages, which
never saw a short page on a thread of 50+ comments and fetched the same
list 50 times: `issue view 3782` printed 3000 comments instead of 60.
`comments --limit K` sent K as a page size the forge ignored, so it
returned the whole thread.
view and pr status's last_comment now send one request. comments fetches
the thread once and cuts the --limit / --tail window client-side; the
X-Total-Count probe and the computed-page tail walk are gone, and the
count of comments outside the window is the length of the list.
--since is unchanged.
tests/comments_no_paging.rs runs the binary against a loopback mock
forge whose comments route ignores page and limit, as Forgejo's does.
priv_client::list_containers now waits at most 30s (LIST_WAIT) for
hive-priv's `nixos-container list`, via the existing `within` helper. Its
periodic callers (crash_watch, agent_sockets, swarm_agent_status, wanted,
mcp_sockets, swarm_status via container_view) already treat an Err as a
skipped cycle, so a hung listing no longer stops their loops for good.
lifecycle::is_running / unit_state run `systemctl is-active` with
kill_on_drop and a 10s timeout (IS_ACTIVE_TIMEOUT); a timeout warns and
reads as not-running / UnitState::Other, the same as a systemctl that
could not be spawned. The new lifecycle::running_state returns None for
that case, and crash_watch keeps the agent's previous observation on None
so a hung systemctl is neither reported as a crash nor as a recovery.
`call` itself and every mutating op (start/stop/destroy/send-snapshot/
daemon-reload) stay unbounded.
Closes#5061
The forge clamps page_size to 50. branches, pr reviews, the review
bodies merged into `comments <pr>`, and view's comment list each sent
one request, so anything past the first page was dropped silently.
labels::repo_labels did loop, but asked for 100/page and stopped on
`len < 100`, so a clamped full page of 50 ended the walk.
Add verbs::all_pages: requests 50/page until a short page, capped at
50 pages with a stderr warning when the cap is hit. Use it in all of
the above; repo-labels now calls labels::repo_labels instead of its
own copy of the loop.
list --json now prints the same "N of M shown" stderr trailer as the
pretty output; its paging and stdout shape are unchanged.
Closes#5051
With actionsFromSelf, Forgejo resolves bare `uses: actions/<name>` against
the forge itself, so every action a workflow names needs a mirror there.
Only actions/checkout was seeded, and coverage.yml's
`actions/upload-artifact@v3` has failed at job setup with "repository not
found" since the CI runner moved to swarm-ci-runner (e2da5509).
The single hardcoded checkout mirror becomes a list of action names both
share, mirrored from github.com/actions/* like checkout already was. v3
points at the same commit (ff15f03) on github.com and code.forgejo.org.
The controller's forge-objects pass creates any missing mirror on every
pass, and the changed SWARM_CONTROLLER_FORGE_MIRRORS env restarts the unit
on switch, so an existing forge picks the mirror up on deploy.
Closes#5040
The publisher was a oneshot with RemainAfterExit=true, started only through
`wantedBy = container@hive-forge.service`. Once it had run it stayed
`active (exited)`, so a later container start made its start a no-op: a token
re-minted without a host reboot or a unit change never reached the store.
- RemainAfterExit is dropped, so each start runs the script again.
- systemd.paths.swarm-bao-forge-publisher watches the token file in the
container (PathChanged) and starts the publisher when the mint writes it,
the same shape as swarm-secret-publish's path unit.
- The code comment, secrets.md and credentials.md now name both triggers;
forge-placement pins the missing RemainAfterExit and the path unit.
Refs #4998
A controller that started before the forge's host had published its token
ran without forge access until someone restarted it: on a split-host first
deploy that is a manual step. Now the forge client lives in a slot that is
filled once (`Late<T>`, an `Arc<OnceLock<T>>`):
- `forge::enable` tries the store once at start. On failure it logs once and
re-reads in the background with backoff (30s doubling to a 5-minute cap,
no log per retry), fills the slot and logs once when the token is there.
- The forge passes (#5014's per-hive token mint among them), the config-PR
poll and the webhook registration start from that moment, through
`start_forge`'s callback.
- Job nodes and HTTP handlers read the slot when they run, so they answer
"no forge" before and work after.
A token replaced in the store after the slot is filled still reaches the
controller with its next restart. The round-1 change that shared
`queue_identity::fetch_with_retry` is reverted; nothing else uses it.
Refs #4998
The controller's forge token never entered bao: the forge minted it in its
container, hive-forge-swarm-controller-token copied it to a host file, and the
controller LoadCredential'ed that file. forgeTokenFile defaulted to that path
only where the forge ran, so a controller on any other host had no forge
access unless an operator copied the file by hand.
Now the forge's host publishes it and the controller reads it:
- swarm-bao-forge-publisher (replaces hive-forge-swarm-controller-token)
waits for the in-container token, logs in with its own forge-publisher leaf
and writes it to swarm/controller/swarm-controller/forge/token. It runs on
every forge container start, the old unit's trigger.
- New principal swarm-forge-publisher: leaf minted by glue-bao-tls.nix,
policy create/update on that one path, written by
swarm-bao-forge-publisher-policy, CN reserved as a hive name.
- The controller policy gains read on that path. swarm-controller reads it
at start under its own certificate with the OIDC secret's retry, and runs
without forge access if it still cannot.
- forgeTokenFile, its LoadCredential and the forgeEnv gate are gone;
SWARM_CONTROLLER_FORGE_URL is always set. hostSwarmControllerTokenFile is
removed, and a tmpfiles rule deletes the old host copy on the forge host.
The in-container file stays: the mint unit's only-once guard reads it, and
it is the publisher's source.
Refs #4998
Agent creation gains a GrantDocsRead node (after CreateForgeUser) that adds
the agent as a read collaborator on internal/docs, and forge::docs_grant
runs a pass over the store's hive-agent-* roster at start and every five
minutes that queues the same job for any agent that is not one. The pass only
adds; an unreadable roster or forge read changes nothing.
hive-c0re drops shared_docs_access, its call in sync_agent, and the
SHARED_ORG / SHARED_DOCS_REPO constants only it used.
`hive-<hive>` is each hive's read-only forge account, and an agent's forge
account takes the agent's name, so an agent called `hive-<x>` would claim
hive `<x>`'s account. `hive_types::reserved_agent_prefix_violation` names
the rule, and `broken_name_rules` applies it, so both creation paths that
go through it (`POST /api/agents` and placing an undeclared agent on a
hive) refuse such a name for a new agent. An agent already in the roster
keeps its name with a warning, as for every other naming rule.
The email check between hive and agent accounts stays as the second
guard, for agents created before this rule.
Refs #4851
swarm-controller now gives every hive in its directory a forge account,
`hive-<hive>`, in a new `hives` team of `agent-configs` (read on
`repo.code` + `repo.pulls`, every repo including later ones), and mints
one `read:repository` token named `swarm-hive` into the store at
`swarm/hives/<hive>/forge/token`. A five-minute pass queues a
`MintHiveForgeToken` node for a hive whose token is missing or no longer
matches the forge, whose account is missing, or whose team membership or
team settings drifted. A hive whose store or forge read failed is never
queued, so an outage cannot rotate every hive's token. The token decision
reuses `agent_token`'s classifier and replace step, now parameterised by
name and scopes.
An agent may be called `hive-<x>`. The hive account is created with
`hive-<hive>@hives.hyperhive.local` and the job refuses an account by that
name with any other email before it locks it down, adds it to the team or
mints for it; agent creation already refuses the hive's account by the
same email check.
The controller's bao policy gains the `swarm/hives/+/forge/token` leaf
(create/read/update), pinned by the bao-grants module-eval assertion. The
hive's own policy already reads `swarm/hives/<hive>/*`.
hive-c0re reads that token through `forge::reader_token()`, falling back
to the core token file when the store holds none, and uses it for:
fetching a merged config rev (swarm deploy, `hivectl forge
reconcile-config`), reseeding a purged `applied`, the meta-update
cascade's fast-forward, seeding `proposed` on first deploy, and finding
the PR that merged a refused rev. Those reads no longer require a local
`hive-forge` container. The `/knowledge` clone moves from the forge
host's boot sweep into the knowledge pull, so every hive clones it.
Posting the deploy-failure comment on that PR stays on the core token.
Refs #4851
The reseed bail text, the `ensure_all` doc, the forge module doc and a
rebuild comment still described hive-c0re mirroring the applied config to
`agent-configs/<name>`. State what the code does now; scope the
config-changes doc to swarm-created agents and say what a legacy agent
whose repo has no `main` gets.
Refs #4851
Per the operator's ruling on #4851 (option (a)): the hive holds no
write identity on the forge, so the `push_config` mirror of an applied
repo's tags and `main` to `agent-configs/<name>` is removed, along with
its `run_config_push` helper and its call in `sync_agent`.
Docs and comments that described the mirror now say that forge `main`
advances only by an operator's merge.
Refs #4851
A hive's queue credential and the swarm controller's could subscribe to
every subject in the swarm account, so a hive could read every other
hive's deploy requests and every agent's terminal, state, input and
request replies.
Their grants now carry a subscribe allow-list:
- a hive `hive-<h>`: `$SWARM.knowledge`, `$SWARM.deploy.<h>` and its
reply inbox `_INBOX.client.hive-<h>.>`
- the reader (swarm-controller): `$SWARM.term.*`, `$SWARM.term.*.sub.*`,
`$SWARM.agent-state.*` and its reply inbox
`_INBOX.client.<client-id>.>`
`swarm_queue_client::connect`, which hive-c0re and swarm-controller both
use, now connects with `_INBOX.client.<client-id>` as its inbox prefix,
the same shape agents got for their own credential. In async-nats 0.50
every request reply and every ordered consumer (the hive's wanted-state
watch, the controller's KV key listings) delivers under that prefix, so
the one inbox grant covers them all.
A hive or controller still on a build without the prefix has every
JetStream reply refused once the responder runs this policy, so the
clients need to be rebuilt before, or with, swarm-nats-auth.
Closes#4945
`continue_` cleared the stop reason, reset the turn budget and remembered
the caller's `dir` before the session lookup ran, and nothing restored
them when the lookup refused. With the strict lookup a refusal is an
expected outcome (the caller omitted `dir`), so every such retry erased
why the session stopped from `status`, gave a capped goal session its
budget back, and made the wrong `dir` the remembered one for the next
bare `continue`.
The lookup now runs first, against the dir peeked rather than committed;
only a session that is found gets `State::begin_continue`, which does
all three. Same for the ACP path's recorded-session check.
resume_target no longer falls back to the bare name when the project dir
holds no session titled it: continue (and a goal run's next turn) refuses
before anything spawns, naming the directory searched. A store that cannot
be resolved refuses too, with the io error. The fallback notice is gone.
resume_target logs the fallback and returns a notice naming the project
dir searched; continue appends it to its reply and to a failed resume's
error, so a caller hitting claude's 'matches 2 sessions' knows to pass dir.
The test's final assertion now pins the whole attach list so the continue
site is covered on its own, and the fallback branch has a test.
`continue`, and every later turn of a goal run, passed `--resume <name>`.
claude 2.1.287 resolves a title across the project dirs of every git
worktree of the repo and refuses when two sessions match, while `start`
archives a prior session only in its own cwd's project dir. A name first
started under one worktree and then started again under a sibling stayed
live in both, and every resume of it failed with `--resume "<name>"
matches 2 sessions`. That is the #5000 incident: build-4921a/b were first
run under /home/atlas/wt/4901-build, then under their own worktrees.
Both resume sites now look up the live session titled `name` in the
config's own project dir (the same `SessionStore::find_by_title` lookup
`start` archives with) and pass its id. The id comes off disk at each
resume, so it survives a daemon restart. When that dir holds no session
under the name, the name is passed as before and claude's answer stands.
The test drives a fake claude that keeps sessions on disk and resolves
titles across project dirs the way claude does. Its script is written by
a child process, so no write fd on it exists in the test binary for a
concurrent fork to inherit (the ETXTBSY of #4898).
The responder reads its OIDC client secret at
`swarm/services/<client-id>/oidc/client` with the store login it already
opens for the callout keys, and holds it in memory. `--client-secret-file`
is removed.
When introspection answers non-2xx, the responder re-reads the secret from
the store once and, if it changed, retries the introspection once. Both fit
inside the request's 1.5s introspection budget, under the server's 2s
callout timeout: the budget is now one deadline for the whole request. A
re-read that does not finish by then keeps running in the background, the
request is denied, and the next request introspects with what it read. One
re-read runs at a time; a refusal while one runs waits on it.
In swarm-nats.nix the `swarm-bao-nats-auth-oidc` fetch unit, the flag and
the `oidc-client.secret` LoadCredential entry are gone, along with the
unit's policy ordering in glue-bao-readers-policy-order.nix. A tmpfiles `r`
rule removes the copy a container still holds at
/var/lib/nixos-containers/swarm-nats/var/lib/swarm-nats-auth/oidc-client.secret;
nothing reads it.
No policy change: the `swarm-nats-auth` role already reads this path
(`natsAuthReaders` in swarm-bao.nix), which is the role the removed fetch
unit logged in as.
Deploy order: none, and no operator step. The store's grant already exists,
so the queue host can be deployed before, after or without the store's host
and authelia's. Rolling back re-creates the fetch unit and the file; the
store leaf is untouched.
On a hive fronting no swarm name, swarm-services-cert's token-less
GET pki/ca/pem presents baoHiveIdentity{Cert,Key}File again. The fetch
never logs in and grants nothing; the client certificate only completes
the store listener's mTLS. A hive host without the store therefore needs
no copy of the services-issuer pair.
The issue path, which logs in to pki/issue/swarm-services, still presents
only deploy.bao.servicesIssuerClient{Cert,Key}File.
module-eval-hive-tls: the root-fetch cases pin the hive identity and the
absence of the services-issuer pair (a fixture holding both, and one
holding only the hive identity); the issue-path case pins the pair on a
hive host and on a host without a hive; a gateway without the pair
evaluates and its issue path names both options.
Root-fetch identity: option (a), mara on PR #5002 (comment 92591).
Refs #4921
Drop the eval assertion b78190e1 added. It refused every self-signed
gateway host that does not run the store, which is the default hive
shape, and contradicts docs/swarm/secrets.md's supported no-store
deployment. The approved #4921 plan has such a host evaluate and fail
at runtime; `requireServicesIssuer` already exits 1 naming both
options, on the root-fetch branch and before issuing.
The module-eval case that pinned `hiveWithController { }` as refused
now pins it, and a hive-less gateway with no pair, as evaluating with
the unit's refusal in the script. The fixture pairs the assertion
forced into forge-placement, nats-tls and swarm-otel-identity are
reverted.
Refs #4921
swarm-services-cert issues the swarm-services leaf a self-signed gateway
serves, and on a host fronting no swarm name fetches the store's pki root.
It is gated on the gateway, not on a hive, yet its leaf pair was named
under deploy.hive-controller.tls and defaulted from the hive reader's cert
directory, and the root fetch presented the hive's own identity. A gateway
host with no hive had no such identity, so that branch was red by
construction.
- The pair moves to deploy.bao.servicesIssuerClient{Cert,Key}File, beside
the store's other host-unit readers, and glue-bao-tls.nix defaults it on
the store's host from the leaf it already mints. The old
deploy.hive-controller.tls.baoClient{Cert,Key}File paths are
mkRenamedOptionModule aliases. glue-services-issuer-bao-identity.nix,
which derived the default from deploy.bao.clientCertFile, is gone.
- The no-swarm-name root fetch presents the services-issuer pair. The hive
identity is now only hive-tls-resign's.
- A self-signed gateway with no pair fails evaluation, naming both options.
A pair named but unreadable still fails the unit, naming the paths.
Refs #4921
`swarm-bao-nats-callout-xkey` runs `ensure-callout-keys`, importing
/var/lib/swarm-nats-callout/{issuer,callout-user}.seed when present so the
keys keep their public halves, then fetches all three public halves from
the store. The verb removes each seed file once a read of the store returns
a seed; when the store's seed differs, it is kept and a warning names the
file. No tmpfiles rule touches those files, since one could run before the
import with the store sealed and leave the next run to mint a new key.
The server renders the issuer and user public halves from the fetched files,
checked for shape as the xkey's is, and does not start without them. The
responder reads both seeds from the store, so the copies into the container
and their LoadCredential entries are gone, and a tmpfiles rule removes the
copies a container still holds. `swarm-nats-callout-keys` is removed.
On the store's host the fetch unit is also ordered after
`swarm-bao-secret-publisher-policy`, so the publisher's denial of the seed
paths is written before the unit creates or keeps a seed there.
`deploy.nats.autoGenerateCallout`, `calloutUserPublicKey`,
`calloutIssuerPublicKey`, `calloutUserSeedFile` and `calloutIssuerSeedFile`
are removed with `mkRemovedOptionModule`, under `swarm.nats` too, each
message saying the keys live in the store.
glue-nats-bao-identity.nix and glue-matrix-ctl-bao-identity.nix defaulted
deploy.nats.baoClient{Cert,Key}File and deploy.matrix.ctlBaoClient{Cert,Key}File
to siblings of deploy.bao.clientCertFile, the hive's own store reader. That
pointed a store host whose reader was moved at files nobody mints, and on a
queue or homeserver host without the store that held a placed hive reader
invented a nats.pem / matrix-ctl.pem path beside it.
Both glues are now gated on the store running on this host
(deploy.bao.enable, the condition glue-bao-tls.nix mints under) and name the
directory the minting unit writes to, through nix/host-modules/lib/bao-pki-dir.nix.
Host shapes:
- Store host, default reader: unchanged. The evaluated paths stay
/var/lib/swarm-bao-pki/nats{,-key}.pem and
/var/lib/swarm-bao-pki/matrix-ctl{,-key}.pem, and the rendered units are
the same.
- Store host whose hive reader was moved: now gets the leaf minted there.
- Queue or homeserver host without the store, holding a hive reader and not
naming these options: no default any more. It still evaluates (neither
option has an eval assertion). swarm-bao-nats-tls exits 1 naming
deploy.nats.baoClient{Cert,Key}File when the queue's certificate is missing
or due for renewal; swarm-bao-matrix-oidc exits 1 naming
ctlBaoClient{Cert,Key}File, and the swarm appservice render/publish units
and the PKI bind mount no longer render. If that host had placed nats.pem
or matrix-ctl.pem beside its hive reader, it worked before and now needs
both options named.
- Hosts without the store and without a hive reader, and hosts naming the
options: unchanged.
Both refusals and option descriptions now say the default comes from
deploy.bao.enable.
Refs #4921
glue-nats-auth-bao-identity.nix defaulted deploy.nats.authBaoClient{Cert,Key}File
to a sibling of deploy.bao.clientCertFile, which is the hive's own store
reader. That tied a swarm service to a hive identity, pointed a store host
whose reader was moved at a file nobody mints, and on a queue host without
the store that held only a placed hive reader invented a nats-auth.pem path
that passed the eval assertion and failed at runtime.
The glue is now gated on the store running on this host (deploy.bao.enable,
the condition glue-bao-tls.nix mints under) and names the directory the
minting unit writes to, shared through nix/host-modules/lib/bao-pki-dir.nix.
A queue host without the store gets no default and the eval assertion
refuses it until the responder's own leaf is named; the assertion message
now names deploy.bao.enable as where the default comes from.
On a default store host the evaluated paths are unchanged:
/var/lib/swarm-bao-pki/nats-auth{,-key}.pem.
Refs #4921
Every nats-server this module starts renders `auth_callout.xkey` or does
not start, and the responder serves only that server, in the same
container and generation. A request without `Nats-Server-Xkey` therefore
comes from no supported server, so the plaintext framing is gone: such a
request is logged at warn and gets no reply, and the server that sent it
times out the connect as a denial. A signed denial would have had to go
out unsealed, which is the path this removes.
Also scopes two claims to what is true: the xkey's public half is read by
the swarm-nats-auth policy only, and the metadata-delete gap in the
replace-key procedure is about the two xkey paths, not all of
`secret/metadata/`.
secrets.md now says that the responder reads the seed at every start, so
a store outage plus a responder restart refuses every client; that on a
split deployment the store's host deploys first; and that replacing the
key takes `bao kv metadata delete` on both paths, since a soft delete
keeps the metadata and the create is then refused. It also drops the
claim that every token reads `swarm/public/`.
Writing bao's `default` policy needed a new stanza in the granter's own
policy, which only the bootstrap token can upload, so deploying it took
a manual root-token step. Nothing needed it: the one reader of the
callout xkey's public half is `swarm-bao-nats-callout-xkey`, which logs
in as the `swarm-nats-auth` role, and that role's policy already names
the path. `swarm-bao-nats-auth-policy` writes that policy as the granter
on every deploy, under the granter's existing `sys/policies/acl/swarm-*`
grant.
Removed: the `swarm-bao-public-read-policy` unit, its marker block, the
granter's `sys/policies/acl/default` stanza, the granter-role restart of
that unit, the bao.md section and the module-eval cases pinning them.
swarm-bao-nats-callout-xkey runs on the queue's host before the
swarm-nats container, under the responder's store leaf: it runs
`swarm-nats-auth ensure-xkey`, reads the public half back from the
store, and writes it where the container's runtime config dir is. On a
change it restarts the server.
nats-server now always starts through the runtime wrapper, in manual
mode too. A privileged ExecStartPre renders auth_callout with the xkey
(and, in auto mode, the two generated nkeys) at every start, and fails
the server when no xkey has been fetched. The auto-mode key unit now
only mints seeds and writes their public halves.
The responder is wanted by nats.service, so a server started late, once
its key arrived, brings the responder up with it.
The swarm-nats-auth role gains create+read on exactly the callout xkey's
two leaves, and no update, so a stored pair is never replaced.
swarm/public/ is readable by every token through bao's default policy.
swarm-bao-public-read-policy reads that policy, replaces only its own
marker-delimited block, and writes it back when it differs; it never
writes a policy it read back empty. Writing `default` is a new grant on
the granter's own policy, so on an existing store it lands only after
the bootstrap-token step re-runs; until then the unit says so and exits
0, and swarm-bao-granter-role restarts it on success.
swarm.forge.ci.* were per-host options that 4423da48 had already moved to deploy.forgejo.ci.*, so they rename straight to deploy.ci-runner like the forgejo.ci ones; ciRunnerRemoved goes with its last caller.
The forwarded enable now trips deploy.ci-runner's store-identity assertion on a host with no deploy.bao.enable. The assertion names both options and the default, the doc states the requirement, and a module-eval case pins the refusal.
deploy.forgejo.ci.{enable,name,concurrency,labels,package,jobTimeout} each
have a same-typed, same-meaning counterpart under deploy.ci-runner on the
same host, so they are mkRenamedOptionModule entries again and a config that
still sets them evaluates with the standard rename warning. The swarm.forge.ci
spellings stay removals.
docs/scheduler/ci.md states the rename, that the old hive-ci runner's entry
is deleted by hand in the forge admin panel, and that actionsFromSelf
defaults on where the controller runs.
The CA-file assertion compared the option's raw string, so
`/var/lib//swarm-ca/root.pem` or `/var/lib/hive-tls/./ca.pem` got past it.
Both sides are normalised lexically now: empty and `.` components drop out,
`..` removes the one before it, a trailing `/` goes. A symlink is not
resolved at eval time, so a path through one still passes; docs/swarm/ca.md
says so.
A switch that changed `hive-tls-ca` stopped and started it, and the stop
propagated through `Requires=` to the store's container (sealed again under
the shamir seal), every container binding the bundle, and every
`<name>-ca-bundle` unit with its consumers. `reloadIfChanged` with the same
script as `ExecReload` rewrites the bundle in place and restarts none of them.
`Requires=` stays, so a failed assembly still keeps them from starting.
With `swarm-services-root.pem` missing, `hive-tls-ca` writes an empty bundle
and every `<name>-ca-bundle` unit fails. Its log line now names the root's
path. docs/swarm/ca.md describes both.
The docs, option descriptions and comments that named the swarm root, the
swarm CA or the hive CA as an existing trust anchor now name the secret
store's pki root or the trust bundle that holds it:
- docs/swarm/README.md, secrets.md, bao.md
- deploy.bao.clientCaFile's and gateway.verifiedProxyTo's descriptions
- comments in hive-c0re/src/meta.rs, hive-agent/src/swarm_queue.rs,
swarm-logs/src/auth.rs, swarm-controller/src/vcs_metrics.rs,
hive-forge, swarm-authelia, swarm-ci-runner, glue-bao-tls, swarm-bao,
swarm-grafana, swarm-otel, otel, hive-c0re, lib/hive-ca-trust.nix,
hive-forge/Cargo.toml and the flake-update workflow
Nothing behaves differently: every edit is a nix, Rust or TOML comment, an
option description, or a test assertion message. The Rust edits change the
workspace source, which this PR already rebuilds for hivectl.
Refs #4588
- swarm-services-cert (and its daily renewal) treats a services leaf that
`openssl verify` rejects against swarm-services-root.pem as invalid and
asks the store for a new one, as hive-tls-resign already does for the
gateway leaf. The trust bundle is that root alone, so a leaf under any
other issuer verifies for no consumer. With the store unreachable the
unit fails, and where the gateway waits for the leaf, the import fails
with it.
- Every host also deletes the old hive CA's ca.srl and .swarm-ca-adopted
in deploy.hive-controller.tls.stateDir, with the same tmpfiles `r`
rules as ca.pem/ca-previous.pem/ca-key.pem. Nothing reads either.
- docs/swarm/ca.md: the services leaf's re-issue conditions, the two
extra deleted files, and that the deletion is irreversible: rolling back
to a generation with swarm-ca.service mints a new, unrelated root that
nothing issues under.
module-eval hive-tls: a case for the services-leaf check in the boot
issuance and the renewal (red with the line removed), and the deletion
case covers the two extra files.
Refs #4588
The gateway leaf comes from the store's pki mount, so a hive CA's
certificates anchor nothing and its key is a secret outside the store.
- hive-tls-ca writes trust-bundle.pem from swarm-services-root.pem alone;
the ca.pem and ca-previous.pem slots are gone.
- Every host deletes ca.pem, ca-previous.pem and ca-key.pem in
deploy.hive-controller.tls.stateDir with tmpfiles `r` rules that follow
the option. The removal shim, now host-ca-removed.nix, holds these
beside the /var/lib/swarm-ca rule and the swarm.ca.* removals.
- deploy.bao.clientCaFile, deploy.bao.serverCaFile and
deploy.swarm-controller.hiveClientCaFile naming /var/lib/swarm-ca or one
of the deleted hive CA files fail evaluation, naming the store's mTLS CA
(/var/lib/swarm-bao-pki/ca.pem on the store's host) as the replacement.
module-eval hive-tls covers the single-anchor bundle, the deletion (incl.
a relocated stateDir) and the assertion on all three options.
Refs #4588
The secret store's pki root issues every hive's gateway leaf and the
swarm's service names, so the root swarm-ca.service generated on the
host anchors nothing and its key is a secret outside the store.
- swarm-ca.nix is gone: swarm-ca.service, root.pem and root-key.pem,
and the swarm.ca.{autoConfigure,stateDir,validityDays} options. A
definition of any of them fails evaluation with a removal message
(swarm-ca-removed.nix), and a tmpfiles rule deletes /var/lib/swarm-ca
on every host. singleHostSwarm no longer sets a swarm.* option.
- hive-tls-ca drops the $root slot: trust-bundle.pem holds the store's
pki root and a hive CA's public ca.pem/ca-previous.pem only.
- hivectl peer-config no longer prints the scp line for the swarm root;
the store's pki root is the one step.
- The gateway's cert import no longer stands a hive leaf or a throwaway
in for a missing swarm-services leaf where it Requires= the issuing
unit: it fails, naming swarm-services-cert.service. The stand-in stays
where the gateway does not wait (no swarm name, or the store's UI
only, whose passthrough must start with the store sealed).
Comments and docs that named swarm-ca.nix, swarm.ca.* or the host root
now name the store's pki root.
Refs #4588
The disk sampler ran `du -sxb` as the unprivileged hive-core user over
each agent's state/, work/ and container rootfs. hive-core cannot read
the rootfs or all of the agent-owned files, so `du` failed and each
failed path silently counted as 0: the dashboard showed a few MB for
agents using over 13 GB.
Add a hive-priv `MeasureAgentDisk { agent_name }` op. hive-priv
validates the name, derives the three paths itself (the request carries
no path), skips the ones that don't exist, and runs one
`du -s -x -b -c -P` as root with a 10 min timeout. A timeout, a non-zero
exit or output without a `total` line is an error.
hive-c0re calls it every 30 min instead of running `du` every 5 min:
each pass walks every agent's full tree as root on the disk the agents
work on, and the sizes move slowly. A failed measurement is cached as
None, so `disk_bytes` is null, the OTel storage.usage sample is omitted
and the dashboard shows "unknown", never 0.
`Coordinator::agent_work_dir` had no other caller and is removed.
Closes#4956
systemd hands a unit's credentials to every process of that unit, and a
host job is a process of the runner daemon, so the `LoadCredential=` on
gitea-runner-swarm still let any workflow read the registration token
from `$CREDENTIALS_DIRECTORY`.
swarm-ci-runner-register.service, a oneshot under the daemon's dynamic
user and state dir, now holds the only `LoadCredential=` and runs the
precond and `act_runner register` (only when `.runner` is missing). The
daemon unit has no credential, no `EnvironmentFile` and no `TOKEN`; its
pre-start step starts the register unit, so a missing token fails the
daemon's start and `Restart=on-failure` retries it.
The precond compares `.runner`'s labels in the form the daemon rewrites
them to (`nix` -> `nix:host`); compared verbatim, a scheme-less label
re-registered the runner on every start.
tmpfiles removes the unused `registration-token.env` and upstream's
`.token-hash`/`.labels` markers.