After a rollback, a record a newer build wrote (a new enum variant, a
changed required field) stopped decoding, and the strict
`schedules::read` turned that into an error on every read and write of
the key until someone deleted it by hand.
- agent_limits / inference (one document per agent): an undecodable
document reads as unset, with a WARN naming the bucket, key and error.
The PUT's "equal to stored, skip the write" shortcut no longer applies
to an undecodable document, so a PUT of the defaults replaces it too.
- schedule definitions (one document, every schedule): salvaged per
definition, as the review-classifier run list is. An undecodable
definition is kept as its raw JSON, hidden from the list, edits and
projections, written back by every commit after the readable ones, and
its id is never minted again. A document with no list of schedules
stays an error: reading it as empty would empty every agent's key.
- per-agent agent-schedules: an undecodable key reads as no schedules
and is rewritten on the next projection, even when the agent has none.
`schedules::read` is replaced by `read_or_default`; `wanted` keeps its
own strict read.
Closes#5193
A run record this build couldn't decode was dropped from the list it
read, and the next recorded run wrote the list back without it, so a
rollback followed by a roll-forward lost the newer build's runs for
good. The stored list is now a list of entries, each a decoded run or
the raw JSON of one that didn't decode. The route still shows only the
decoded runs. An undecodable entry keeps its place, counts toward the
100, and is written back as it was read. A read that finds any logs one
WARN with their count and the first error.
The settings decode derived its known fields from what
`Settings::default()` serializes, so a later field skipped when unset
would have been stripped from a valid stored document and lost on the
next PUT. The known fields are now an explicit list. A test serializes
a fully populated `Settings`, checks that its keys match the list, and
checks that `decode` round-trips it.
The controller keeps the classifier's newest runs as one JSON array and
its settings as one JSON object, each under one KV key. A document this
build could not decode, as after a rollback past a build that added an
outcome or a settings field, failed every read and every write until
someone deleted the key by hand. A classifier newer than its controller
refused every run request for the missing `model`.
The run store now decodes the array one record at a time: a record that
does not decode is dropped with a WARN naming the bucket, key, index and
error, and a document that is not an array reads as no runs. Listing
keeps working, and the next recorded run writes back only the records
that decoded.
The stored settings ignore a field this build does not know; one that
still does not decode reads as the defaults with a WARN. The PUT body
keeps `deny_unknown_fields`. A PUT now always writes, so a PUT of the
defaults also replaces an undecodable document.
A run request without `model` runs on haiku, the swarm-wide setting's
default, now one constant in swarm-queue-client.
`schedules::read` gains a sibling, `read_with`, taking the decoder.
Closes#5162
The metrics datasource defaulted to http://127.0.0.1:<port>, which only
reached the store when Grafana and VictoriaMetrics ran on the same host.
Swarm-tier services may not assume colocation (#4499), so the default is
now https://<swarm.victoriametrics.domain>, the gateway vhost the store's
host already serves. That vhost's query routes carry no auth_request, and
the Grafana container already trusts the hive CA for its authelia token
exchange, so no credential is added.
The logs datasource stays on loopback: the log store's query route needs
a bearer token Grafana does not hold yet.
Part of #3742 (item 4, metrics).
`swarmctl agent create` declares an agent the swarm has no declaration
for as paused, so ruth needs setting `up` from her card after step 4.
Restores the warning to re-create agents before a hive-side change makes
a container require a store identity.
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 poller read `<state>/github-token` once at start, so after a PAT
rotation it kept polling with the revoked token until the unit
restarted. It now re-reads the file on every tick, as
`hive-forge-notify` does: a different non-empty value rebuilds the
GitHub source, clears the own-login cache, and logs one info line. A
missing or empty file keeps the token in use.
A 401 from the notifications endpoint is now logged at warn, once per
token value; other non-2xx statuses stay at debug.
Part of #5017 (slice G).
The provider key was resolved once, at spawn. A provider rejecting it
shows only as an empty end_turn or an idle timeout, neither of which
drops the agent, so a key rotated in the store never reached a running
opencode until the process died for another reason.
After a turn fails with EmptyEndTurn or IdleTimeout, the runtime now
resolves the key once more. When that yields a key that differs from
the one the agent was spawned with, the agent is dropped (SIGINT to its
process group, as every other respawn) and the next turn spawns it with
the new key and reloads the recorded session. No key read (inherited
env value, no store, a failed or timed-out read, nothing stored) keeps
the running agent. The failed turn's error is returned unchanged.
Part of #5017 (slice A).
The classifier ran every classification on a hard-coded `haiku` with no
`--effort`, and the controller ran at most two requested reviews at once,
also hard-coded, so changing any of them took a code change and a
redeploy.
swarm-controller now keeps one settings document,
`{model, effort, max_concurrent_reviews}`, in its own
`review-classifier-settings` KV bucket under `all`, the way it keeps the
schedule definitions. No document means today's values: haiku, no
`--effort`, two reviews. `GET /api/review-classifier/settings` reads it;
`PUT` replaces it whole, needs the gateway's `Remote-User` like the
try-out POST, and refuses a bad model (the inference route's check), an
effort outside the five levels or a limit outside 1..=8 with
problem+json 400.
Every run request (try-out and requested review) now carries the model
and effort, and the classifier passes them to claude instead of its
constant, which is gone. A try-out reads the settings when it is posted;
a review reads them for the limit before it waits for a place and again
for the model and effort once it has one. The review semaphore is
replaced by a running count under a resizable limit, so a PUT, or the
next review's read, changes how many run without a restart, and a lower
limit lets the runs already going finish. MAX_WAITING_REVIEWS is
unchanged.
Closes#5151, part of #5096.
Scheduled prompts live at swarm level (swarm-controller's
/api/schedules, `swarmctl schedule`, the swarm UI's /schedules page).
The hive dashboard's SCH3DUL3S tab only listed the rows left in
hive-c0re's `scheduled_prompts` table so operators could copy them
over; the operator confirmed everything is migrated.
Removed:
- the SCH3DUL3S tab, its pane, schedules.js, its CSS (including the
`.rqe-source*` rules, whose only emitter was schedules.js) and the
hub tile's "schedules" entry
- hive-c0re's GET /api/schedules and the `scheduled_prompts` store
(with its v1 column migration and test)
- hive-sh4re's `schedule` wire types, which only that route used
- docs describing the tab, the route and the two tables
The rebuild-queue cancel route that shared the module moves to
dashboard/rebuild_queue.rs under an OpenAPI tag of its own.
The two sqlite tables are not dropped: nothing opens them any more,
and persistence.md says they can be dropped by hand.
Closes#4916
Seven host oneshots fetch a secret from the swarm secret store into a file
their consumer loads at start: swarm-bao-grafana-oidc, swarm-bao-otel-oidc,
swarm-bao-matrix-oidc, swarm-bao-ci-runner, swarm-bao-forge-oidc,
swarm-bao-hive-otel-oidc and swarm-bao-forwarder-oidc. Each was
RemainAfterExit with no timer, so it ran at boot or deploy and never again:
a secret re-minted in the store reached its consumer only through an
operator restarting the fetch.
lib/store-refresh-timer.nix gives each one a timer (OnUnitInactiveSec 10min,
RandomizedDelaySec 1min, the agent's own store re-read interval), and each
drops RemainAfterExit so a timer start is not a no-op. Each script already
writes the file only when secret_differs, and refresh_consumer/refresh_unit
restart the consumer only when the file is newer than its last start, so a
run reading an unchanged value restarts nothing.
Two docs said these units do not run on a timer; credentials.md's
ci-runner row said the runner token is not re-pulled.
Part of #5017 (slice T).
One card per run, newest first, from GET /api/review-classifier/runs: repo,
PR, head commit, trigger, when, how long, how many classifications and the
outcome as a badge. A card opens to the try-out's result table, the overall
verdict, the reason and the posted review's link. TryOutResult takes the
answer it shows rather than a whole try-out result, and its styles move to
their own file.
Every run is recorded when it ends: a try-out when its reply settles it,
a requested review once its review is posted or the post fails, and a
request turned away by a full queue once its comment is posted. A
duplicate request that starts nothing is not recorded. The newest 100
are one JSON array under `recent` in a new controller-owned KV bucket,
`review-classifier-runs`, written with compare-and-set as
`schedule-definitions` is (five attempts). A failed write is logged; the
review is already posted by then.
Each recorded run is also one info event, "review classifier run
finished", with the record's fields as tracing fields. The controller
logs natively to the journal, so they reach VictoriaLogs as F_<NAME>.
GET /api/review-classifier/runs returns the list, newest first, gated
by the gateway like the try-out GET. post_classifier_review now returns
the posted review's html_url, which the record keeps as review_url.
Closes#5153, part of #5096.
A hive key becomes a certificate subject and a directory name, but only the
reserved-name and fragment guards looked at it, so `a/b` or `../x` evaluated
and misbehaved later. Assert at eval time that every key is 1-63 characters
of [a-z0-9-], the rule `Ident` applies in hive-types, and name the offending
keys in the message.
The store host's `swarm-bao-pki` now also signs, for every hive in
`swarm.hives` other than its own, the reader (`<hive>`), queue-agent
(`<queueAgentCommonNamePrefix>-<hive>`) and gateway-leaf
(`<hiveLeafCommonNamePrefix>-<hive>`) leaves, into
/var/lib/swarm-bao-pki/hives/<hive>/ with the basenames the own hive's
leaves use. Same CA, same `signLeaf`, idempotent on absence like every
other leaf in the unit. `signLeaf` now takes the CA from the top of the
PKI directory rather than from the leaf's directory; every existing
caller passed that directory, so their output is unchanged.
The operator still copies the files to the other hive's host and names
them in config; only the openssl step against the CA goes away
(mara on #5016, c95685: "ok mint").
Closes#5016
The row moves from the per-agent page into @hive/shared so both pages mount
the same one. The terminal's debug rows now follow the preference as it
changes, so rows already on screen hide or show at once instead of only the
rows that arrive after the toggle.
Eight classifications in .hyperhive/review-classifier.yml: no AI
advertising in commit messages, no committed secrets, no clippy
allow attributes, no debug leftovers, no dead code, tests with
behaviour changes, docs with operator-facing changes, one purpose per PR.
A swarm-controller test parses the real file so it can't rot, and the
rust source fileset in nix/rust.nix includes it so the test finds it
in the build sandbox.
systemd ignores `08%` and `085480%` for CPUQuota= and `08%` for MemoryMax=
("Invalid ... ignoring"), which leaves the agent uncapped. `hundredths` now
refuses a whole part that starts with `0` unless it is exactly `0`, for both
fields. `0.5%` and `0.01%` stay accepted.
The grant test covered the bare consumer-create subject only, so a grant
of `$JS.API.CONSUMER.CREATE.KV_agent-limits.>` would have passed it.
It now also checks a named consumer, bare and with a key filter.
Raised by the #4268 verify pass.
systemd ignores a CPUQuota= or MemoryMax= it can't parse ("Invalid ...,
ignoring") and runs the unit without that limit. A swarm cap replaces
the hive's answer, so such a value left the agent uncapped while the
route, GET and the dashboard all said it was capped. argus on #4268 (R3)
found values the route took that systemd ignores: 12.345%, 400.123%,
0.001%, 17000000T, 99999999999999999999T and 99999999999999999999%.
The validators now accept a subset of what systemd parses, measured
against `systemd-analyze verify` (systemd 260):
- a percentage takes at most two decimals and is above zero, up to 100%
for memory_max and 21474836.47% for cpu_quota (systemd keeps the quota
in hundredths of a percent in a C int, not a u64)
- a size is one parse_size term: digits, an optional fraction, then
nothing, B, K, M, G or T, case-sensitive. The whole part and the
fraction digits fit u64, the whole part (plus one with a fraction)
fits u64 once scaled, and the byte count is at least 1 and below
2^64 - 1, as systemd checks them.
P and E, which systemd reads, stay refused because hive-c0re's validator
refuses them, and one value the hive refuses stops every cap update on
that hive. Values are stored as given: lower-case suffixes and a B
after a multiplier are refused rather than normalized, as systemd
refuses them.
The newline-injection values from the hive's test are in both bad lists
and go through the router in a_cap_with_a_second_line_is_a_400_and_the_caps_stay.
GET reads through a new agent_limits::open that returns None for a
missing bucket, answered as {}, instead of creating the bucket.
Every 30 seconds `workers::swarm_limits` reads the `agent-limits` key of
each agent this hive has and stores the result in
swarm-resource-limits.json, where the drop-in, the heap ceiling and the
dashboard read it. A cap therefore follows its agent to whichever hive
runs it.
Each value goes through the hive's own validate_cpu_quota /
validate_memory_max before it is stored, since hive-priv renders it
verbatim into the container's drop-in, and one bad value refuses the whole
set (argus on #4268, carried over from the wanted-state consumer this
replaces). A failed read, or a bucket that does not answer, leaves the
stored caps as they are; three failures in a row raise a banner.
swarm-nats-auth grants every hive `$JS.API.DIRECT.GET.KV_agent-limits.>`:
read-only, and every agent's key, not just the hive's own. Scoping it to
the hive's agents would need a roster in the auth responder, which
policy.rs argues against. mara chose this on #4268 (c95293); narrowing it
is a possible low-priority follow-up, not planned. A test pins that the
grant reads every key, writes none, and reaches no other bucket.
Contributes to #3865.
An agent's CPU and memory caps, set from the swarm. Keyed by the agent
alone, in a new `agent-limits` KV bucket that swarm-controller alone
writes, the same shape as `agent-inference` (#5034): mara on #4268,
"agents are not tied to a hive. dont tie the limits to the hive."
PUT {"cpu_quota": "400%", "memory_max": "8G"} every field optional,
replaces the stored value
GET the stored caps, {} when there are none
DELETE 204
PUT refuses an agent no hive's wanted state declares (as the inference
route does), unknown fields (422), zero and anything systemd does not take,
before the queue is asked, and stores memory in the spelling systemd takes
(8gb -> 8G). The hive hands these values to systemd verbatim.
The routes sit under /api/, behind the swarm UI vhost's authelia
subrequest like every sibling; the core-toggle module-eval case pins that
gate.
The bucket's reader on the hive comes in the next commit.
Contributes to #3865.
mara's ruling: "swarm level overrides replace the hive ui limits. otherwise
dont change the logic."
swarm value present -> it is the value; nothing hive-side is consulted
swarm value absent -> exactly today's chain: per-agent field, else hive
Per field, so a swarm memory ceiling leaves CPU on the hive's chain.
The fallback chain itself is not touched: effective_from_with_swarm calls
effective_from as it was and lets the swarm replace its result, and
effective_with_swarm delegates to effective, so there stays one place the
chain lives. `an_empty_swarm_map_leaves_the_existing_chain_bit_for_bit`
asserts the wrapper is a no-op across every shape the chain distinguishes.
The swarm's caps live in their own swarm-resource-limits.json, not in the
operator's resource-limits.json: different writers, different lifetimes,
and a swarm write into the operator's file would make "reset to hive
defaults" destroy something the operator never set. It is read and written
through the same read_map / write_map, so an unreadable file is an error,
and write_dropins resolves both files through limits_to_apply, whose
keep-the-last-applied-drop-in policy covers the swarm file too.
stage_limits hands limits_to_apply the swarm caps file too, so the
agent's memory-max-bytes copy, which it sizes its heap and subagents
against, is the MemoryMax= systemd enforces: the swarm cap when there is
one.
Nothing writes swarm-resource-limits.json yet; the reader comes with the
swarm's agent-limits bucket later in this series.
Two read-only routes feed them: GET /api/pulls/repos lists the repos with an
open pull request, and GET /api/pulls?repo=owner/name lists a repo's open
pull requests, highest number first. The page offers the lists through the
browser's own suggestion list and keeps both fields free text, since a run
also works on a merged or closed PR.
Follow-ups from the review of #5142.
A `Done` answer with fewer or more rows than the classifications sent, or
none, now posts the no-verdict comment instead of the classifier's overall
verdict, so a short answer can't approve.
Requests are accepted per repo, PR and head into a set bounded at the two
running plus eight waiting. A request for a key already running or waiting
starts nothing and logs at info; a request with every place taken starts no
run and posts a no-verdict comment saying so. Before, every delivery spawned
a task waiting on the semaphore, so toggling the review request queued one
paid run per toggle.
Table cells get a zero-width space after `#`, `!`, `://` and `www.`, so a
model's reason can't render an issue or PR reference or a bare URL as a link.
The secret publisher's policy denies the classifier's forge token leaf, the
way #5139 denied Grafana's admin password, with a module-eval-bao-grants arm
pinning it. The controller posts reviews with whatever token is there, so a
publisher write picked the account reviews come from.
Closes#5143, part of #5096.
The docs tell the operator to write
`secret/swarm/services/review-classifier/anthropic-api-key` by hand in
the bao UI, but the UI's OIDC login for authelia's `admins` group lands
on `swarm-operator-viewer`, which is list+read on `secret/metadata/*`
and nothing under `secret/data/`. bao refuses the write, so an admin
can't set the key and every classifier run fails with "no API key".
The viewer policy gains `create` and `update` on that one leaf, no
`read`: a UI session still never sees a secret value. The leaf is a
named binding, shared with the classifier's reader policy.
module-eval pins the viewer's two stanzas and that no `data/` grant
carries `read`. docs/swarm/secrets.md and services.md describe the
write.
Closes#5148
Requesting the forge account `review-classifier` as a reviewer now runs the
repo's rules against the PR and posts the result as that account's review,
on the head commit the run read. A push starts no run; only a fresh review
request does.
- An instance-wide `pull_request_review_request` system hook delivers to
`/webhook/forge/review-request`. A request for any other reviewer, or a
removed request, does nothing.
- The controller reads the PR, loads the rules with the same reader as
`GET /api/review-classifier/rules`, sends one request on the try-out
subject, and posts APPROVED or REQUEST_CHANGES with the table as the body.
- With no clean verdict (PR or rules don't load, the run fails, or any
classification has no answer) it posts a COMMENT saying why, never an
approval. The event is one function, `no_clean_verdict_event`.
- Table cells are flattened, markup-escaped, and every `@` gets a
zero-width space, so a model's reason can't break the table or mention
anyone.
- At most two requested reviews run at once; the rest wait in memory.
- The account is minted the way hive accounts are: a five-minute pass
queues a `MintClassifierForgeToken` node when the token at
`swarm/services/review-classifier/forge/token` is missing or stale. The
token is `write:repository`. The controller's bao policy gains that leaf
(create/read/update), pinned in module-eval bao-grants. The classifier
never sees the token.
The account gets no repo access automatically; a repo admin adds it by
hand. A re-request retires the account's earlier review: Forgejo clears
`official` on a reviewer's earlier reviews when it is requested again
(models/issues/review.go AddReviewRequest, v16.0.5), and approvals and
rejection blocks count official reviews only.
Refs #5137
A plain kv delete keeps the path's metadata, so the unit's create-only
write (-cas=0) is refused and the old password stays in force. Say
metadata delete, name who can run it, and point at the xkey section.
The table is re-aligned for the wider cell.
docs/swarm/sso.md still said Grafana ships admin/admin and that a dead
provider locks out the built-in admin. The password is minted into the
store and the API still takes basic auth for it, so the paragraph now says
where an operator reads it in an emergency. secrets.md says how to rotate
the password and to deploy the store's host first, and no longer calls SSO
the only way in.
The secret publisher's `services/*` write covered the admin-password
leaf, so a file-copier could set the password Grafana applies at its next
start. The leaf gets the same exact-path deny as the callout seeds, with
an arm in module-eval-bao-grants.
Grafana's built-in admin kept the nixpkgs default password `admin`, and
Grafana's API takes basic auth whatever `auth.disable_login_form` says, so
the public vhost gave server-admin to `admin:admin`.
`swarm-bao-grafana-admin` creates a random password in the store under
Grafana's existing leaf when none is stored (`create` without `update`, so
the first one is kept), then copies it into the container.
`security.admin_password` reads it for a new database, and a `grafana cli
admin reset-admin-password` in `grafana.service`'s `preStart` applies it to
an existing one on every start.
Closes#5138
A button beside the repo field reads the repo's rules from its default branch into the classification rows, names the file and the commit it read, asks before replacing rows the operator wrote, and shows the route's error without touching the rows.
The rules file is now read with the existing forgejo-api client: the
commit's root tree, then the `.hyperhive` tree, then the
`review-classifier.{yml,json}` blob, base64-decoded. Every request takes
single-segment path parameters, so nothing depends on how a nested path
is escaped, and the controller keeps one HTTP transport to the forge.
The second reqwest client, `Client::with_token` and `raw_file_url` are
gone.
A tree the forge truncated is an error, not a missing file. A 404 on a
tree or blob doesn't come back as a forge 404, so only the repo lookup's
404 reads as "no such repo".
The `is_repo_not_found` doc comment is back on its function.
Part of #5096.
forgejo-api escapes the whole file path of a raw-file request, so the
rules file went out as `raw/.hyperhive%2Freview-classifier.yml` and
reached the forge only if every proxy and router in between decoded the
`%2F`. The controller now sends that one request itself: `raw_file_url`
escapes each path segment on its own and joins them with `/`, and
`forge::Client` holds a second HTTP client with the same token for it.
A 404 from the default-branch lookup now means "no commit on the
default branch", like an empty repo, instead of falling through to the
route's "no repo" answer. Only the repo lookup's 404 still means the
repo doesn't exist.
Tests cover the URL shape, the reads against a loopback stub forge
that serves only the literal path, the branch 404, 503 with no forge
access, 503 at the forge deadline on a paused clock, and a bad
polarity value.
Part of #5096.
`GET /api/review-classifier/rules?repo=owner/name` reads
`.hyperhive/review-classifier.yml` or `.json` from the repo's default
branch with the controller's forge token, on every request, and answers
the classifications in the try-out POST's shape, plus the file, the
branch and the commit it was read at. The swarm UI's try-out page loads
them into its editor (#5134).
The file is one `classifications` list of the try-out's `Classification`.
A missing file, both files, a parse error, any unknown key, an empty list
or more than ten entries is a problem+json error, never a pass. The
checks the POST makes on its classifications move into one function both
paths call.
The doc names the file and its schema, replacing the placeholders, and
documents the route.
Part of #5096.
The repo check takes the endpoint's own character set and refuses . and .. parts. The running and expired messages say a run can last up to half an hour and that a controller restart drops the result.
Two classifications sharing a name made the result table's row key collide, so the form now refuses duplicate names and the table keys rows by position anyway. The page said it stores nothing in the browser, but the result table remembers its column filters like every table does; the header comment and the docs now say that. The row cap's comment says to keep it in step with the endpoint's own cap.
A page to try a review classification against a real PR: repo, PR number and one or more classifications (name, description, polarity, threshold). Run posts to the controller's try-out endpoint and polls until the run is done or failed, saying it is running meanwhile, then shows the review's table, the overall verdict and the head commit. A row that could not run shows its error in place of a reason, and a failed run shows its error. The inputs stay so the run can be edited and repeated. Nothing is stored and nothing is posted to the forge. Route, nav entry and a section in docs/swarm/ui.md.
deploy.review-classifier.enable now defaults to allSwarmServices, as the
other shared services do, but only where the host has the classifier's
store identity (deploy.bao.reviewClassifierClient{Cert,Key}File). The
classifier module asserts that pair; glue-bao-tls.nix mints it only on the
host that runs the store, so a services host with the store placed
elsewhere would otherwise fail eval. An explicit value still wins.
The operator still writes the Anthropic key by hand; until then each run
fails with "no API key…" while the service runs.
The routes' annotations now list every 400 reason (including the JSON
shape rejections), the 503 causes, the 31-minute running window, the
404 after a controller restart, and that an errored row reads
not-applicable with certainty 0 and an empty reason, so the page keys
off error, and that overall can be approve beside errored rows.
hive-claude adds its variables on top of the parent's environment, so
claude inherited the daemon's store identity paths, CREDENTIALS_DIRECTORY
and queue coordinates. claude now starts as this same binary with
SWARM_REVIEW_CLASSIFIER_EXEC_CLAUDE naming the real one; main execs it
before the runtime starts, with the environment cleared down to the API
key, PATH, HOME, CLAUDE_CONFIG_DIR and claude's telemetry settings. The
exec keeps the pid, so hive-claude's cancel and idle timeout still reach
it. An integration test runs the built binary as the trampoline and
checks that a BAO_* variable does not reach the program it execs.
The leaf's key was copied by a host unit into the container's root on
host disk. nspawn now reads the cert, key and store CA as root at
container start (--load-credential) and passes them in as system
credentials; the daemon's unit loads them by bare id, as an agent's
container does with its own leaf (nix/agent-modules/bao.nix). The copy
unit is gone. Where this host mints the leaf, the container starts after
swarm-bao-pki, since nspawn refuses a missing credential source. An
assertion refuses identity paths with whitespace, which nixos-container's
unquoted extra flags would split.
Pins that deploy.review-classifier.enable is not derived from
allSwarmServices: the same fixture turns the queue on and leaves the
classifier and its container absent.
- review-classifier.md: claude runs spawn through hive-claude directly
with no tools and no session (hive-runtime holds only TurnStop); the
answer's fields, the 500-character reason cap and what a failure does;
how the try-out page reaches the classifier; the settled parts of the
open list (API key path, try-out subject, --tools "" on claude).
The banner stays.
- services.md: a Review classifier section with the two operator steps.
- secrets.md, credentials.md: the classifier's leaf and the key row.
- network.md: the container's row. CLAUDE.md: the crate.
deploy.review-classifier.enable (off by default, not derived from
allSwarmServices) puts swarm-review-classifier in its own network
namespace on the bridge, with claude-code and the daemon. A host unit
copies the classifier's store leaf into the container; the daemon reads
its queue client secret and the API key from the store with it.
- glue-bao-tls mints the review-classifier leaf; swarm-bao writes the
swarm-review-classifier role, reading exactly the API key path and the
classifier's queue client secret; swarm.nix reserves its CN.
- glue-review-classifier-oidc-client registers the machine client where
authelia runs, and the publisher carries its secret to the store.
- swarm-nats passes the client id to the responder as
--review-classifier-client.
- Where the host runs a hive-tier collector, claude exports its metrics
there; the daemon adds the repo and the PR to the resource attributes.
- module-eval: the new grant and leaf (bao-grants, now sixteen granting
units), the container's presence and its unit (core-toggle), and the
client registration and publishing (secret-publisher).
POST /api/review-classifier/try-out validates the run (owner/name repo,
a PR number, one to ten classifications with distinct names, thresholds
up to 100), reads the PR's head commit, commits and diff with the
controller's forge token, sends one request to the review classifier on
the queue and answers 202 with an id. GET .../try-out/{id} reports the
run as running, done (rows and overall verdict) or failed (the error).
Results live in memory for an hour after the reply; there is no run
store. POST then GET because a run can outlast the gateway's 60-second
proxy read timeout. Both routes are utoipa-annotated, so they show on
/api/docs/.
A new service crate. It reads its queue client secret from the store at
start, takes try-outs on the review classifier subject in its queue
group, and for each run reads the Anthropic API key from the store,
builds the shared context (commits with full messages, diff with
lockfiles collapsed), runs one claude --print per classification with no
session, no tools, no MCP server and the key in the child's environment
only, validates the JSON answer and aggregates the rows. A missing key or
a run where no classification got an answer fails the run; one failed
classification is an excluded row with its error. One journal line per
run names the operator, repo, PR, head SHA, rows and overall verdict.
swarm-secret-client gains the two paths the service reads.
swarm-queue-client gains review_classifier: the try-out subject, the
request and reply that travel on it, and the deadlines both ends share.
The controller's reader grant may publish the subject; a client named
with --review-classifier-client may subscribe to it and its inbox, and
answers under NATS allow_responses (resp in the user JWT) within the
run deadline. Every other client id is still denied.