Watch
0
0
Fork
You've already forked hyperhive
0

nix: allSwarmServices turns on the review classifier

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.
This commit is contained in:
atlas 2026-10-12 10:48:32 +02:00 • committed by mara
commit 34bde7a5af
5 changed files with 67 additions and 24 deletions

View file

@ -236,17 +236,18 @@ the container and the threat model are in
### Review classifier
The review classifier's try-out service, in a `swarm-review-classifier`
container on the host that sets `deploy.review-classifier.enable`. It doesn't
derive from `allSwarmServices`: every run spends money on an Anthropic API
key, so running one is a decision of its own.
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.
Two steps turn it on:
1. Write the key in the secret store's UI at
`secret/swarm/services/review-classifier/anthropic-api-key`, field
`api_key`. The service reads it on every run, so writing or removing it
takes effect on the next run, with no restart.
2. Set `services.hyperhive.deploy.review-classifier.enable = true` on a host.
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

View file

@ -653,9 +653,13 @@ in
`secret/swarm/services/review-classifier/anthropic-api-key`, field
`api_key`.
Not derived from
{option}`services.hyperhive.deploy.allSwarmServices`: every run spends
money on that key, so running one is a decision of its own.
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…".
'';
};

View file

@ -105,4 +105,16 @@ 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
);
}

View file

@ -1,6 +1,7 @@
# The review classifier, in a `swarm-review-classifier` container on the host
# that sets `deploy.review-classifier.enable`. It answers the swarm UI's review
# try-outs: swarm-controller sends a PR and the operator's classifications over
# 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.
#

View file

@ -93,6 +93,12 @@ let
# swarm-required-services.nix documents the switch for.
swarmServicesHere = hiveWithController { deploy.allSwarmServices = true; };
# Same, with the operator opting out of the classifier alone.
swarmServicesWithoutClassifier = hiveWithController {
deploy.allSwarmServices = true;
deploy.review-classifier.enable = false;
};
# Same, with one of those services placed on another host. Every derivation
# in that module is `mkDefault` so this stays expressible.
swarmServicesBaoElsewhere = hive {
@ -158,7 +164,8 @@ let
lib.filter (l: swarmServiceLinkDomains ? ${l.label}) cfg.services.hyperhive.swarm.controller.links;
# Every service container at once: the services-only host plus the CI
# runner, the one container on a netns of its own.
# runner. The CI runner and the review classifier are the two containers on
# a netns of their own.
serviceContainersWithCi = hive {
deploy.hive-controller.enable = false;
deploy.allSwarmServices = true;
@ -178,15 +185,33 @@ let
lib.length (lib.attrNames es) == 9 && lib.all lib.id (lib.attrValues es);
}
{
# Every run spends money on an operator's key, so hosting the shared
# services does not start the classifier. The queue is the control:
# the same fixture does turn the switch's own services on.
name = "hosting the swarm's shared services leaves the review classifier off";
# The container is the control that the toggle reaches the module, not
# only the option; `nats` is the control that the switch is on at all.
name = "hosting the swarm's shared services turns on the review classifier";
ok =
!swarmServicesHere.services.hyperhive.deploy.review-classifier.enable
&& !(swarmServicesHere.containers ? swarm-review-classifier)
swarmServicesHere.services.hyperhive.deploy.review-classifier.enable
&& swarmServicesHere.containers ? swarm-review-classifier
&& swarmServicesHere.services.hyperhive.deploy.nats.enable;
}
{
# The default yields to an operator who has spoken. The same fixture
# without the line is the control.
name = "an explicit false keeps the review classifier off where the shared services are hosted";
ok =
!swarmServicesWithoutClassifier.services.hyperhive.deploy.review-classifier.enable
&& !(swarmServicesWithoutClassifier.containers ? swarm-review-classifier)
&& swarmServicesWithoutClassifier.services.hyperhive.deploy.nats.enable;
}
{
# The classifier's module asserts a store identity that only the store's
# host mints, so with the store placed elsewhere the default stays off
# rather than failing that host's eval. `nats` is the control.
name = "the review classifier stays off where the shared services are hosted without the store";
ok =
!swarmServicesBaoElsewhere.services.hyperhive.deploy.review-classifier.enable
&& !(swarmServicesBaoElsewhere.containers ? swarm-review-classifier)
&& swarmServicesBaoElsewhere.services.hyperhive.deploy.nats.enable;
}
{
# The switch fills in for an operator who has not spoken and yields to
# one who has — that is what keeps a shared service placeable on a host
@ -431,7 +456,7 @@ let
# that lost a container can't pass by comparing fewer of them.
name = "every service container restates its host-side privateNetwork";
ok =
lib.length (lib.attrNames serviceContainers) == 10
lib.length (lib.attrNames serviceContainers) == 11
&& lib.all (
c: c.privateNetwork == (c.config.services.hyperhive.swarmContainer.privateNetwork or null)
) (lib.attrValues serviceContainers);