Compare commits
50 changed files with 97 additions and 3188 deletions
|
|
@ -187,12 +187,6 @@ hand-maintained per-file tree drifts out of sync with the code.
|
|||
defaults look for `VAULT_ADDR`/`VAULT_CLIENT_CERT`/`VAULT_CLIENT_KEY`, which
|
||||
no unit in this tree sets, so falling through to them yields a client with
|
||||
**no identity** and a TLS handshake failure that names no cause.
|
||||
- **`swarm-review-classifier/`** — the review classifier's try-out daemon,
|
||||
in its own container where `deploy.review-classifier.enable` is set. Takes
|
||||
a PR and classifications from `swarm-controller` over the queue, runs one
|
||||
`claude --print` per classification (no tools, no session) and replies
|
||||
with the rows and the overall verdict. Holds no forge credential. Design:
|
||||
[`docs/swarm/review-classifier.md`](docs/swarm/review-classifier.md).
|
||||
- **`swarmctl/`** — swarm-level operator CLI, installed by the
|
||||
swarm-controller module on the host that runs the daemon. Runs as
|
||||
**root and acts directly** — it serves no socket, publishes no HTTP
|
||||
|
|
|
|||
19
Cargo.lock
generated
19
Cargo.lock
generated
|
|
@ -4850,7 +4850,6 @@ dependencies = [
|
|||
"swarm-queue-client",
|
||||
"swarm-secret-client",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tracing",
|
||||
"url",
|
||||
"utoipa",
|
||||
|
|
@ -4929,24 +4928,6 @@ dependencies = [
|
|||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swarm-review-classifier"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
"futures-util",
|
||||
"hive-claude",
|
||||
"hive-log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"swarm-queue-client",
|
||||
"swarm-secret-client",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ members = [
|
|||
"swarm-matrix-ctl",
|
||||
"swarm-nats-auth",
|
||||
"swarm-queue-client",
|
||||
"swarm-review-classifier",
|
||||
"swarm-logs",
|
||||
"swarm-secret-client",
|
||||
"swarmctl",
|
||||
|
|
|
|||
|
|
@ -43,12 +43,11 @@ that touches it.
|
|||
```
|
||||
|
||||
| container | netns | IPv4 | listens / reached via |
|
||||
| ------------------------- | ----------------------- | -------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| ----------------- | ----------------------- | -------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| gateway (host) | host | host addresses | nginx `:80`/`:443` (every vhost); dnsmasq `bridgeIp:53` + DHCP `:67` on the bridge |
|
||||
| `hive-forge` | host (shared) | host addresses | forgejo `:3000` http, `:2222` git-ssh; fronted by the `forge.<swarm-domain>` vhost |
|
||||
| `hive-matrix` | host (shared) | host addresses | tuwunel `:8008` (+ optional federation port); fronted by the matrix vhost |
|
||||
| `swarm-ci-runner` | private, veth on bridge | DHCP pool | outbound only (runner → forge); no inbound surface |
|
||||
| `swarm-review-classifier` | private, veth on bridge | DHCP pool | outbound only (classifier → queue, secret store, hive collector); no inbound surface |
|
||||
| `h-<agent>` | private, veth on bridge | DHCP pool | web UI via UDS `/run/hive-agent/<name>` → nginx sub-path; in-container UI port hashed 8100–8999 |
|
||||
|
||||
The flows, end to end:
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ of the cell says how.
|
|||
<!-- vale write-good.Passive = NO -->
|
||||
|
||||
| store path | minter | reader — pulls at runtime, holds in memory | automatic re-mint | automatic re-pull |
|
||||
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `swarm/agents/<agent>/matrix/main` | `swarm-controller`, with the swarm's appservice token, at agent creation and in a five-minute pass | the agent container itself, under the certificate its hive passed in | ✅ the pass re-mints when the stored token is missing, unknown to the homeserver, or someone else's | ✅ `hive-matrix-daemon` exits when the homeserver rejects its token, and a five-minute timer restarts it, which reads the store again |
|
||||
| `swarm/agents/<agent>/matrix/<account>` | `swarm-controller` | the agent container itself, under the certificate its hive passed in | must be stated | must be stated |
|
||||
| `swarm/controller/swarm-controller/matrix/appservice-token` | `swarm-matrix-ctl`, inside the `hive-matrix` container, once | `swarm-controller`, under its own certificate | ❌ `swarm-matrix-ctl` mints it once; the container keeps its copy and republishes it when the store's differs | ✅ the controller reads it on every five-minute matrix pass |
|
||||
|
|
@ -71,9 +71,8 @@ of the cell says how.
|
|||
| `swarm/hives/<hive>/forge/token` | `swarm-controller`, with the forge's admin API, for every hive in its directory in a five-minute pass | `swarm-controller` under its own certificate, before it decides whether to mint, and hive-c0re's `forge::reader_token()`, under the hive's own certificate | ✅ the controller's pass re-mints when the stored token is missing or no longer matches the forge (last eight characters and scopes) | ✅ hive-c0re reads the store on every forge read |
|
||||
| `swarm/hives/<hive>/queue/agent` | authelia | `swarm-bao-queue-agent` on the hive's host, under its own per-hive certificate; no agent's policy reaches it | must be stated | must be stated |
|
||||
| `swarm/hives/<hive>/oidc/client` | authelia, at its first boot; `swarm-secret-publish` copies it in | hive-c0re, under the hive's own certificate, when it first connects to the queue | ❌ authelia mints it once. A re-mint is republished by `swarm-secret-publish`'s path unit | ❌ read until the first connect succeeds, retried with a backoff of up to five minutes while it's missing; then held until hive-c0re restarts |
|
||||
| `swarm/services/<clientId>/oidc/client` | authelia | that service's own reader, under its own certificate — the queue responder's under `swarm-nats-auth`'s, the homeserver's under `swarm-matrix-ctl`'s, the forge's under `swarm-forge-publisher`'s, the review classifier's under `swarm-review-classifier`'s; the bao UI's by the store's granter | must be stated | must be stated |
|
||||
| `swarm/services/<clientId>/oidc/client` | authelia | that service's own reader, under its own certificate — the queue responder's under `swarm-nats-auth`'s, the homeserver's under `swarm-matrix-ctl`'s, the forge's under `swarm-forge-publisher`'s; the bao UI's by the store's granter | must be stated | must be stated |
|
||||
| `swarm/services/ci-runner/registration-token` | `swarm-controller`, from the forge's admin API, at start and in a five-minute pass; its grant on the path is write-only | `swarm-bao-ci-runner` on the runner's host, under the runner's own certificate, into a file only `swarm-ci-runner-register` in the runner's container loads; no other principal's policy reads it | ❌ the forge hands out one token until an admin resets it; the controller writes a reset token within five minutes | ❌ `swarm-bao-ci-runner` reads it when it starts, at boot or deploy, and restarts the runner when the value changed |
|
||||
| `swarm/services/review-classifier/anthropic-api-key` | the operator, by hand in the store's UI, field `api_key` | `swarm-review-classifier` in its container, under its own certificate, at the start of every run; held for that run and handed to `claude`'s environment only; no other principal's policy reads it | ❌ an operator's key; replaced by writing a new one | ✅ read on every run, so a written or removed key takes effect on the next run |
|
||||
| _(not in the store)_ a hive's mTLS leaf | the store's own PKI, or an operator placing it by hand | its own client, off disk — the exception above, because it's what makes every other row's pull possible | must be stated | must be stated |
|
||||
|
||||
<!-- vale write-good.Passive = YES -->
|
||||
|
|
|
|||
|
|
@ -98,16 +98,14 @@ It looks like an agent from the inside, with three things missing: it has no
|
|||
full agent identity, no state directory and no tools. Every model session is
|
||||
ephemeral, and nothing carries over from one run to the next.
|
||||
|
||||
On the claude runtimes, each classification is one `claude --print` the
|
||||
service spawns through the `hive-claude` crate directly, with no session to
|
||||
resume, no tools (`--tools ""`), no MCP server (`--strict-mcp-config` and no
|
||||
config) and an empty directory of its own. A classification needs no durable
|
||||
session, so the service doesn't use [`hive-runtime`](../../hive-runtime/README.md),
|
||||
whose `Runtime` trait drives the agents' durable sessions.
|
||||
|
||||
The service is `swarm-review-classifier`, and its container is the one
|
||||
[`services.md`](services.md#review-classifier) describes. It holds no forge
|
||||
credential: `swarm-controller` reads the PR and sends it along.
|
||||
The model sessions run through [`hive-runtime`](../../hive-runtime/README.md),
|
||||
the crate `hive-agent` and `hive-subagent-mcp` already drive their turns
|
||||
through: the `Runtime` trait in `hive-runtime/src/lib.rs`, with
|
||||
`ClaudeRuntime` and `AcpRuntime` behind it. That trait runs turns on a durable
|
||||
session, and the subagent daemon's claude path
|
||||
(`hive-subagent-mcp/src/session.rs`) spawns claude directly. A refactor
|
||||
first moves the cancellable per-turn claude session into `hive-runtime` and
|
||||
moves the subagent daemon onto it; the classifier builds on that shared seam.
|
||||
|
||||
### Swarm-level settings
|
||||
|
||||
|
|
@ -149,9 +147,9 @@ The model answers each classification in JSON. The answer holds:
|
|||
|
||||
- a **verdict**, one of _match_, _no match_ or _not applicable to this PR_
|
||||
- a **certainty** from 0 to 100, how sure the model is of that verdict
|
||||
- a **reason**, at most 500 characters
|
||||
- a **reason**, limited in length
|
||||
|
||||
An answer:
|
||||
An illustrative answer, with placeholder field names:
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -161,13 +159,6 @@ An answer:
|
|||
}
|
||||
```
|
||||
|
||||
The verdict field holds `match`, `no-match` or `not-applicable`, and the
|
||||
certainty field a whole number. A runtime error, a timeout, or an answer that
|
||||
isn't that JSON object leaves the classification without a verdict: it's listed
|
||||
with its error, and doesn't count. A run with no API key, or in which no
|
||||
classification got an answer, fails as a whole, and shows its error instead of a
|
||||
verdict.
|
||||
|
||||
### Aggregation into the verdict
|
||||
|
||||
The classifier collects the answers into one result, keyed by
|
||||
|
|
@ -233,14 +224,6 @@ repo and a PR, write or edit a classification, and run it against that PR;
|
|||
change the prompt and run it again until it answers the way you want.
|
||||
Nothing reaches the forge while you iterate.
|
||||
|
||||
The page posts the run to `swarm-controller` (`POST
|
||||
/api/review-classifier/try-out`), which reads the PR with its own forge token
|
||||
and sends it to the classifier as one queue request on
|
||||
`$SWARM.review-classifier.try-out`. The page then reads the result back (`GET
|
||||
/api/review-classifier/try-out/{id}`): a run can take longer than a request
|
||||
stays open. The controller keeps the result in memory for an hour; there is no
|
||||
run store.
|
||||
|
||||
When it's ready, **open a PR** has the service account push a branch that
|
||||
adds the classification to the repo's config, with you, the swarm UI user,
|
||||
as commit author. The page then links you to the forge's new-PR page for
|
||||
|
|
@ -251,8 +234,7 @@ that branch, where you log in as usual and open the PR yourself.
|
|||
The swarm UI shows no token counts, cache counts or cost, on any page. On the
|
||||
claude runtimes, every classifier run starts its `claude` process with
|
||||
`OTEL_RESOURCE_ATTRIBUTES` naming the repo and the PR, so the cost reads from
|
||||
the metrics store by repo and PR. The process exports to the hive-tier collector on
|
||||
the classifier's host, where that host runs one. On a subscription, cost is notional.
|
||||
the metrics store by repo and PR. On a subscription, cost is notional.
|
||||
|
||||
On the ACP runtime, the harness counts the agent's `usage_update` running
|
||||
totals and exports them as `hyperhive.agent.cost.usage`, labelled with only
|
||||
|
|
@ -273,8 +255,13 @@ These get settled, and filled in here, as the work reaches them:
|
|||
token counts before choosing.
|
||||
- **Run store and retention.** Where run records live, how long they stay,
|
||||
and how the recent-runs page reads them.
|
||||
- **Failure handling.** What a runtime error, malformed JSON, a timeout or a
|
||||
rate limit does to a classification and to the verdict.
|
||||
- **JSON validation and the reason cap.** The exact answer schema, and the
|
||||
reason's length limit.
|
||||
- **Oversized diffs.** What happens when the diff doesn't fit the model's
|
||||
context. Lockfiles already collapse to one line each, as in `hive-forge diff`.
|
||||
context, and whether lockfiles collapse as in `hive-forge diff`
|
||||
(`hive-forge/src/verbs/diff.rs`).
|
||||
- **Stale reviews.** Whether the repo's stale-review dismissal setting
|
||||
retires the bot's review on an older commit, or the service dismisses its
|
||||
own earlier review when it posts a new one.
|
||||
|
|
@ -285,19 +272,16 @@ These get settled, and filled in here, as the work reaches them:
|
|||
whether anything gets redacted.
|
||||
- **Prompt injection.** A PR's diff and commit messages are model input, so
|
||||
a PR can try to talk the classifier into approving it.
|
||||
- **The "no tools" mechanism on ACP.** How the ACP runtime runs a session with
|
||||
no tools at all. The claude runtimes pass `--tools ""`.
|
||||
- **Credential paths.** Where in bao the ACP and subscription runtimes'
|
||||
credentials and the service account's forge token live, and which component
|
||||
holds the token. The API key is at
|
||||
`secret/swarm/services/review-classifier/anthropic-api-key`, field `api_key`.
|
||||
- **The "no tools" mechanism.** How each runtime runs a session with no
|
||||
tools at all.
|
||||
- **Credential paths.** Where in bao the runtime credentials and the service
|
||||
account's forge token live, and which component holds the token.
|
||||
- **Model naming across runtimes.** How the swarm-level model setting
|
||||
handles runtime-specific model names.
|
||||
- **ACP cost labels.** How an ACP run's cost gets a repo and PR label, so
|
||||
it reads from the metrics store like the claude runtimes' cost.
|
||||
- **Async message medium for review runs.** Which queue subjects or streams
|
||||
carry a webhook-started run. A try-out is one request on
|
||||
`$SWARM.review-classifier.try-out`, answered on its reply subject.
|
||||
- **Async message medium.** Which queue subjects or streams carry the
|
||||
messages between `swarm-controller` and the classifier.
|
||||
- **Reviewer handoff.** How a later reviewer, human or agent, learns the
|
||||
classifier passed a PR before it starts its own review.
|
||||
- **Opt-in details.** Beyond the config file: the service account's access
|
||||
|
|
@ -305,3 +289,5 @@ These get settled, and filled in here, as the work reaches them:
|
|||
branches.
|
||||
- **Commit author for try-out PRs.** How the classifier derives the commit
|
||||
author's name and email from the swarm UI login.
|
||||
- **Refactor scope.** How much of `hive-agent`'s and `hive-subagent-mcp`'s
|
||||
runtime code moves into `hive-runtime` before the classifier uses it.
|
||||
|
|
|
|||
|
|
@ -390,46 +390,43 @@ next controller restart.
|
|||
### Per-principal identities
|
||||
|
||||
Bao matches a cert-auth role on the certificate's subject, so a certificate is an
|
||||
identity and sharing one merges the identities. Each of these seven leaves has
|
||||
a role and a policy of its own, and reaches one path, except the review
|
||||
classifier's and the forge host's, which reach two:
|
||||
identity and sharing one merges the identities. Each of these six leaves has a
|
||||
role and a policy of its own, and reaches one path, except the forge host's,
|
||||
which reaches two:
|
||||
|
||||
| unit | option pair under `deploy.bao.` | reads, or writes |
|
||||
| --------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
|
||||
| --------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| `swarm-bao-queue-agent` | `queueAgentClientCertFile` / `queueAgentClientKeyFile` | `swarm/hives/<hive>/queue/agent` |
|
||||
| `swarm-bao-grafana-oidc` | `grafanaOidcClientCertFile` / `grafanaOidcClientKeyFile` | `swarm/services/<grafana client id>/oidc/client` |
|
||||
| `swarm-bao-otel-oidc` | `otelOidcClientCertFile` / `otelOidcClientKeyFile` | `swarm/services/<collector client id>/oidc/client` |
|
||||
| `swarm-bao-forwarder-oidc` | `forwarderOidcClientCertFile` / `forwarderOidcClientKeyFile` | `swarm/services/<store forwarder client id>/oidc/client` |
|
||||
| `swarm-bao-ci-runner` | `ciRunnerClientCertFile` / `ciRunnerClientKeyFile` | `swarm/services/ci-runner/registration-token` |
|
||||
| `swarm-review-classifier`, in its container | `reviewClassifierClientCertFile` / `reviewClassifierClientKeyFile` | `swarm/services/review-classifier/anthropic-api-key` and `swarm/services/<classifier client id>/oidc/client` |
|
||||
| `swarm-bao-forge-publisher`, `swarm-bao-forge-oidc` | `forgePublisherClientCertFile` / `forgePublisherClientKeyFile` | writes `swarm/controller/swarm-controller/forge/token`; reads `swarm/services/<forge client id>/oidc/client` |
|
||||
|
||||
The first exists **per hive**, because the path it reads carries a hive name
|
||||
and every hive runs its own reader. Its subject is
|
||||
`<deploy.bao.queueAgentCommonNamePrefix>-<hive>`; `swarm.nix` reserves that
|
||||
composed spelling as a hive name, so nobody can name a hive into another hive's
|
||||
role. The other six reach a path that names a swarm service rather than a hive,
|
||||
role. The other five reach a path that names a swarm service rather than a hive,
|
||||
so one role each is enough and their subjects are the flat
|
||||
`deploy.bao.grafanaOidcCommonName`, `deploy.bao.otelOidcCommonName`,
|
||||
`deploy.bao.forwarderOidcCommonName`, `deploy.bao.ciRunnerCommonName`,
|
||||
`deploy.bao.reviewClassifierCommonName` and
|
||||
`deploy.bao.forwarderOidcCommonName`, `deploy.bao.ciRunnerCommonName` and
|
||||
`deploy.bao.forgePublisherCommonName`. The last is the only writer, and the one
|
||||
leaf two units share: the forge's host holds one store identity for the token it
|
||||
publishes and the OIDC client secret it reads.
|
||||
|
||||
On a host that mints its own PKI, `glue-bao-tls.nix` signs all seven and defaults
|
||||
all fourteen options, and there is nothing to do. Elsewhere you issue each leaf from
|
||||
On a host that mints its own PKI, `glue-bao-tls.nix` signs all six and defaults
|
||||
all twelve options, and there is nothing to do. Elsewhere you issue each leaf from
|
||||
that CA out of band and name it here — one file per principal rather than one file
|
||||
shared by seven, which is the whole of what this buys. `swarm-ci-runner.nix`
|
||||
shared by six, which is the whole of what this buys. `swarm-ci-runner.nix`
|
||||
refuses the build wherever the runner runs without its pair, as
|
||||
`swarm-review-classifier.nix` does for the classifier and `swarm-grafana.nix`
|
||||
for Grafana. The forge's pair refuses nothing: without
|
||||
`swarm-grafana.nix` does for Grafana. The forge's pair refuses nothing: without
|
||||
it `swarm-bao-forge-publisher` and `swarm-bao-forge-oidc` render, fail at start
|
||||
naming both options, and the controller runs without forge access and the forge
|
||||
without a login source until an operator places the leaf.
|
||||
|
||||
On a host without the store, where the forwarder never runs, forgetting one of
|
||||
the other seven isn't a quiet degrade. A hand-configured remote-store hive that
|
||||
the other six isn't a quiet degrade. A hand-configured remote-store hive that
|
||||
names the hive's own `clientCertFile` and misses a principal's pair has the
|
||||
build refused, by the module of the reader it missed, naming that pair — see
|
||||
[what a missing leaf costs](#what-a-missing-leaf-costs) below.
|
||||
|
|
|
|||
|
|
@ -232,25 +232,3 @@ publishes to the secret store, which it reads under its own `ci-runner` leaf.
|
|||
On a host without the store, that leaf is the one file to copy. Registration,
|
||||
the container and the threat model are in
|
||||
[`../scheduler/ci.md`](../scheduler/ci.md).
|
||||
|
||||
### Review classifier
|
||||
|
||||
The review classifier's try-out service, in a `swarm-review-classifier`
|
||||
container on the host that sets `deploy.review-classifier.enable`.
|
||||
`allSwarmServices` defaults it on wherever the host has the classifier's store
|
||||
identity: the host that runs the store has it, and a services host with the
|
||||
store elsewhere gets the classifier only once it holds a copy of the pair (see
|
||||
below). Set `deploy.review-classifier.enable` directly to override either way.
|
||||
|
||||
The operator writes the Anthropic key by hand, in the secret store's UI at
|
||||
`secret/swarm/services/review-classifier/anthropic-api-key`, field `api_key`.
|
||||
Nothing writes it for you: until it exists the service runs and every run fails
|
||||
with `no API key…`. The service reads the key on every run, so writing or
|
||||
removing it takes effect on the next run, with no restart. Every run spends
|
||||
money on that key.
|
||||
|
||||
It needs neither the forge nor a hive on its host. It reaches the queue and the
|
||||
secret store by name, under its own `review-classifier` leaf. On a host
|
||||
without the store, that leaf is the one file to copy, as for the CI runner.
|
||||
The swarm UI's try-out page drives it through swarm-controller; what a run is
|
||||
and what it costs are in [`review-classifier.md`](review-classifier.md).
|
||||
|
|
|
|||
|
|
@ -160,9 +160,6 @@
|
|||
services.hyperhive.deploy.matrix.ctlPackage =
|
||||
lib.mkDefault
|
||||
self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-matrix-ctl;
|
||||
services.hyperhive.deploy.review-classifier.package =
|
||||
lib.mkDefault
|
||||
self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-review-classifier;
|
||||
services.hyperhive.deploy.authelia.bridgePackage =
|
||||
lib.mkDefault
|
||||
self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-authelia-bridge;
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@
|
|||
./glue-nats-bao-identity.nix
|
||||
./glue-nats-oidc-client.nix
|
||||
./glue-queue-agent-credential.nix
|
||||
./glue-review-classifier-oidc-client.nix
|
||||
./glue-secret-publisher-bao-identity.nix
|
||||
./glue-swarm-bao-otel-oidc-client.nix
|
||||
./glue-swarm-otel-oidc-client.nix
|
||||
|
|
@ -48,7 +47,6 @@
|
|||
./swarm-bao-service.nix
|
||||
./swarm-bao.nix
|
||||
./swarm-ci-runner.nix
|
||||
./swarm-review-classifier.nix
|
||||
./swarm-secret-publisher.nix
|
||||
./swarm-nats-service.nix
|
||||
./swarm-nats.nix
|
||||
|
|
|
|||
|
|
@ -642,27 +642,6 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
review-classifier.enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
example = true;
|
||||
description = ''
|
||||
Run the review classifier in a `swarm-review-classifier` container on
|
||||
this host. It answers the swarm UI's review try-outs with `claude` on
|
||||
the Anthropic API key the operator writes to the secret store at
|
||||
`secret/swarm/services/review-classifier/anthropic-api-key`, field
|
||||
`api_key`.
|
||||
|
||||
On by default where
|
||||
{option}`services.hyperhive.deploy.allSwarmServices` is set and this
|
||||
host has the classifier's store identity (the pair
|
||||
{option}`services.hyperhive.deploy.bao.reviewClassifierClientCertFile`
|
||||
and `reviewClassifierClientKeyFile`, minted on the store's host). The
|
||||
key is never written for you: until the operator writes it the service
|
||||
runs and every run fails with "no API key…".
|
||||
'';
|
||||
};
|
||||
|
||||
hive-controller.enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
|
|
|
|||
|
|
@ -107,8 +107,6 @@ in
|
|||
forwarderOidcClientKeyFile = lib.mkDefault "${pkiDir}/forwarder-oidc-key.pem";
|
||||
ciRunnerClientCertFile = lib.mkDefault "${pkiDir}/ci-runner.pem";
|
||||
ciRunnerClientKeyFile = lib.mkDefault "${pkiDir}/ci-runner-key.pem";
|
||||
reviewClassifierClientCertFile = lib.mkDefault "${pkiDir}/review-classifier.pem";
|
||||
reviewClassifierClientKeyFile = lib.mkDefault "${pkiDir}/review-classifier-key.pem";
|
||||
forgePublisherClientCertFile = lib.mkDefault "${pkiDir}/forge-publisher.pem";
|
||||
forgePublisherClientKeyFile = lib.mkDefault "${pkiDir}/forge-publisher-key.pem";
|
||||
servicesIssuerClientCertFile = lib.mkDefault "${pkiDir}/services-issuer.pem";
|
||||
|
|
@ -270,13 +268,6 @@ in
|
|||
[ -s ${pkiDir}/ci-runner.pem ] || ${signLeaf} ${pkiDir} ci-runner \
|
||||
${lib.escapeShellArg deployCfg.bao.ciRunnerCommonName} "" clientAuth
|
||||
|
||||
# The review classifier's, which reads its API key and queue client
|
||||
# secret. Minted here for the matrix-ctl leaf's reason: the classifier
|
||||
# is a swarm service, so on a classifier host without the store this is
|
||||
# the file an operator copies.
|
||||
[ -s ${pkiDir}/review-classifier.pem ] || ${signLeaf} ${pkiDir} review-classifier \
|
||||
${lib.escapeShellArg deployCfg.bao.reviewClassifierCommonName} "" clientAuth
|
||||
|
||||
# The forge's token publisher. Minted here for the matrix-ctl leaf's
|
||||
# reason: the forge is a swarm singleton, so on a forge host without the
|
||||
# store this is the file an operator copies.
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
# Glue: register the review classifier as an OIDC client wherever authelia
|
||||
# runs.
|
||||
#
|
||||
# ONE PAIRING PER FILE — swarm-review-classifier ← authelia, and nothing else.
|
||||
# Deleting this leaves a classifier whose token requests authelia answers with
|
||||
# a 401, and no minted secret for swarm-secret-publish to put in the store.
|
||||
#
|
||||
# ⚠️ Gated on authelia being HERE, and NOT on this host running the
|
||||
# classifier, for ./glue-controller-oidc-client.nix's reason: a client is a row
|
||||
# in THIS host's provider config. Registered whether or not any host enables
|
||||
# the classifier: nothing in `swarm.*` records that, so a swarm without one
|
||||
# carries one unused client and one unused minted secret.
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
hyperhiveCfg = config.services.hyperhive;
|
||||
deployCfg = hyperhiveCfg.deploy;
|
||||
clientId = hyperhiveCfg.swarm.review-classifier.clientId;
|
||||
in
|
||||
{
|
||||
config = lib.mkIf deployCfg.authelia.enable {
|
||||
services.hyperhive.swarm.authelia.oidc.clients = [
|
||||
{
|
||||
id = clientId;
|
||||
description = "HyperHive review classifier";
|
||||
# `client_credentials`: a daemon authenticating as itself.
|
||||
kind = "machine";
|
||||
audience = [ clientId ];
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
@ -932,19 +932,6 @@ let
|
|||
}
|
||||
];
|
||||
|
||||
# The review classifier: the API key the operator writes, read once per run,
|
||||
# and its own queue client secret, read at start. Both by the daemon inside
|
||||
# its container.
|
||||
reviewClassifierReaders = [
|
||||
{
|
||||
name = "swarm-review-classifier";
|
||||
cn = baoDeploy.reviewClassifierCommonName;
|
||||
policyText =
|
||||
readStanza "${credentialMountPath}/data/swarm/services/review-classifier/anthropic-api-key"
|
||||
+ readStanza "${credentialMountPath}/data/swarm/services/${hyperhiveCfg.swarm.review-classifier.clientId}/oidc/client";
|
||||
}
|
||||
];
|
||||
|
||||
# The forge host's one store identity. `swarm-bao-forge-publisher` writes
|
||||
# the controller's forge token under it, the one writer of that path and
|
||||
# write-only: it copies a file in and never reads one back.
|
||||
|
|
@ -1767,20 +1754,6 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
reviewClassifierCommonName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "swarm-review-classifier";
|
||||
description = ''
|
||||
Subject the store's `swarm-review-classifier` cert-auth role accepts:
|
||||
the identity the review classifier presents from inside its
|
||||
container. Its grant is two paths, read only:
|
||||
`swarm/services/review-classifier/anthropic-api-key` and its own
|
||||
queue client secret.
|
||||
|
||||
⚠️ Reserved as a hive name by ./swarm.nix, like its siblings.
|
||||
'';
|
||||
};
|
||||
|
||||
forgePublisherCommonName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "swarm-forge-publisher";
|
||||
|
|
@ -2030,32 +2003,6 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
reviewClassifierClientCertFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "/var/lib/swarm-bao-pki/review-classifier.pem";
|
||||
description = ''
|
||||
Certificate the review classifier presents to the store. Its subject
|
||||
must be
|
||||
{option}`services.hyperhive.deploy.bao.reviewClassifierCommonName`.
|
||||
|
||||
./glue-bao-tls.nix points it at the leaf it mints on the store's host.
|
||||
On a classifier host without the store, it is a file the operator
|
||||
copies from the store's host. A path, never a value.
|
||||
'';
|
||||
};
|
||||
|
||||
reviewClassifierClientKeyFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "/var/lib/swarm-bao-pki/review-classifier-key.pem";
|
||||
description = ''
|
||||
Private key for
|
||||
{option}`services.hyperhive.deploy.bao.reviewClassifierClientCertFile`.
|
||||
A path, never a value.
|
||||
'';
|
||||
};
|
||||
|
||||
forgePublisherClientCertFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
|
|
@ -3127,8 +3074,6 @@ in
|
|||
|
||||
systemd.services.swarm-bao-ci-runner-policy = readerPolicyUnit "write the CI runner's registration-token-reader bao policy and cert-auth role" ciRunnerReaders;
|
||||
|
||||
systemd.services.swarm-bao-review-classifier-policy = readerPolicyUnit "write the review classifier's API-key-reader bao policy and cert-auth role" reviewClassifierReaders;
|
||||
|
||||
systemd.services.swarm-bao-forge-publisher-policy = readerPolicyUnit "write the forge's controller-token-publisher bao policy and cert-auth role" forgePublisherWriters;
|
||||
|
||||
# A FOURTH sibling, same shape and same reasons as the two above. This
|
||||
|
|
|
|||
|
|
@ -708,8 +708,6 @@ in
|
|||
"--hive-client-prefix ${lib.escapeShellArg autheliaCfg.hiveClientPrefix}"
|
||||
"--agent-client-suffix ${lib.escapeShellArg autheliaCfg.agentClientSuffix}"
|
||||
"--reader-client ${lib.escapeShellArg controllerCfg.queueClientId}"
|
||||
# Hears try-outs and answers each by reply, and nothing else.
|
||||
"--review-classifier-client ${lib.escapeShellArg config.services.hyperhive.swarm.review-classifier.clientId}"
|
||||
# What an agent may publish to, keyed on its own name: its
|
||||
# terminal rows and its turn-state header
|
||||
# (`hive-agent::swarm_agent_state`), two subject families so a
|
||||
|
|
|
|||
|
|
@ -105,16 +105,4 @@ in
|
|||
# `singleHostSwarm` box, which gets here through `allSwarmServices`, would
|
||||
# have no forge at all.
|
||||
config.services.hyperhive.deploy.forgejo.enable = lib.mkDefault deployCfg.allSwarmServices;
|
||||
|
||||
# The review classifier. Its module asserts a store identity, which
|
||||
# glue-bao-tls.nix mints only on the host that runs the store; with the store
|
||||
# placed elsewhere both files stay null and an unconditional `true` here would
|
||||
# fail that host's eval. So the default follows the identity, and a host
|
||||
# holding a copy of the pair (`deploy.bao.reviewClassifierClient{Cert,Key}File`)
|
||||
# gets the classifier too.
|
||||
config.services.hyperhive.deploy.review-classifier.enable = lib.mkDefault (
|
||||
deployCfg.allSwarmServices
|
||||
&& deployCfg.bao.reviewClassifierClientCertFile != null
|
||||
&& deployCfg.bao.reviewClassifierClientKeyFile != null
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,213 +0,0 @@
|
|||
# The review classifier, in a `swarm-review-classifier` container on the host
|
||||
# that sets `deploy.review-classifier.enable`, which `deploy.allSwarmServices`
|
||||
# defaults on where the host has the classifier's store identity. It answers the
|
||||
# swarm UI's review try-outs: swarm-controller sends a PR and the operator's classifications over
|
||||
# the queue, and the daemon runs `claude` once per classification and replies.
|
||||
# docs/swarm/review-classifier.md has the design.
|
||||
#
|
||||
# ⚠️ Its own network namespace on the bridge, as ./swarm-ci-runner.nix has: a
|
||||
# PR's diff and commit messages are model input, so the container reaches the
|
||||
# swarm services by name and nothing on the host's loopback.
|
||||
#
|
||||
# The container holds one credential, its own store leaf, which nspawn hands in
|
||||
# as a system credential with no copy on disk. The daemon reads everything else
|
||||
# from the store with it: its queue client secret at start and the API key on
|
||||
# every run.
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
hyperhiveCfg = config.services.hyperhive;
|
||||
cfg = hyperhiveCfg.deploy.review-classifier;
|
||||
baoCfg = hyperhiveCfg.swarm.bao;
|
||||
baoDeploy = hyperhiveCfg.deploy.bao;
|
||||
natsCfg = hyperhiveCfg.swarm.nats;
|
||||
autheliaCfg = hyperhiveCfg.swarm.authelia;
|
||||
gatewayCfg = hyperhiveCfg.gateway;
|
||||
networkCfg = hyperhiveCfg.network;
|
||||
otelCfg = hyperhiveCfg.otel;
|
||||
tlsCfg = hyperhiveCfg.deploy.hive-controller.tls;
|
||||
|
||||
machine = "swarm-review-classifier";
|
||||
unit = "swarm-review-classifier";
|
||||
clientId = hyperhiveCfg.swarm.review-classifier.clientId;
|
||||
|
||||
# The daemon reaches authelia, the store and the queue by name over TLS, and
|
||||
# in self-signed mode their leaves chain to the hive CA.
|
||||
caTrust = import ./lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; };
|
||||
caBundleModule = caTrust.trustBundle {
|
||||
inherit pkgs;
|
||||
name = machine;
|
||||
consumers = [ unit ];
|
||||
};
|
||||
|
||||
haveClientIdentity =
|
||||
baoDeploy.reviewClassifierClientCertFile != null && baoDeploy.reviewClassifierClientKeyFile != null;
|
||||
|
||||
# Credential id → host source. nspawn reads each source as root when the
|
||||
# container starts and hands it in as a system credential, held in memory;
|
||||
# the daemon's unit alone loads them. No copy lands on disk, as for an
|
||||
# agent's own leaf (`nix/agent-modules/bao.nix`). Ids carry no dot, the
|
||||
# spelling `--load-credential` is safe with.
|
||||
storeFiles = lib.filterAttrs (_: source: source != null) {
|
||||
review-classifier-bao-cert = baoDeploy.reviewClassifierClientCertFile;
|
||||
review-classifier-bao-key = baoDeploy.reviewClassifierClientKeyFile;
|
||||
review-classifier-bao-server-ca = baoDeploy.serverCaFile;
|
||||
};
|
||||
# nixos-container expands `extraFlags` unquoted, so a source path must be
|
||||
# one word.
|
||||
oneWord = path: builtins.match ".*[[:space:]].*" path == null;
|
||||
credential = id: "/run/credentials/${unit}.service/${id}";
|
||||
|
||||
# What `claude` exports its metrics with, where this host runs a hive-tier
|
||||
# collector on the bridge. The daemon adds the repo and the PR to the
|
||||
# resource attributes for each run.
|
||||
otelEnv = lib.optionalAttrs otelCfg.enable {
|
||||
CLAUDE_CODE_ENABLE_TELEMETRY = "1";
|
||||
OTEL_METRICS_EXPORTER = "otlp";
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT = "http://${networkCfg.bridgeIp}:${toString otelCfg.collector.port}";
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL = "http/protobuf";
|
||||
# Claude Code defaults to delta, which the metrics store drops.
|
||||
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = "cumulative";
|
||||
OTEL_RESOURCE_ATTRIBUTES = "service.name=hyperhive-review-classifier,swarm=${
|
||||
if hyperhiveCfg.swarm.name == null then "unknown" else hyperhiveCfg.swarm.name
|
||||
}";
|
||||
};
|
||||
|
||||
privateNetwork = true;
|
||||
in
|
||||
{
|
||||
options.services.hyperhive.swarm.review-classifier.clientId = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
readOnly = true;
|
||||
default = "swarm-review-classifier";
|
||||
description = ''
|
||||
The OAuth2 client id the review classifier presents to the swarm queue.
|
||||
Read-only: the host running authelia registers it, the queue's
|
||||
auth-callout responder grants it the try-out subject, and the secret
|
||||
publisher carries its secret to the store, all from this one value.
|
||||
'';
|
||||
};
|
||||
|
||||
options.services.hyperhive.deploy.review-classifier.package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-review-classifier";
|
||||
description = "The review classifier daemon package.";
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = haveClientIdentity;
|
||||
message = ''
|
||||
services.hyperhive.deploy.review-classifier.enable requires the
|
||||
classifier's swarm-secret-store client identity: set both
|
||||
|
||||
services.hyperhive.deploy.bao.reviewClassifierClientCertFile
|
||||
services.hyperhive.deploy.bao.reviewClassifierClientKeyFile
|
||||
|
||||
The classifier reads its queue client secret and the API key with it.
|
||||
|
||||
Both default to the pair glue-bao-tls.nix mints when
|
||||
services.hyperhive.deploy.bao.enable is true (the host that runs the
|
||||
store). Otherwise copy review-classifier.pem and
|
||||
review-classifier-key.pem from the store host's /var/lib/swarm-bao-pki/
|
||||
and name them in those two options.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = lib.all oneWord (lib.attrValues storeFiles);
|
||||
message = ''
|
||||
The review classifier's store identity paths must not contain
|
||||
whitespace: nspawn's --load-credential receives them through
|
||||
nixos-container's unquoted extra flags.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
# The container hangs off the bridge, takes its address from the gateway's
|
||||
# DHCP and resolves through its dnsmasq.
|
||||
services.hyperhive.network.enable = lib.mkDefault true;
|
||||
services.hyperhive.gateway.enable = lib.mkDefault true;
|
||||
services.hyperhive.gateway.dns.enable = lib.mkDefault true;
|
||||
|
||||
# nspawn fails to start the container when a credential source is
|
||||
# missing, so where this host mints the leaf the container waits for it.
|
||||
systemd.services."container@${machine}" = lib.mkMerge [
|
||||
caTrust.containerOrdering
|
||||
{ after = [ "swarm-bao-pki.service" ]; }
|
||||
];
|
||||
|
||||
containers.${machine} = {
|
||||
autoStart = true;
|
||||
# The daemon keeps no state; its scratch directories are under PrivateTmp.
|
||||
ephemeral = false;
|
||||
# Journal files on the host, not inside the container: nixpkgs hardcodes
|
||||
# --link-journal=try-guest, and EXTRA_NSPAWN_FLAGS expands after it.
|
||||
extraFlags = [
|
||||
"--link-journal=host"
|
||||
]
|
||||
++ lib.mapAttrsToList (id: source: "--load-credential=${id}:${source}") storeFiles;
|
||||
inherit privateNetwork;
|
||||
hostBridge = networkCfg.bridgeName;
|
||||
bindMounts = caTrust.bindMount;
|
||||
|
||||
config =
|
||||
{ pkgs, lib, ... }:
|
||||
{
|
||||
imports = [
|
||||
../container-modules/swarm-container.nix
|
||||
caBundleModule
|
||||
];
|
||||
|
||||
services.hyperhive.swarmContainer = {
|
||||
inherit privateNetwork;
|
||||
writesOwnResolvConf = false;
|
||||
};
|
||||
networking.nameservers = [ networkCfg.bridgeIp ];
|
||||
networking.interfaces.eth0.useDHCP = true;
|
||||
|
||||
# `claude-code` is unfree, and this container evaluates its own
|
||||
# nixpkgs, so the host's allowance does not reach here.
|
||||
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ "claude-code" ];
|
||||
|
||||
systemd.services.${unit} = {
|
||||
description = "answer the swarm UI's review try-outs";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
wants = [ "network-online.target" ];
|
||||
after = [ "network-online.target" ];
|
||||
environment = {
|
||||
BAO_ADDR = "https://${baoCfg.domain}:${toString baoCfg.port}";
|
||||
BAO_CLIENT_CERT = credential "review-classifier-bao-cert";
|
||||
BAO_CLIENT_KEY = credential "review-classifier-bao-key";
|
||||
SWARM_REVIEW_CLASSIFIER_NATS_URL = "tls://${natsCfg.domain}:${toString natsCfg.port}";
|
||||
SWARM_REVIEW_CLASSIFIER_OIDC_TOKEN_ENDPOINT = "${autheliaCfg.url}/api/oidc/token";
|
||||
SWARM_REVIEW_CLASSIFIER_OIDC_CLIENT_ID = clientId;
|
||||
SWARM_REVIEW_CLASSIFIER_CLAUDE = "${pkgs.claude-code}/bin/claude";
|
||||
}
|
||||
// lib.optionalAttrs (storeFiles ? review-classifier-bao-server-ca) {
|
||||
BAO_CACERT = credential "review-classifier-bao-server-ca";
|
||||
}
|
||||
// lib.optionalAttrs caTrust.useSelfSigned {
|
||||
SWARM_REVIEW_CLASSIFIER_OIDC_CA_FILE = caTrust.bundlePathFor machine;
|
||||
}
|
||||
// otelEnv;
|
||||
serviceConfig = {
|
||||
ExecStart = "${cfg.package}/bin/swarm-review-classifier";
|
||||
DynamicUser = true;
|
||||
# Each classification's scratch directory, gone with the unit.
|
||||
PrivateTmp = true;
|
||||
# Bare ids: the system credentials nspawn handed in.
|
||||
LoadCredential = lib.attrNames storeFiles;
|
||||
Restart = "on-failure";
|
||||
RestartSec = "10s";
|
||||
SyslogIdentifier = unit;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -90,8 +90,6 @@ let
|
|||
hyperhiveCfg.swarm.matrix.sso.clientId
|
||||
# The forge's login source, read by `swarm-bao-forge-oidc` on its host.
|
||||
hyperhiveCfg.swarm.forge.sso.clientId
|
||||
# The review classifier, which reads it itself inside its container.
|
||||
hyperhiveCfg.swarm.review-classifier.clientId
|
||||
];
|
||||
|
||||
# The swarm controller's OIDC client, which it reads back from the store
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ let
|
|||
deployCfg.bao.granterCommonName
|
||||
deployCfg.bao.natsAuthCommonName
|
||||
deployCfg.bao.ciRunnerCommonName
|
||||
deployCfg.bao.reviewClassifierCommonName
|
||||
deployCfg.bao.forgePublisherCommonName
|
||||
]
|
||||
# The per-hive subjects, spelled out per hive rather than as the prefix.
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ let
|
|||
_: u: (u.environment.BAO_CLIENT_CERT or null) == granterCertFile
|
||||
) baoGrantWithConsumers.systemd.services;
|
||||
|
||||
# The sixteen units that write a `swarm-*` grant, by name, for the discovery
|
||||
# The fifteen units that write a `swarm-*` grant, by name, for the discovery
|
||||
# control below.
|
||||
grantingUnitNames = [
|
||||
"swarm-bao-controller-policy"
|
||||
|
|
@ -188,7 +188,6 @@ let
|
|||
"swarm-bao-hive-leaf-policy"
|
||||
"swarm-bao-ci-runner-policy"
|
||||
"swarm-bao-forge-publisher-policy"
|
||||
"swarm-bao-review-classifier-policy"
|
||||
];
|
||||
|
||||
# Comment lines dropped first: both the HCL and the scripts explain
|
||||
|
|
@ -853,24 +852,24 @@ let
|
|||
}
|
||||
{
|
||||
# A store host without the granter's pair writes its grants some other
|
||||
# way, so none of the sixteen units may exist. Without this arm
|
||||
# way, so none of the fifteen units may exist. Without this arm
|
||||
# `lib.mkIf haveGranter` could be dropped from any of them and every other
|
||||
# case here would still pass.
|
||||
name = "without the granter's pair none of the sixteen granting units render";
|
||||
name = "without the granter's pair none of the fifteen granting units render";
|
||||
ok =
|
||||
let
|
||||
s = baoGranterOptOut.systemd.services;
|
||||
in
|
||||
lib.all (unit: !(s ? ${unit})) (grantingUnitNames ++ [ "swarm-bao-granter-role" ])
|
||||
# The control: the same store with the pair renders all sixteen.
|
||||
# The control: the same store with the pair renders all fifteen.
|
||||
&& lib.all (unit: baoGrantHere.systemd.services ? ${unit}) grantingUnitNames;
|
||||
}
|
||||
{
|
||||
# 🩸 What replaced the silent skip. With no bootstrap token the sixteen still
|
||||
# 🩸 What replaced the silent skip. With no bootstrap token the fifteen still
|
||||
# render, and a refused granter fails them with the step that fixes it.
|
||||
# A store host that never named a token is told to name one, since the
|
||||
# unit that sets the granter up renders only where it has.
|
||||
name = "a store host without a bootstrap token renders the sixteen, each failing loudly with the one-time step";
|
||||
name = "a store host without a bootstrap token renders the fifteen, each failing loudly with the one-time step";
|
||||
ok =
|
||||
let
|
||||
s = baoGranterNoToken.systemd.services;
|
||||
|
|
@ -1373,34 +1372,6 @@ let
|
|||
in
|
||||
lib.hasInfix "/ci-runner.pem ]" s && lib.hasInfix "swarm-ci-runner \"\" clientAuth" s;
|
||||
}
|
||||
{
|
||||
# The classifier reads the key the operator writes and its own queue
|
||||
# client secret, each read-only and spelled to the leaf: `services/*`
|
||||
# would reach every swarm service's secret.
|
||||
name = "the review classifier's grant is its API key and its queue client secret, and nothing else";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-review-classifier-policy.script;
|
||||
in
|
||||
lib.hasInfix "path \"secret/data/swarm/services/review-classifier/anthropic-api-key\" {\n capabilities = [\"read\"]\n}" s
|
||||
&& lib.hasInfix "path \"secret/data/swarm/services/swarm-review-classifier/oidc/client\" {\n capabilities = [\"read\"]\n}" s
|
||||
&& lib.length (lib.filter lib.isList (builtins.split "path \"" s)) == 2
|
||||
&& !(lib.hasInfix "secret/data/swarm/services/*" s)
|
||||
&& !(lib.hasInfix "secret/data/swarm/agents" s)
|
||||
&& !(lib.hasInfix "secret/data/swarm/hives" s)
|
||||
&& lib.hasInfix "auth/cert/certs/swarm-review-classifier" s
|
||||
&& lib.hasInfix "allowed_common_names=swarm-review-classifier" s
|
||||
&& lib.hasInfix "token_policies=swarm-review-classifier" s;
|
||||
}
|
||||
{
|
||||
name = "the PKI unit signs the review classifier's leaf under its own subject";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-pki.script;
|
||||
in
|
||||
lib.hasInfix "/review-classifier.pem ]" s
|
||||
&& lib.hasInfix "swarm-review-classifier \"\" clientAuth" s;
|
||||
}
|
||||
{
|
||||
# The forge's host is the one writer of the controller's forge token,
|
||||
# and reads the forge's own OIDC client secret under the same leaf.
|
||||
|
|
@ -1595,8 +1566,8 @@ let
|
|||
}
|
||||
{
|
||||
# What makes the case above mean something: discovery by the granter's
|
||||
# certificate reaches all sixteen units, and each yields calls.
|
||||
name = "the granter-policy check sees all sixteen granting units, and parses calls from each";
|
||||
# certificate reaches all fifteen units, and each yields calls.
|
||||
name = "the granter-policy check sees all fifteen granting units, and parses calls from each";
|
||||
ok =
|
||||
lib.sort lib.lessThan (lib.attrNames granterUnits) == lib.sort lib.lessThan grantingUnitNames
|
||||
&& lib.all (u: baoCalls u.script != [ ]) (lib.attrValues granterUnits)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue