Watch
0
0
Fork
You've already forked hyperhive
0

swarm builder: the all-swarm-services switch turns it on, like every other swarm service

deploy.builder.enable defaults on under deploy.allSwarmServices (mkDefault),
where the host has the builder's store identity pair, as for the review
classifier. An explicit false still wins.
This commit is contained in:
atlas 2026-10-14 22:33:57 +02:00 • committed by mara
commit d24dbaeadf
7 changed files with 68 additions and 26 deletions

View file

@ -268,9 +268,11 @@ the container and the threat model are in
### Builder ### Builder
The swarm builder, in a `swarm-builder` container on the host that sets The swarm builder, in a `swarm-builder` container on the host that sets
`deploy.builder.enable`. It doesn't derive from `allSwarmServices`: it `deploy.builder.enable`. `allSwarmServices` defaults it on wherever the host
evaluates and builds unreviewed config PRs through its host's nix daemon, so has the builder's store identity, as for the review classifier below: the host
running one is a decision of its own, as for the CI runner. that runs the store has it. Set `deploy.builder.enable = false` to opt out. It
evaluates unreviewed config PRs and builds them through its host's nix daemon,
from a container with its own network namespace.
When an `agent-configs/<agent>` PR opens, reopens or gets a new push, When an `agent-configs/<agent>` PR opens, reopens or gets a new push,
swarm-controller queues a `build_agent` job. Jobs run one at a time, and a swarm-controller queues a `build_agent` job. Jobs run one at a time, and a

View file

@ -133,7 +133,8 @@ checkpoints**, not about sandboxing the agent from its own tools:
scheduled prompts are operator-only, set at swarm level scheduled prompts are operator-only, set at swarm level
(see [`schedules.md`](../swarm/schedules.md)). (see [`schedules.md`](../swarm/schedules.md)).
Before that merge, the swarm builder evaluates an unmerged config PR Before that merge, the swarm builder evaluates an unmerged config PR
off-hive, on any host that sets `deploy.builder.enable`: it builds the off-hive, on any host that sets `deploy.builder.enable` (the
all-swarm-services switch defaults it on): it builds the
PR's head with a read-only forge token, in its own netns, as an untrusted PR's head with a read-only forge token, in its own netns, as an untrusted
client of its host's nix daemon (see [`builder.md`](../swarm/builder.md)). client of its host's nix daemon (see [`builder.md`](../swarm/builder.md)).

View file

@ -694,13 +694,13 @@ in
this host's nix daemon, and swarm-controller writes the result as a this host's nix daemon, and swarm-controller writes the result as a
`swarm-builder` commit status on the PR. `swarm-builder` commit status on the PR.
Not derived from On by default where
{option}`services.hyperhive.deploy.allSwarmServices`: the builder {option}`services.hyperhive.deploy.allSwarmServices` is set and this
evaluates and builds unreviewed config PRs on this host's nix daemon, host has the builder's store identity (the pair
so running one is a decision of its own, as for the CI runner. It needs {option}`services.hyperhive.deploy.bao.builderClientCertFile` and
the builder's store identity `builderClientKeyFile`, minted on the store's host). It evaluates
({option}`services.hyperhive.deploy.bao.builderClientCertFile` and unreviewed config PRs and builds them through this host's nix daemon,
`builderClientKeyFile`), which the store's host mints. from a container with its own network namespace.
''; '';
}; };

View file

@ -1,5 +1,6 @@
# The swarm builder, in a `swarm-builder` container on the host that sets # The swarm builder, in a `swarm-builder` container on the host that sets
# `deploy.builder.enable`, which nothing turns on for the operator: it runs # `deploy.builder.enable`, which `deploy.allSwarmServices` defaults on where the
# host has the builder's store identity (./swarm-required-services.nix). It runs
# unreviewed config PRs through the host's nix daemon. It builds agent configs # unreviewed config PRs through the host's nix daemon. It builds agent configs
# for swarm-controller: the controller sends a config PR's head over the # for swarm-controller: the controller sends a config PR's head over the
# queue, and the daemon builds that config on this host's agent base and # queue, and the daemon builds that config on this host's agent base and

View file

@ -117,4 +117,13 @@ in
&& deployCfg.bao.reviewClassifierClientCertFile != null && deployCfg.bao.reviewClassifierClientCertFile != null
&& deployCfg.bao.reviewClassifierClientKeyFile != null && deployCfg.bao.reviewClassifierClientKeyFile != null
); );
# The builder, by the same rule and for the same reason: its module asserts
# the builder's store identity. It evaluates unreviewed config PRs, which is
# why ./swarm-builder.nix gives it a container with its own netns.
config.services.hyperhive.deploy.builder.enable = lib.mkDefault (
deployCfg.allSwarmServices
&& deployCfg.bao.builderClientCertFile != null
&& deployCfg.bao.builderClientKeyFile != null
);
} }

View file

@ -32,6 +32,10 @@ let
}; };
servicesHere = hiveWithController { deploy.allSwarmServices = true; }; servicesHere = hiveWithController { deploy.allSwarmServices = true; };
# Short-circuits on `assertion`, so only a failing one has its message read.
refusedWithoutIdentity =
m:
lib.any (a: !a.assertion && lib.hasInfix "deploy.builder.enable requires" a.message) m.assertions;
natsHere = hive { natsHere = hive {
swarm.nats.enable = true; swarm.nats.enable = true;
@ -54,13 +58,17 @@ let
ok = !(bare.containers ? swarm-builder) && withBuilder.containers ? swarm-builder; ok = !(bare.containers ? swarm-builder) && withBuilder.containers ? swarm-builder;
} }
{ {
# It runs unreviewed config PRs through the host's nix daemon, so hosting # The store's host mints the builder's leaf, so the switch needs no
# the shared services, even with the store's leaf here, does not turn it # manual identity step: the pair is set and the module's identity
# on. `nats` is the control that the switch is on at all. # assertion, which nothing else reads in this fixture, passes. `nats` is
name = "hosting the shared services leaves the builder off"; # the control that the switch is on.
name = "hosting the shared services turns the builder on, with its store identity";
ok = ok =
!servicesHere.services.hyperhive.deploy.builder.enable servicesHere.services.hyperhive.deploy.builder.enable
&& !(servicesHere.containers ? swarm-builder) && servicesHere.containers ? swarm-builder
&& servicesHere.services.hyperhive.deploy.bao.builderClientCertFile != null
&& servicesHere.services.hyperhive.deploy.bao.builderClientKeyFile != null
&& !(refusedWithoutIdentity servicesHere)
&& servicesHere.services.hyperhive.deploy.bao.enable && servicesHere.services.hyperhive.deploy.bao.enable
&& servicesHere.services.hyperhive.deploy.nats.enable; && servicesHere.services.hyperhive.deploy.nats.enable;
} }

View file

@ -90,10 +90,12 @@ let
# swarm-required-services.nix documents the switch for. # swarm-required-services.nix documents the switch for.
swarmServicesHere = hiveWithController { deploy.allSwarmServices = true; }; swarmServicesHere = hiveWithController { deploy.allSwarmServices = true; };
# Same, with the operator opting out of the classifier alone. # Same, with the operator opting out of the classifier and the builder,
swarmServicesWithoutClassifier = hiveWithController { # which the switch defaults on and everything else it owns stays on.
swarmServicesOptedOut = hiveWithController {
deploy.allSwarmServices = true; deploy.allSwarmServices = true;
deploy.review-classifier.enable = false; deploy.review-classifier.enable = false;
deploy.builder.enable = false;
}; };
# Same, with one of those services placed on another host. Every derivation # Same, with one of those services placed on another host. Every derivation
@ -161,8 +163,8 @@ let
lib.filter (l: swarmServiceLinkDomains ? ${l.label}) cfg.services.hyperhive.swarm.controller.links; lib.filter (l: swarmServiceLinkDomains ? ${l.label}) cfg.services.hyperhive.swarm.controller.links;
# Every service container at once: the services-only host plus the CI # Every service container at once: the services-only host plus the CI
# runner. The CI runner, the review classifier and the metrics store are the # runner. The CI runner, the review classifier, the builder and the metrics
# three containers on a netns of their own. # store are the four containers on a netns of their own.
serviceContainersWithCi = hive { serviceContainersWithCi = hive {
deploy.hive-controller.enable = false; deploy.hive-controller.enable = false;
deploy.allSwarmServices = true; deploy.allSwarmServices = true;
@ -212,9 +214,19 @@ let
# without the line is the control. # without the line is the control.
name = "an explicit false keeps the review classifier off where the shared services are hosted"; name = "an explicit false keeps the review classifier off where the shared services are hosted";
ok = ok =
!swarmServicesWithoutClassifier.services.hyperhive.deploy.review-classifier.enable !swarmServicesOptedOut.services.hyperhive.deploy.review-classifier.enable
&& !(swarmServicesWithoutClassifier.containers ? swarm-review-classifier) && !(swarmServicesOptedOut.containers ? swarm-review-classifier)
&& swarmServicesWithoutClassifier.services.hyperhive.deploy.nats.enable; && swarmServicesOptedOut.services.hyperhive.deploy.nats.enable;
}
{
# Same as the classifier's case above, for the builder: the opt-out is
# the only difference from `swarmServicesHere`, which has the container.
name = "an explicit false keeps the swarm builder off where the shared services are hosted";
ok =
!swarmServicesOptedOut.services.hyperhive.deploy.builder.enable
&& !(swarmServicesOptedOut.containers ? swarm-builder)
&& swarmServicesHere.containers ? swarm-builder
&& swarmServicesOptedOut.services.hyperhive.deploy.nats.enable;
} }
{ {
# The classifier's module asserts a store identity that only the store's # The classifier's module asserts a store identity that only the store's
@ -226,6 +238,15 @@ let
&& !(swarmServicesBaoElsewhere.containers ? swarm-review-classifier) && !(swarmServicesBaoElsewhere.containers ? swarm-review-classifier)
&& swarmServicesBaoElsewhere.services.hyperhive.deploy.nats.enable; && swarmServicesBaoElsewhere.services.hyperhive.deploy.nats.enable;
} }
{
# The builder's module asserts its own store identity, so the same
# holds for it. `nats` is the control.
name = "the swarm builder stays off where the shared services are hosted without the store";
ok =
!swarmServicesBaoElsewhere.services.hyperhive.deploy.builder.enable
&& !(swarmServicesBaoElsewhere.containers ? swarm-builder)
&& swarmServicesBaoElsewhere.services.hyperhive.deploy.nats.enable;
}
{ {
# The switch fills in for an operator who has not spoken and yields to # 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 # one who has — that is what keeps a shared service placeable on a host
@ -489,7 +510,7 @@ let
# that lost a container can't pass by comparing fewer of them. # that lost a container can't pass by comparing fewer of them.
name = "every service container restates its host-side privateNetwork"; name = "every service container restates its host-side privateNetwork";
ok = ok =
lib.length (lib.attrNames serviceContainers) == 11 lib.length (lib.attrNames serviceContainers) == 12
&& lib.all ( && lib.all (
c: c.privateNetwork == (c.config.services.hyperhive.swarmContainer.privateNetwork or null) c: c.privateNetwork == (c.config.services.hyperhive.swarmContainer.privateNetwork or null)
) (lib.attrValues serviceContainers); ) (lib.attrValues serviceContainers);