hyperhive/flake.nix
atlas c1ffa12a99 feat(#2280): packages.hivectl and packages.hive-forge — single-bin splits
Operators who only want hivectl or hive-forge can now add:
  inputs.hyperhive.packages.${system}.hivectl
  inputs.hyperhive.packages.${system}.hive-forge
to their environment.systemPackages without pulling the full
workspace (hive-c0re daemon, hive-ag3nt harness, etc.) into PATH.

Both reuse the shared cargoArtifacts so there is no extra compile
cost when both packages and the default workspace are built together.

packages.hivectl carries the shell completions and the wireguard-tools
wrapper identical to what packages.default ships for hivectl.
packages.hive-forge is a lean CLI-only build.
2026-07-10 21:31:38 +02:00

511 lines
24 KiB
Nix

{
description = "hyperhive multi-Claude-Code-agent orchestration on nixos-containers";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
# 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,
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 —
# 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: scripts/, docs/, root-level *.md (README/CLAUDE/TODO/…).
# Kept (build needs them): .nix, .rs, Cargo.*, branding/,
# frontend/, prompts/, flake.lock. docs/ is deliberately dropped:
# it is shipped to agent containers as its OWN narrow
# meta-flake input (`hyperhiveDocsSource`, below), threaded through
# hive-c0re → the meta flake → `hyperhive.docs.source`. Keeping docs/
# out of THIS source means a doc edit only re-hashes the docs input
# (a cheap re-link), not the whole flake source (which would rebuild
# every agent container). The old `nix/reference-docs.nix` build from
# `../docs` stays only for standalone `nix build .#reference-docs`
# from a full checkout — the meta path never evaluates it.
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 "scripts/" rel || rel == "scripts")
&& !(lib.hasPrefix "docs/" rel || rel == "docs")
&& !(type == "regular" && !lib.hasInfix "/" rel && lib.hasSuffix ".md" rel);
};
# The repo `docs/` tree as a standalone narrow source. Its
# store path moves ONLY on doc edits, decoupled from
# `hyperhiveFlakeSource`. hive-c0re threads this to the meta flake as
# the `hyperhive-docs` input (same pattern as `hyperhiveFlake`); the
# harness resolves `$HIVE_DOCS_DIR` from it via `hyperhive.docs.source`.
# Evaluated here on the host where docs/ exists — it cannot be derived
# from inside the docs-stripped `hyperhiveFlakeSource`.
hyperhiveDocsSource = lib.cleanSourceWith {
name = "hyperhive-docs-source";
src = ./docs;
};
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.
# `cmake` builds `aws-lc-sys` (BoringSSL) from source — pulled in by
# the `rustls` (aws-lc-rs) crypto provider under the OTLP/reqwest
# stack in `hive-metric`. Without it the crane deps build fails on
# `cargo-package-reqwest-0.13.4`. Kept in the shared inputs since
# more metric/crypto deps are expected to land in the workspace.
nativeBuildInputs = [
pkgs.git
pkgs.sqlite
pkgs.pkg-config
pkgs.cmake
];
}
);
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 <shell>` 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
'';
};
# Operator CLI only — ships `hivectl` (with shell completions
# and the `wg` wrapper) without the daemon binaries. Suitable
# for `nix profile install .#hivectl` / `environment.systemPackages
# = [ inputs.hyperhive.packages.${system}.hivectl ]` when the
# operator only wants the admin CLI on their workstation and
# does not want `hive-c0re` or `hive-ag3nt` in PATH.
# Shares `cargoArtifacts` with `packages.default` so there is no
# double-compile cost when both are in the same `nix build`
# invocation.
hivectl = craneLib.buildPackage {
src = cleanSrc;
inherit cargoArtifacts;
cargoExtraArgs = "--bin hivectl";
pname = "hivectl";
version = "0.1.0";
meta.description = "hyperhive operator CLI";
doCheck = false;
nativeBuildInputs = nativeBuildInputs ++ [
pkgs.installShellFiles
pkgs.makeWrapper
];
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
'';
};
# Forgejo CLI only — ships `hive-forge` without the rest of
# the workspace. Useful for operator workstations / CI
# environments that only need forge access. Shares
# `cargoArtifacts` with `packages.default`.
hive-forge = craneLib.buildPackage {
src = cleanSrc;
inherit cargoArtifacts;
cargoExtraArgs = "--bin hive-forge";
pname = "hive-forge";
version = "0.1.0";
meta.description = "hyperhive Forgejo CLI";
doCheck = false;
};
# 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 { };
# The repo docs/ markdown tree as a standalone derivation —
# agents read it in-container (added as a claude additional
# directory) and the website repo reuses it as a flake input,
# neither of which needs the branding/prompt assets. See
# nix/reference-docs.nix. (`docs` above is the auto-generated
# nix-options reference, a different artifact.)
reference-docs = pkgs.callPackage ./nix/reference-docs.nix { };
# XDG icon set + .desktop entries for hyperhive processes.
# Narrow input: only the branding SVG, so unrelated source changes
# don't bust this derivation's cache.
xdg-icons = pkgs.callPackage ./nix/packages/hive-xdg-icons.nix {
hyperhiveSvg = ./branding/hyperhive.svg;
};
# 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;
# Standalone docs/ tree (see nix/reference-docs.nix). Exposed
# via the overlay so the harness module can build the
# in-container agent docs dir from it.
hyperhive-docs = self.packages.${prev.stdenv.hostPlatform.system}.reference-docs;
};
};
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.<system>.default` via a `hyperhivePackage` argument.
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;
hyperhiveXdgIcons = system: self.packages.${system}.xdg-icons;
hyperhiveFlake = "${hyperhiveFlakeSource}";
# Narrow docs/ source, threaded as its own meta-flake input so
# doc edits don't re-hash the whole flake source.
hyperhiveDocs = "${hyperhiveDocsSource}";
# 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
];
}
];
};
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.<system>.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.<system>.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"
'';
}
);
};
}