From d24dbaeadfbccd129fcb5f819d4847e80226fdda Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 14 Oct 2026 22:33:57 +0200 Subject: [PATCH] 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. --- docs/swarm/services.md | 8 +++-- docs/trust-boundary/security.md | 3 +- nix/host-modules/deploy.nix | 14 ++++---- nix/host-modules/swarm-builder.nix | 3 +- nix/host-modules/swarm-required-services.nix | 9 +++++ nix/module-eval/builder.nix | 20 +++++++---- nix/module-eval/swarm-services-switch.nix | 37 +++++++++++++++----- 7 files changed, 68 insertions(+), 26 deletions(-) diff --git a/docs/swarm/services.md b/docs/swarm/services.md index 3a097d7c..54a4bc2b 100644 --- a/docs/swarm/services.md +++ b/docs/swarm/services.md @@ -268,9 +268,11 @@ the container and the threat model are in ### Builder The swarm builder, in a `swarm-builder` container on the host that sets -`deploy.builder.enable`. It doesn't derive from `allSwarmServices`: it -evaluates and builds unreviewed config PRs through its host's nix daemon, so -running one is a decision of its own, as for the CI runner. +`deploy.builder.enable`. `allSwarmServices` defaults it on wherever the host +has the builder's store identity, as for the review classifier below: the host +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/` PR opens, reopens or gets a new push, swarm-controller queues a `build_agent` job. Jobs run one at a time, and a diff --git a/docs/trust-boundary/security.md b/docs/trust-boundary/security.md index 7348cd68..7f63df48 100644 --- a/docs/trust-boundary/security.md +++ b/docs/trust-boundary/security.md @@ -133,7 +133,8 @@ checkpoints**, not about sandboxing the agent from its own tools: scheduled prompts are operator-only, set at swarm level (see [`schedules.md`](../swarm/schedules.md)). 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 client of its host's nix daemon (see [`builder.md`](../swarm/builder.md)). diff --git a/nix/host-modules/deploy.nix b/nix/host-modules/deploy.nix index c555e92b..a8c72b17 100644 --- a/nix/host-modules/deploy.nix +++ b/nix/host-modules/deploy.nix @@ -694,13 +694,13 @@ in this host's nix daemon, and swarm-controller writes the result as a `swarm-builder` commit status on the PR. - Not derived from - {option}`services.hyperhive.deploy.allSwarmServices`: the builder - evaluates and builds unreviewed config PRs on this host's nix daemon, - so running one is a decision of its own, as for the CI runner. It needs - the builder's store identity - ({option}`services.hyperhive.deploy.bao.builderClientCertFile` and - `builderClientKeyFile`), which the store's host mints. + On by default where + {option}`services.hyperhive.deploy.allSwarmServices` is set and this + host has the builder's store identity (the pair + {option}`services.hyperhive.deploy.bao.builderClientCertFile` and + `builderClientKeyFile`, minted on the store's host). It evaluates + unreviewed config PRs and builds them through this host's nix daemon, + from a container with its own network namespace. ''; }; diff --git a/nix/host-modules/swarm-builder.nix b/nix/host-modules/swarm-builder.nix index 59def0b5..a4aa0f4f 100644 --- a/nix/host-modules/swarm-builder.nix +++ b/nix/host-modules/swarm-builder.nix @@ -1,5 +1,6 @@ # 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 # 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 diff --git a/nix/host-modules/swarm-required-services.nix b/nix/host-modules/swarm-required-services.nix index 31097135..aa5162d0 100644 --- a/nix/host-modules/swarm-required-services.nix +++ b/nix/host-modules/swarm-required-services.nix @@ -117,4 +117,13 @@ in && deployCfg.bao.reviewClassifierClientCertFile != 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 + ); } diff --git a/nix/module-eval/builder.nix b/nix/module-eval/builder.nix index dd2f54b2..c552bc0b 100644 --- a/nix/module-eval/builder.nix +++ b/nix/module-eval/builder.nix @@ -32,6 +32,10 @@ let }; 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 { swarm.nats.enable = true; @@ -54,13 +58,17 @@ let ok = !(bare.containers ? swarm-builder) && withBuilder.containers ? swarm-builder; } { - # It runs unreviewed config PRs through the host's nix daemon, so hosting - # the shared services, even with the store's leaf here, does not turn it - # on. `nats` is the control that the switch is on at all. - name = "hosting the shared services leaves the builder off"; + # The store's host mints the builder's leaf, so the switch needs no + # manual identity step: the pair is set and the module's identity + # assertion, which nothing else reads in this fixture, passes. `nats` is + # the control that the switch is on. + name = "hosting the shared services turns the builder on, with its store identity"; ok = - !servicesHere.services.hyperhive.deploy.builder.enable - && !(servicesHere.containers ? swarm-builder) + servicesHere.services.hyperhive.deploy.builder.enable + && 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.nats.enable; } diff --git a/nix/module-eval/swarm-services-switch.nix b/nix/module-eval/swarm-services-switch.nix index c2430f99..fb7b38c4 100644 --- a/nix/module-eval/swarm-services-switch.nix +++ b/nix/module-eval/swarm-services-switch.nix @@ -90,10 +90,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 { + # Same, with the operator opting out of the classifier and the builder, + # which the switch defaults on and everything else it owns stays on. + swarmServicesOptedOut = hiveWithController { deploy.allSwarmServices = true; deploy.review-classifier.enable = false; + deploy.builder.enable = false; }; # 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; # 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 - # three containers on a netns of their own. + # runner. The CI runner, the review classifier, the builder and the metrics + # store are the four containers on a netns of their own. serviceContainersWithCi = hive { deploy.hive-controller.enable = false; deploy.allSwarmServices = true; @@ -212,9 +214,19 @@ let # 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; + !swarmServicesOptedOut.services.hyperhive.deploy.review-classifier.enable + && !(swarmServicesOptedOut.containers ? swarm-review-classifier) + && 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 @@ -226,6 +238,15 @@ let && !(swarmServicesBaoElsewhere.containers ? swarm-review-classifier) && 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 # 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. name = "every service container restates its host-side privateNetwork"; ok = - lib.length (lib.attrNames serviceContainers) == 11 + lib.length (lib.attrNames serviceContainers) == 12 && lib.all ( c: c.privateNetwork == (c.config.services.hyperhive.swarmContainer.privateNetwork or null) ) (lib.attrValues serviceContainers);