diff --git a/docs/swarm/builder.md b/docs/swarm/builder.md index a70b90de..f918b1f2 100644 --- a/docs/swarm/builder.md +++ b/docs/swarm/builder.md @@ -26,6 +26,10 @@ Where it runs and how it's enabled: [`services.md`](services.md#builder). - `hyperhive` and `nixpkgs`: the sources the builder's own host was deployed with, the same two a hive on that host renders into its meta flake (`c0re.hyperhiveFlake`, `c0re.nixpkgsFlake`). + - `hyperhive-docs`: the `docs/` source a hive on that host renders into its + meta flake (`c0re.hyperhiveDocs`). The check sets + `services.hyperhive.agent.docs.source` from it, as the meta flake does, + since the `hyperhive` source has no `docs/`. - `agent`: `git+https:///agent-configs/?ref=refs/pull//head&rev=`. A build has one hour. 4. The reply is the system closure's store path, or the last lines `nix` @@ -48,7 +52,7 @@ Where it runs and how it's enabled: [`services.md`](services.md#builder). - It composes the config the way a hive's meta flake does (`agent-base.extendModules` with the config's `nixosModules.default`), minus - what a hive adds: its CA bundle, docs source, `claude-code` path, ports and + what a hive adds: its CA bundle, `claude-code` path, ports and OTEL settings, and the `follows` that point the config's own inputs at the hive's. A green check can therefore still fail on a hive, and the reverse. - The agent base is the builder's host's, not the hive's. Hives on another diff --git a/nix/builder/check-flake/flake.nix b/nix/builder/check-flake/flake.nix index 0f769417..f39eab56 100644 --- a/nix/builder/check-flake/flake.nix +++ b/nix/builder/check-flake/flake.nix @@ -1,6 +1,8 @@ # The swarm builder's build check: one agent config on hyperhive's agent base, # composed the way a hive's meta flake composes it, minus the values a hive -# adds (its CA, docs source, claude-code path and ports). +# adds (its CA, claude-code path and ports). The docs source is set the way the +# meta flake sets it: `hyperhive`'s source has no `docs/`, so the default +# `docs.source` fails to evaluate. # # Every input is replaced at build time with `--override-input` # (swarm-builder/src/nix_build.rs), so the URLs below are never fetched and @@ -12,15 +14,25 @@ nixpkgs.url = "flake:nixpkgs"; hyperhive.url = "flake:hyperhive"; hyperhive.inputs.nixpkgs.follows = "nixpkgs"; + hyperhive-docs.url = "flake:hyperhive-docs"; + hyperhive-docs.flake = false; agent.url = "flake:agent"; }; outputs = - { hyperhive, agent, ... }: + { + hyperhive, + hyperhive-docs, + agent, + ... + }: { packages.x86_64-linux.check = (hyperhive.nixosConfigurations.agent-base.extendModules { - modules = [ agent.nixosModules.default ]; + modules = [ + agent.nixosModules.default + { services.hyperhive.agent.docs.source = hyperhive-docs.outPath; } + ]; }).config.system.build.toplevel; }; } diff --git a/nix/host-modules/swarm-builder.nix b/nix/host-modules/swarm-builder.nix index 3518b1d8..1cfabbc8 100644 --- a/nix/host-modules/swarm-builder.nix +++ b/nix/host-modules/swarm-builder.nix @@ -89,10 +89,11 @@ let # The `nix` the host daemon runs, so client and daemon speak one protocol. nix = config.nix.package; - # Strings, not paths: both are already store paths, and the flake reference - # the daemon passes must be the same one a hive's meta flake names. + # Strings, not paths: all three are already store paths, and the flake + # references the daemon passes must be the ones a hive's meta flake names. hyperhiveFlake = hyperhiveCfg.c0re.hyperhiveFlake; nixpkgsFlake = hyperhiveCfg.c0re.nixpkgsFlake; + docsFlake = hyperhiveCfg.c0re.hyperhiveDocs; privateNetwork = true; in @@ -253,6 +254,7 @@ in SWARM_BUILDER_CHECK_FLAKE = "${../builder/check-flake}"; SWARM_BUILDER_HYPERHIVE_FLAKE = hyperhiveFlake; SWARM_BUILDER_NIXPKGS_FLAKE = nixpkgsFlake; + SWARM_BUILDER_HYPERHIVE_DOCS_FLAKE = docsFlake; SWARM_BUILDER_FORGE_URL = "https://${forgeCfg.domain}"; SWARM_BUILDER_CONFIG_ORG = "agent-configs"; } diff --git a/nix/module-eval/builder.nix b/nix/module-eval/builder.nix index 42b2aa87..5073b95f 100644 --- a/nix/module-eval/builder.nix +++ b/nix/module-eval/builder.nix @@ -49,6 +49,30 @@ let svc = inner.systemd.services.swarm-builder; env = svc.environment; + # The check flake on the inputs the builder's unit names, called as + # `nix build` calls it. `hyperhive` is entered through the filtered source's + # own `flake.nix`, so its agent base sees the tree without `docs/` that a + # real build gets; `self`'s own agent base would find `docs/` here. + builderCheck = + agentModule: + let + src = env.SWARM_BUILDER_HYPERHIVE_FLAKE; + hyperhive = (import "${src}/flake.nix").outputs ( + self.inputs + // { + self = hyperhive // { + outPath = src; + inherit (self) inputs; + }; + } + ); + in + ((import "${env.SWARM_BUILDER_CHECK_FLAKE}/flake.nix").outputs { + inherit hyperhive; + hyperhive-docs.outPath = env.SWARM_BUILDER_HYPERHIVE_DOCS_FLAKE; + agent.nixosModules.default = agentModule; + }).packages.x86_64-linux.check; + cases = [ { name = "the swarm builder's container exists only where its host enables it"; @@ -113,9 +137,10 @@ let { # The check builds on the sources a hive on this host would render into # its meta flake, so a green check means the same base. - name = "the builder builds on the hyperhive and nixpkgs this host's hives use"; + name = "the builder builds on the hyperhive, docs and nixpkgs this host's hives use"; ok = env.SWARM_BUILDER_HYPERHIVE_FLAKE == withBuilder.services.hyperhive.c0re.hyperhiveFlake + && env.SWARM_BUILDER_HYPERHIVE_DOCS_FLAKE == withBuilder.services.hyperhive.c0re.hyperhiveDocs && env.SWARM_BUILDER_NIXPKGS_FLAKE == withBuilder.services.hyperhive.c0re.nixpkgsFlake && lib.hasSuffix "-check-flake" env.SWARM_BUILDER_CHECK_FLAKE && env.SWARM_BUILDER_CONFIG_ORG == "agent-configs" @@ -150,6 +175,18 @@ let && lib.elem "execution" reviewer && !(lib.elem "meta" reviewer); } + { + # `docs.enable` reads `docs.source`, whose default builds from the + # `docs/` the `hyperhive` source has none of; an eval error there fails + # this whole group. + name = "the builder's check evaluates a config with the reference docs on"; + ok = + lib.hasSuffix ".drv" + (builderCheck { + services.hyperhive.agent.user.name = "a1"; + services.hyperhive.agent.docs.enable = true; + }).drvPath; + } { # The responder denies a client id it does not know, and a NATS denial # arrives as a timeout. diff --git a/swarm-builder/README.md b/swarm-builder/README.md index 6892a178..5de45514 100644 --- a/swarm-builder/README.md +++ b/swarm-builder/README.md @@ -12,9 +12,10 @@ the answer into a `swarm-builder` commit status on the PR's head. ## What a build is -- `nix build` of `nix/builder/check-flake`'s `check` output, with its three - inputs overridden: `hyperhive` and `nixpkgs` to this host's own sources, and - `agent` to `git+https:///agent-configs/?ref=refs/pull//head&rev=`. +- `nix build` of `nix/builder/check-flake`'s `check` output, with its four + inputs overridden: `hyperhive` and `nixpkgs` to this host's own sources, + `hyperhive-docs` to the `docs/` source its hives use (`c0re.hyperhiveDocs`), + and `agent` to `git+https:///agent-configs/?ref=refs/pull//head&rev=`. - The evaluation runs here, pure; the builds run in the host daemon's sandbox as an untrusted client, so a config can neither set daemon options nor read this service's environment. diff --git a/swarm-builder/src/main.rs b/swarm-builder/src/main.rs index 7809e900..b46b6fe7 100644 --- a/swarm-builder/src/main.rs +++ b/swarm-builder/src/main.rs @@ -49,6 +49,7 @@ async fn main() -> Result<()> { check_flake: var("CHECK_FLAKE")?, hyperhive: var("HYPERHIVE_FLAKE")?, nixpkgs: var("NIXPKGS_FLAKE")?, + hyperhive_docs: var("HYPERHIVE_DOCS_FLAKE")?, forge_url: forge_url()?, config_org: var("CONFIG_ORG")?, deadline: BUILD_DEADLINE, diff --git a/swarm-builder/src/nix_build.rs b/swarm-builder/src/nix_build.rs index 1fd17015..099a28de 100644 --- a/swarm-builder/src/nix_build.rs +++ b/swarm-builder/src/nix_build.rs @@ -1,5 +1,6 @@ -//! One build: `nix build` of the check flake with its three inputs pointed at -//! this host's hyperhive and nixpkgs and at the agent's config commit. +//! One build: `nix build` of the check flake with its four inputs pointed at +//! this host's hyperhive, hyperhive docs and nixpkgs and at the agent's config +//! commit. //! //! The config repo is private, so `git` fetches it with the builder's forge //! token. The token reaches `git` through a credential helper that reads it @@ -44,6 +45,8 @@ pub struct Builder { pub hyperhive: String, /// The nixpkgs source this host was deployed from. pub nixpkgs: String, + /// The hyperhive `docs/` source this host was deployed from. + pub hyperhive_docs: String, /// `https://`, with no trailing slash and no path. pub forge_url: String, /// The org the config repos are in. @@ -99,6 +102,9 @@ impl Builder { "nixpkgs".to_owned(), self.nixpkgs.clone(), "--override-input".to_owned(), + "hyperhive-docs".to_owned(), + self.hyperhive_docs.clone(), + "--override-input".to_owned(), "agent".to_owned(), self.agent_url(job), ]) @@ -302,6 +308,7 @@ mod tests { check_flake: "/nix/store/c-check".to_owned(), hyperhive: "/nix/store/h-source".to_owned(), nixpkgs: "/nix/store/n-source".to_owned(), + hyperhive_docs: "/nix/store/d-docs-source".to_owned(), forge_url: "https://forge.example".to_owned(), config_org: "agent-configs".to_owned(), nix: PathBuf::from("nix"), @@ -326,7 +333,14 @@ mod tests { .filter(|w| w[0] == "--override-input") .map(|w| w[1].as_str()) .collect(); - assert_eq!(overridden, ["hyperhive", "nixpkgs", "agent"]); + assert_eq!( + overridden, + ["hyperhive", "nixpkgs", "hyperhive-docs", "agent"] + ); + assert!( + args.windows(2) + .any(|w| w[0] == "hyperhive-docs" && w[1] == "/nix/store/d-docs-source") + ); assert!(args.iter().any(|a| a == "/nix/store/c-check#check")); assert!(args.iter().any(|a| a == "--no-write-lock-file")); }