nix: run the review classifier in a container of its own when a host enables it
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).
This commit is contained in:
parent
d70ed41537
commit
5d8ab675cd
14 changed files with 435 additions and 8 deletions
|
|
@ -170,7 +170,7 @@ let
|
|||
_: u: (u.environment.BAO_CLIENT_CERT or null) == granterCertFile
|
||||
) baoGrantWithConsumers.systemd.services;
|
||||
|
||||
# The fifteen units that write a `swarm-*` grant, by name, for the discovery
|
||||
# The sixteen units that write a `swarm-*` grant, by name, for the discovery
|
||||
# control below.
|
||||
grantingUnitNames = [
|
||||
"swarm-bao-controller-policy"
|
||||
|
|
@ -188,6 +188,7 @@ 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
|
||||
|
|
@ -852,24 +853,24 @@ let
|
|||
}
|
||||
{
|
||||
# A store host without the granter's pair writes its grants some other
|
||||
# way, so none of the fifteen units may exist. Without this arm
|
||||
# way, so none of the sixteen 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 fifteen granting units render";
|
||||
name = "without the granter's pair none of the sixteen 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 fifteen.
|
||||
# The control: the same store with the pair renders all sixteen.
|
||||
&& lib.all (unit: baoGrantHere.systemd.services ? ${unit}) grantingUnitNames;
|
||||
}
|
||||
{
|
||||
# 🩸 What replaced the silent skip. With no bootstrap token the fifteen still
|
||||
# 🩸 What replaced the silent skip. With no bootstrap token the sixteen 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 fifteen, each failing loudly with the one-time step";
|
||||
name = "a store host without a bootstrap token renders the sixteen, each failing loudly with the one-time step";
|
||||
ok =
|
||||
let
|
||||
s = baoGranterNoToken.systemd.services;
|
||||
|
|
@ -1372,6 +1373,34 @@ 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.
|
||||
|
|
@ -1566,8 +1595,8 @@ let
|
|||
}
|
||||
{
|
||||
# What makes the case above mean something: discovery by the granter's
|
||||
# certificate reaches all fifteen units, and each yields calls.
|
||||
name = "the granter-policy check sees all fifteen granting units, and parses calls from each";
|
||||
# certificate reaches all sixteen units, and each yields calls.
|
||||
name = "the granter-policy check sees all sixteen 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)
|
||||
|
|
|
|||
|
|
@ -53,6 +53,12 @@ let
|
|||
deploy.ci-runner.enable = true;
|
||||
};
|
||||
|
||||
# On the store's host, which mints the classifier's store identity.
|
||||
withClassifier = hive {
|
||||
deploy.bao.enable = true;
|
||||
deploy.review-classifier.enable = true;
|
||||
};
|
||||
|
||||
# A priority collision is a property of the *option*, not
|
||||
# of the merged value's interior — nix throws the moment the value is
|
||||
# demanded at all, so `seq`-ing each `serviceConfig` value to WHNF is
|
||||
|
|
@ -296,6 +302,32 @@ let
|
|||
name = "the CI container's unit definitions merge without a priority collision";
|
||||
ok = forceCiServiceConfigs;
|
||||
}
|
||||
{
|
||||
# It spends money on an operator's key, so no host runs it by default.
|
||||
name = "the review classifier's container exists only where its host enables it";
|
||||
ok =
|
||||
!(bare.containers ? swarm-review-classifier)
|
||||
&& !(centralToggleOff.containers ? swarm-review-classifier)
|
||||
&& withClassifier.containers ? swarm-review-classifier;
|
||||
}
|
||||
{
|
||||
# The daemon holds its store leaf and nothing else from the host; the
|
||||
# key is read per run and reaches only `claude`'s environment. Reading
|
||||
# the claude path also evaluates the container's unfree allowance.
|
||||
name = "the review classifier loads only its store leaf and carries no API key in its unit";
|
||||
ok =
|
||||
let
|
||||
svc =
|
||||
withClassifier.containers.swarm-review-classifier.config.systemd.services.swarm-review-classifier;
|
||||
env = svc.environment;
|
||||
in
|
||||
lib.elem "bao-client.pem:/var/lib/swarm-review-classifier/bao-client.pem" svc.serviceConfig.LoadCredential
|
||||
&& env.BAO_CLIENT_CERT == "/run/credentials/swarm-review-classifier.service/bao-client.pem"
|
||||
&& env.SWARM_REVIEW_CLASSIFIER_OIDC_CLIENT_ID == "swarm-review-classifier"
|
||||
&& lib.hasSuffix "/bin/claude" env.SWARM_REVIEW_CLASSIFIER_CLAUDE
|
||||
&& !(env ? ANTHROPIC_API_KEY)
|
||||
&& withClassifier.systemd.services ? swarm-review-classifier-identity;
|
||||
}
|
||||
{
|
||||
# The collector reaches these routes through the gateway now, so each
|
||||
# store needs an ingest location of its own. Without one the write rides
|
||||
|
|
|
|||
|
|
@ -227,6 +227,22 @@ let
|
|||
secretPublisherHere.systemd.services.swarm-secret-publish.script
|
||||
);
|
||||
}
|
||||
{
|
||||
# The review classifier reads its queue client secret from the store
|
||||
# inside its container; authelia registers it on every IdP host.
|
||||
name = "the review classifier's client is registered as a machine, and the publisher carries its secret";
|
||||
ok =
|
||||
let
|
||||
c = lib.findFirst (
|
||||
c: c.id == "swarm-review-classifier"
|
||||
) null secretPublisherHere.services.hyperhive.swarm.authelia.oidc.clients;
|
||||
in
|
||||
c != null
|
||||
&& c.kind == "machine"
|
||||
&& lib.hasInfix "secret/swarm/services/swarm-review-classifier/oidc/client" (
|
||||
secretPublisherHere.systemd.services.swarm-secret-publish.script
|
||||
);
|
||||
}
|
||||
{
|
||||
# The store's UI login: authelia registers the client with the callback
|
||||
# the store's `oidc` role allows, and the publisher carries its secret
|
||||
|
|
|
|||
Loading…
Reference in a new issue