{ description = "hyperhive — multi-Claude-Code-agent orchestration on nixos-containers"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; # Crane (replaces the former naersk-based build). Stateless — no nixpkgs input to # follow; `crane.mkLib pkgs` returns the lib at whatever pkgs we # pass it (we use the project's pinned nixpkgs). crane.url = "github:ipetkov/crane"; treefmt-nix = { url = "github:numtide/treefmt-nix"; inputs.nixpkgs.follows = "nixpkgs"; }; }; outputs = inputs@{ self, nixpkgs, nixpkgs-unstable, crane, treefmt-nix, }: let inherit (nixpkgs) lib; systems = [ "aarch64-linux" "x86_64-linux" ]; # Explicit per-consumer source filter for the hyperhive flake path # handed to hive-c0re (`hyperhiveFlake`, below). A filtered view of # this repo that drops files no nix or cargo derivation reads — # documentation, shell helper scripts, and root-level markdown — so # editing them does NOT change the store path and therefore does NOT # force a rebuild of every agent container. # # Companion to the rust `cleanSrc` (crane's cargo-source filter): # each real source is a named, explicitly-filtered derivation used # as `src`, rather than an inline filter at the use site. # # Dropped: docs/, scripts/, root-level *.md (README/CLAUDE/TODO/…). # Kept (build needs them): .nix, .rs, Cargo.*, branding/, frontend/, # prompts/, flake.lock. hyperhiveFlakeSource = lib.cleanSourceWith { name = "hyperhive-flake-source"; src = ./.; filter = path: type: let # Repo-relative path (strip the absolute source-dir prefix). rel = lib.removePrefix (toString ./. + "/") (toString path); in !(lib.hasPrefix "docs/" rel || rel == "docs") && !(lib.hasPrefix "scripts/" rel || rel == "scripts") && !(type == "regular" && !lib.hasInfix "/" rel && lib.hasSuffix ".md" rel); }; treefmt-config = { projectRootFile = "flake.nix"; programs = { keep-sorted.enable = true; nixfmt.enable = true; rustfmt.enable = true; taplo.enable = true; }; }; forAllSystems = f: lib.genAttrs systems ( system: f rec { inherit system; pkgs = nixpkgs.legacyPackages.${system}; treefmt-eval = treefmt-nix.lib.evalModule pkgs treefmt-config; craneLib = crane.mkLib pkgs; # Narrowed source tree the rust derivations consume. # `cleanCargoSource` is crane's standard "everything cargo # cares about" filter (Cargo.toml/Cargo.lock + *.rs). All # non-rust runtime assets — branding + the claude system # prompt template + claude-settings.json — live in the # separate `hyperhive-assets` derivation and are # loaded by the binaries at runtime from `$HIVE_ASSETS_DIR`. # The unit tests in `hive-ag3nt::prompt` read the same # `prompts/system.md` directly from the workspace tree at # *test* runtime (via `env!("CARGO_MANIFEST_DIR")` — a # compile-time string, no file open at compile), so the # prompt template doesn't have to be in this fileset to # keep `cargo test` honest. Net effect: tweaks to any # non-`*.rs` / non-`Cargo.*` file (README, branding, # nix modules, frontend tree, OR `hive-ag3nt/prompts/*`) # do NOT bust this src hash, so the rust derivations # stay fully cached. cleanSrc = craneLib.cleanCargoSource ./.; # Build the workspace's dependency tree once, cached as # its own derivation. `buildPackage` and `cargoClippy` # both reuse this via `inherit cargoArtifacts;` so a # workspace-only edit doesn't rebuild deps. All three # derivations consume the same `cleanSrc` so the input # hash stays consistent across the chain. cargoArtifacts = craneLib.buildDepsOnly { src = cleanSrc; # Workspace Cargo.toml is virtual (no `[package].name`), # so crane can't auto-derive a name. Spell it out # explicitly here and below — keeps the derivation name # stable across crane bumps + silences the placeholder # warning. Same `pname` everywhere so dep + workspace + # clippy share a clean naming family. pname = "hyperhive-workspace"; version = "0.1.0"; inherit nativeBuildInputs; }; # Shared between buildDepsOnly + buildPackage + cargoClippy # so the three derivations see the same toolchain shape. # git: naersk used to auto-include it; crane is more # minimal, so we add it explicitly so hive-c0re's # `lifecycle::tests::setup_proposed_*` (which shell out to # `git init` + commit) pass under `cargo test` in the # sandbox. # `librsvg` used to live here for `hive-c0re/build.rs`'s # rsvg-convert call — that whole codepath moved into the # `hyperhive-assets` derivation, so the rust # derivation no longer needs the dependency. # `sqlite` required by matrix-sdk's `sqlite` feature # (`hive-matrix-mcp` workspace member) — the # matrix-sdk-sqlite + rusqlite stack links against system # libsqlite3 by default. nativeBuildInputs = [ pkgs.git pkgs.sqlite pkgs.pkg-config ]; } ); in { packages = forAllSystems ( { pkgs, craneLib, cleanSrc, cargoArtifacts, nativeBuildInputs, ... }: let docsAttrs = import ./nix/docs { inherit pkgs self; inherit (nixpkgs) lib; inherit (nixpkgs.lib) nixosSystem; }; in { # Build the workspace binaries without running tests. Tests # are run as a separate check (`checks.cargo-test`) that # carries the `hyperhive-assets` build input — `hive-ag3nt:: # prompt::tests` reads the production prompt template at test # runtime through `$HIVE_ASSETS_DIR`, so wiring the env var # into the build phase here would make the prompt's hash a # build input of `default` (defeats the asset-split cache goal: a # prompt edit would still bust the binary derivation, even # though no .rs file changed). Keeping tests in a separate # check derivation localises the asset-rebuild blast radius # to that one check — `nix flake check` still exercises them. default = craneLib.buildPackage { src = cleanSrc; inherit cargoArtifacts; # `installShellFiles` provides `installShellCompletion` and # `makeWrapper` provides `wrapProgram` for the postInstall below; # appended (not in the shared set) so they're build inputs only of # this binary derivation. nativeBuildInputs = nativeBuildInputs ++ [ pkgs.installShellFiles pkgs.makeWrapper ]; pname = "hyperhive-workspace"; version = "0.1.0"; meta.description = "hyperhive workspace (hive-c0re, hive-ag3nt, hive-root)"; doCheck = false; # Ship hivectl shell completions in the package (the binary's own # `completions ` verb is the single source of truth, so they # never drift from the actual verbs). Lands at # `$out/share/{zsh/site-functions,bash-completion,fish}/…`; an # operator gets working completion as soon as hivectl is in their # system/user profile with the shell's completion enabled. # # Then wrap hivectl with `wireguard-tools` on PATH so its `wg` # subcommands (`wg init`/`peer`/`status`) work even before the # WireGuard mesh is configured — `wg init` is the *first* setup # step, run before `swarm.wireguard.enable` (which would otherwise # be what pulls wireguard-tools onto the system). Completion # generation runs first since wrapProgram renames the real binary. postInstall = '' installShellCompletion --cmd hivectl \ --bash <("$out/bin/hivectl" completions bash) \ --zsh <("$out/bin/hivectl" completions zsh) \ --fish <("$out/bin/hivectl" completions fish) wrapProgram "$out/bin/hivectl" \ --prefix PATH : ${pkgs.wireguard-tools}/bin ''; }; # Bundled browser assets — see ./nix/frontend.nix. Output is # $out/{dashboard,agent}/ which the Rust binaries serve via # tower_http::ServeDir. frontend = pkgs.callPackage ./nix/frontend.nix { branding-svg = ./branding/hyperhive.svg; }; # Static runtime assets the rust binaries read via # `hive_sh4re::assets::*`: branding/* + prompts/*, # plus the rendered agent-configs.png. Split out of the # rust derivation so a tweak to e.g. system.md doesn't bust # the cargo cache. Build input of the `cargo-test` check but # NOT of `packages.default`, so the binary derivation stays # cached when a prompt edit ripples through. assets = pkgs.callPackage ./nix/assets.nix { }; # Pre-built per-container system closures. Exposed as packages # so operators can `nix build .#agent-base-toplevel` (or wire # them into their host system closure via the # `preBuildAgentTemplates` option on the hive-c0re module — # see nix/modules/hive-c0re.nix). Speeds up the first agent # spawn dramatically because the heavy lifting (nixpkgs + # claude-code + hive-ag3nt binary) is already in the store # when the meta evaluator goes to build the container. # # nixosConfigurations are pinned to x86_64-linux (nixos- # containers only run native arch), so these toplevels are # only useful on an x86_64-linux host — flake check across # systems still tolerates evaluating them on aarch64 because # they're plain derivations, but `nix build` from a non-x86 # host would only succeed via a remote x86 builder. agent-base-toplevel = self.nixosConfigurations.agent-base.config.system.build.toplevel; ruth-toplevel = self.nixosConfigurations.ruth.config.system.build.toplevel; # Auto-generated nix options reference for hyperhive. # `docs` bundles host + agent pages into one tree; the split # outputs are useful when consumers only want one surface. # All three are pure markdown — no rust or frontend deps in # the closure, so `nix build .#docs` is cheap. docs = docsAttrs.bundle; docs-host = docsAttrs.host; docs-agent = docsAttrs.agent; } ); overlays = { default = final: prev: { hyperhive = self.packages.${prev.stdenv.hostPlatform.system}.default; # Bundled frontend dist (see ./nix/frontend.nix). Output is # $out/{dashboard,agent}/; consumers pick the surface they # need. Exposed via the overlay so containers' nix evaluations # can reach it as `pkgs.hyperhive-frontend` once the overlay # is applied (manager + agent containers both apply it via # `mkContainer` further down). hyperhive-frontend = self.packages.${prev.stdenv.hostPlatform.system}.frontend; # Static runtime assets. Exposed alongside the binary # so the harness module can wire $HIVE_ASSETS_DIR straight # to `${pkgs.hyperhive-assets}/share/hyperhive`. hyperhive-assets = self.packages.${prev.stdenv.hostPlatform.system}.assets; }; claude-unstable = final: prev: let # The overlay imports its own nixpkgs-unstable instance to # pin claude-code there. That instance has its own config # (independent from the user's prev.config), so we have to # set allowUnfreePredicate inline to whitelist claude-code # specifically — otherwise the unstable import itself # refuses to evaluate. This is scoped: only claude-code # bypasses unfree, nothing else. unstable = import nixpkgs-unstable { inherit (prev.stdenv.hostPlatform) system; config.allowUnfreePredicate = pkg: builtins.elem (prev.lib.getName pkg) [ "claude-code" ]; }; in { inherit (unstable) claude-code; }; }; nixosModules = { agent-base = ./nix/templates/agent-base.nix; ruth = ./nix/templates/manager.nix; # The hive-c0re module wants `pkgs.hyperhive` for its default # `services.hyperhive.c0re.package`. To avoid making operators apply an # overlay (which would also pollute their host pkgs with our # build), we thread the package straight from this flake's # `packages..default` via a `hyperhivePackage` argument. # The `claude-unstable` overlay only matters inside our container # builds (already applied internally in `nixosConfigurations`). hive-c0re = import ./nix/modules/hive-c0re.nix { hyperhivePackage = system: self.packages.${system}.default; hyperhiveFrontend = system: self.packages.${system}.frontend; hyperhiveAssets = system: self.packages.${system}.assets; hyperhiveFlake = "${hyperhiveFlakeSource}"; # Store path of the nixpkgs-unstable input this flake was evaluated # with — the channel that carries claude-code. Passed as the default # for `services.hyperhive.c0re.nixpkgsUnstableFlake` so operators can # override it without touching this file. hyperhiveNixpkgsUnstable = "path:${nixpkgs-unstable}"; # Per-container toplevels — wired into `system.extraDependencies` # when `services.hyperhive.c0re.preBuildAgentTemplates` is on so the # host system closure pre-fetches the heavy build inputs. # Defined only for x86_64-linux because nixosConfigurations are # hardcoded to that system; the option's default keeps the # extra deps gated so aarch64 hosts don't accidentally pull # them in via cross-build. agentBaseToplevel = self.packages.x86_64-linux.agent-base-toplevel; managerToplevel = self.packages.x86_64-linux.ruth-toplevel; }; hive-ci = ./nix/modules/hive-ci.nix; hive-forge = ./nix/modules/hive-forge.nix; # Convenience alias: one import covers the full hyperhive host # stack (hive-c0re + hive-forge, since hive-c0re already pulls # in hive-forge). Intended usage: # # imports = [ hyperhive.nixosModules.default ]; # services.hyperhive.enable = true; # default = self.nixosModules.hive-c0re; }; nixosConfigurations = let mkContainer = module: nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ module { nixpkgs.overlays = [ self.overlays.default self.overlays.claude-unstable ]; } ]; }; in { agent-base = mkContainer self.nixosModules.agent-base; ruth = mkContainer self.nixosModules.ruth; }; devShells = forAllSystems ( { pkgs, ... }: { default = pkgs.mkShell { packages = with pkgs; [ cargo clippy librsvg # rsvg-convert — hive-c0re/build.rs invokes it pkg-config rust-analyzer rustc rustfmt sqlite ]; }; } ); formatter = forAllSystems ({ treefmt-eval, ... }: treefmt-eval.config.build.wrapper); checks = forAllSystems ( { pkgs, system, treefmt-eval, craneLib, cleanSrc, cargoArtifacts, nativeBuildInputs, ... }: { formatting = treefmt-eval.config.build.check self; # Clippy as a check via crane's first-class `cargoClippy` # builder. Reuses the shared `cargoArtifacts` (deps already # built) and runs `cargo clippy --workspace --all-targets` # directly — no `overrideAttrs` hack needed, because crane # parses `cargoClippyExtraArgs` correctly (naersk's # `mode = "clippy"` used to mangle the `--` separator, which # is why the old wiring went through overrideAttrs). # # `-D warnings` makes the default/correctness/style lints a # hard CI gate. `-A clippy::pedantic` then drops the pedantic # group from that gate: pedantic is the "extra, opinionated" # group the clippy team grows freely, so denying it means # every toolchain bump that adds a new pedantic lint breaks CI # with zero code changes. The `pedantic = warn` # workspace lint (Cargo.toml) keeps it as advisory signal in # local `cargo clippy` — it just no longer blocks the build. # (`-A` rather than `-W` here: `-W clippy::pedantic` would # re-enable the specific pedantic lints the workspace lints # table allows, e.g. `must_use_candidate`.) clippy = craneLib.cargoClippy { src = cleanSrc; inherit cargoArtifacts nativeBuildInputs; pname = "hyperhive-workspace"; version = "0.1.0"; cargoClippyExtraArgs = "--workspace --all-targets -- -D warnings"; }; # `cargo test --workspace` lifted out of `buildPackage` so the # `hyperhive-assets` dep (which `hive-ag3nt::prompt::tests` # needs via `HIVE_ASSETS_DIR` to assert against the actual # production prompt template) is scoped to this one check # instead of bleeding into the binary derivation's input # hash. Net: editing `hive-ag3nt/prompts/system.md` still # rebuilds this test check (correct — the tests assert # against its wording), but `packages.default` and the # per-container toplevels stay fully cached. cargo-test = craneLib.cargoTest { src = cleanSrc; inherit cargoArtifacts nativeBuildInputs; pname = "hyperhive-workspace"; version = "0.1.0"; cargoTestExtraArgs = "--workspace"; HIVE_ASSETS_DIR = "${self.packages.${system}.assets}/share/hyperhive"; }; # Nix options docs evaluation. Cheap: pulls in # `nixosOptionsDoc` + the host module's stub eval, no rust or # frontend deps. CI fails fast if a module change breaks # option declarations or the doc rendering. Reuses the # `packages..docs` derivation so the per-system eval # of `nix/docs/default.nix` happens once. inherit (self.packages.${system}) docs; # `hivectl` CLI reference freshness check. The committed # markdown at `docs/tools/hivectl-cli.md` is the rendered # output of the hidden `hivectl markdown-docs` subcommand # (clap-markdown walks the binary's own command tree). This # check regenerates it from the built binary and fails if the # committed copy drifted — so a verb / flag / help-string edit # that forgets to refresh the doc is caught in CI. Reuses the # already-built `packages..default` (no extra compile). # Regenerate locally with: # nix build .#default # ./result/bin/hivectl markdown-docs > docs/tools/hivectl-cli.md hivectl-docs = pkgs.runCommand "hivectl-docs-fresh" { nativeBuildInputs = [ pkgs.diffutils ]; } '' ${self.packages.${system}.default}/bin/hivectl markdown-docs > generated.md if ! diff -u ${./docs/tools/hivectl-cli.md} generated.md; then echo "" >&2 echo "ERROR: docs/tools/hivectl-cli.md is out of date — regenerate it:" >&2 echo " nix build .#default && ./result/bin/hivectl markdown-docs > docs/tools/hivectl-cli.md" >&2 exit 1 fi touch "$out" ''; } ); }; }