diff --git a/docs/agent-lifecycle/persistence.md b/docs/agent-lifecycle/persistence.md index 294a30a8..a35e7ac4 100644 --- a/docs/agent-lifecycle/persistence.md +++ b/docs/agent-lifecycle/persistence.md @@ -521,7 +521,7 @@ leftover `.git` here; `migrate::run` deletes it at startup. Contents: the capped party never sets them. It sees only the resulting memory cap, as a byte count or `infinity` in its runtime dir (below). Fallback is per *field*: an absent file, absent agent, or absent field falls back to the - hive-wide `services.hyperhive.c0re.agentCpuQuota` / `agentMemoryMax`, + hive-wide `services.hyperhive.deploy.hive-controller.agentCpuQuota` / `agentMemoryMax`, so an agent can have only its memory capped and still track the hive default for CPU. Every value passes the validators first, and one bad value makes the worker change nothing here. The `CPUWeight=` / `IOWeight=` diff --git a/docs/networking/gateway.md b/docs/networking/gateway.md index 3decc6c5..4920a106 100644 --- a/docs/networking/gateway.md +++ b/docs/networking/gateway.md @@ -324,7 +324,7 @@ c0re process never leaves a partial or unparseable file behind. ### Per-agent static frontend split hive-c0re's environment sets `HIVE_AGENT_FRONTEND_DIR` to the `agent/` -subdirectory of `services.hyperhive.c0re.servedFrontend`. +subdirectory of `services.hyperhive.deploy.hive-controller.servedFrontend`. The nginx include generator (`gateway_nginx::write`) reads this variable and, when set, emits split location blocks per agent instead of a single proxy block. @@ -386,7 +386,7 @@ agent gets a single proxy block and nginx forwards all traffic to the agent daemon. **`extraFiles`**: per-agent `services.hyperhive.agent.frontend.extraFiles` sit in -`mergedDist`, not in the base `services.hyperhive.c0re.frontend` dist. They're not under +`mergedDist`, not in the base `services.hyperhive.deploy.hive-controller.frontend` dist. They're not under the nix-store `alias` path, so requests for them fall through `try_files` to `@_dynamic`, and the agent daemon serves them. diff --git a/docs/process/gotchas.md b/docs/process/gotchas.md index 747eabec..1e575c54 100644 --- a/docs/process/gotchas.md +++ b/docs/process/gotchas.md @@ -537,7 +537,7 @@ The fix (`nix/docs/default.nix`): file contents only. Docs drvs only change when a `.nix` file changes. 2. The package options the modules consume (`services.hyperhive.agent.packages.*`, - `services.hyperhive.c0re.*`) carry no in-module defaults and every + `services.hyperhive.deploy.hive-controller.*`) carry no in-module defaults and every default that references them has a `defaultText`, so the doc walk never forces a package — no stubs needed, and the Rust/frontend build closure stays out of the eval. diff --git a/docs/scheduler/coordinator.md b/docs/scheduler/coordinator.md index da68f554..665aa52a 100644 --- a/docs/scheduler/coordinator.md +++ b/docs/scheduler/coordinator.md @@ -172,7 +172,7 @@ resources are free. Resources: -1. **Build slots** — `services.hyperhive.c0re.buildSlots` permits (default 1), +1. **Build slots** — `services.hyperhive.deploy.hive-controller.buildSlots` permits (default 1), held by nix-heavy nodes for the node's duration. 2. **Per-agent lifecycle lease** — keyed on the **node's** agent (agent is per-node; a DAG can span agents) and globally exclusive per agent across @@ -391,7 +391,7 @@ or the flake root directly — none of which exist in the rendered meta flake. ## Host-level resource + performance options -A handful of `services.hyperhive.c0re.*` options tune container resource +A handful of `services.hyperhive.deploy.hive-controller.*` options tune container resource limits, build parallelism, and first-spawn latency. ### Build slots @@ -498,12 +498,12 @@ v2 **relative shares** that only decide who yields _under contention_. A low-weight container still gets the whole machine when nothing else wants it. -| Option | Default | Description | -| ---------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| `services.hyperhive.c0re.agentCpuQuota` | `"200%"` | CPU cap per agent, as a percentage of one core (`"200%"` = 2 cores). Raise if agents hit CPU limits during builds or heavy tool use. | -| `services.hyperhive.c0re.agentMemoryMax` | `"4G"` | Memory cap per agent. Raise for agents that run large nix builds or hold big in-memory data. | -| `services.hyperhive.c0re.agentCpuWeight` | `80` | `cpu.weight` share per agent, `1`–`10000` or `null` to omit the setting. Kernel default is `100`, so `80` makes agents yield. | -| `services.hyperhive.c0re.agentIoWeight` | `80` | `io.weight` share per agent, same range and `null` handling. See the caveat below — it's a no-op on many hosts. | +| Option | Default | Description | +| ---------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `services.hyperhive.deploy.hive-controller.agentCpuQuota` | `"200%"` | CPU cap per agent, as a percentage of one core (`"200%"` = 2 cores). Raise if agents hit CPU limits during builds or heavy tool use. | +| `services.hyperhive.deploy.hive-controller.agentMemoryMax` | `"4G"` | Memory cap per agent. Raise for agents that run large nix builds or hold big in-memory data. | +| `services.hyperhive.deploy.hive-controller.agentCpuWeight` | `80` | `cpu.weight` share per agent, `1`–`10000` or `null` to omit the setting. Kernel default is `100`, so `80` makes agents yield. | +| `services.hyperhive.deploy.hive-controller.agentIoWeight` | `80` | `io.weight` share per agent, same range and `null` handling. See the caveat below — it's a no-op on many hosts. | Two things to know about the weights: diff --git a/docs/swarm/builder.md b/docs/swarm/builder.md index 89350563..562875a8 100644 --- a/docs/swarm/builder.md +++ b/docs/swarm/builder.md @@ -136,7 +136,7 @@ with the `hive-meta-flake` renderer, all over `https`. Each custom agent that a hive places pins to its `agent-configs/` `main`; the controller omits and logs one whose repo has no commit on `main`, so it doesn't block the rest. Hyperhive pins to one rev, a ratchet on the controller's own -running rev (`services.hyperhive.c0re.hyperhiveRev`, from `self.rev`): +running rev (`services.hyperhive.deploy.hive-controller.hyperhiveRev`, from `self.rev`): - the first commit pins hyperhive's forge `main`; - later, the pin moves to the controller's rev only when the forge's compare diff --git a/docs/tools/hivectl.md b/docs/tools/hivectl.md index 379d49ed..5bc426a2 100644 --- a/docs/tools/hivectl.md +++ b/docs/tools/hivectl.md @@ -4,7 +4,7 @@ host (not inside any container). It talks to `hive-c0re` over the host admin socket `/run/hyperhive/host.sock`, which is root-only by default — so it needs `sudo` unless you grant sudoless access by listing your -login user in `services.hyperhive.c0re.adminUsers` (adds you to the +login user in `services.hyperhive.deploy.hive-controller.adminUsers` (adds you to the `hive-admin` group that owns the socket; see [`docs/trust-boundary/boundary.md`](../trust-boundary/boundary.md#host-admin-socket-access-hivectl)). Available via the `hive-c0re` package in the host NixOS config. diff --git a/docs/trust-boundary/boundary.md b/docs/trust-boundary/boundary.md index 0f401ada..71c8a776 100644 --- a/docs/trust-boundary/boundary.md +++ b/docs/trust-boundary/boundary.md @@ -158,7 +158,7 @@ boundary. By default the socket is `0660` group-owned by **`hive-admin`**, an empty group — so it's effectively **root-only** until an operator is explicitly granted access. Grant sudoless `hivectl` by listing login -users in `services.hyperhive.c0re.adminUsers`; the option adds each to +users in `services.hyperhive.deploy.hive-controller.adminUsers`; the option adds each to `hive-admin`, and members connect without `sudo`. The runtime dir `/run/hyperhive` is `0751` (traverse-only, no listing) so the group can reach the socket path; the socket's own `0660 hive-admin` mode gates diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 58806c6c..6e4b171d 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -234,7 +234,7 @@ library. The cost figure is a deliberately rough estimate from a per-model price table (`est_cost_usd`); it drifts with list pricing and the page labels it accordingly. The table is operator-tunable via the -`services.hyperhive.c0re.modelPrices` nix option — each key is a +`services.hyperhive.deploy.hive-controller.modelPrices` nix option — each key is a model-family short name (matched case-insensitively as a substring of the model id, longest match wins) mapping to `{ input, output, cache_read, cache_write }` USD-per-million-token diff --git a/flake.nix b/flake.nix index 813c77db..b99438ec 100644 --- a/flake.nix +++ b/flake.nix @@ -115,7 +115,7 @@ ]; # The full host stack (nix/host-modules/default.nix aggregator) plus # the package/source wiring from this flake. The wiring is a - # plain config module setting the `services.hyperhive.c0re.*` + # plain config module setting the `services.hyperhive.deploy.hive-controller.*` # package options via `lib.mkDefault` — no overlay involved, and # an operator override still wins. Intended usage: # @@ -126,16 +126,16 @@ { lib, pkgs, ... }: { imports = [ ./nix/host-modules ]; - services.hyperhive.c0re = { + services.hyperhive.c0re.hyperhiveFlake = lib.mkDefault "${sources.hyperhiveFlakeSource}"; + services.hyperhive.deploy.hive-controller = { package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.default; frontend = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.frontend; assets = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.assets; xdgIcons = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.xdg-icons; - hyperhiveFlake = lib.mkDefault "${sources.hyperhiveFlakeSource}"; hyperhiveRev = lib.mkDefault (self.rev or null); # The agent-base toplevel every container builds from — wired into # `system.extraDependencies` when - # `services.hyperhive.c0re.preBuildAgentTemplates` is on so + # `services.hyperhive.deploy.hive-controller.preBuildAgentTemplates` is on so # the host system closure pre-fetches the heavy build # inputs. x86_64-linux only (nixosConfigurations are # hardcoded to that system); the gate keeps aarch64 hosts diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index 70a76f73..c3cc36bc 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -231,7 +231,7 @@ pub struct ServeConfig { /// Number of concurrent nix-heavy job-queue nodes (prebuild / /// profile-swap / create / meta lock). hive-c0re-local like /// `model_prices` — never injected into containers. Set via - /// `services.hyperhive.c0re.buildSlots`. + /// `services.hyperhive.deploy.hive-controller.buildSlots`. pub build_slots: usize, } diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 2e962bbd..ff32c531 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -117,7 +117,7 @@ enum Cmd { model_prices: Option, /// Override: number of concurrent nix-heavy job-queue nodes /// (prebuild / profile-swap / create / meta lock). Set via the - /// `services.hyperhive.c0re.buildSlots` NixOS option. + /// `services.hyperhive.deploy.hive-controller.buildSlots` NixOS option. #[arg(long)] build_slots: Option, }, diff --git a/hive-meta-flake/src/lib.rs b/hive-meta-flake/src/lib.rs index e48f08ed..9acd2d7a 100644 --- a/hive-meta-flake/src/lib.rs +++ b/hive-meta-flake/src/lib.rs @@ -38,7 +38,7 @@ pub struct AgentPin { pub fn pinned_git_url(forge_git_url: &str, rev: Option<&str>) -> Result { let rev = rev.filter(|r| !r.is_empty()).context( "hyperhive rev unknown: the host was built from a dirty tree \ - (`self.rev` unset) and `services.hyperhive.c0re.hyperhiveRev` is not set", + (`self.rev` unset) and `services.hyperhive.deploy.hive-controller.hyperhiveRev` is not set", )?; Ok(format!("git+{forge_git_url}?rev={rev}")) } diff --git a/hive-priv/README.md b/hive-priv/README.md index 033c1548..45f2b27b 100644 --- a/hive-priv/README.md +++ b/hive-priv/README.md @@ -60,7 +60,7 @@ bare string `.tmp` in whatever the caller's cwd happens to be. action_ in an exclusive `flock` before calling it, but `update` used to call it with no lock whatsoever — so `create`'s lock never protected against a concurrent `update` clobbering the same `.tmp`. hive-priv never -sets a per-call cwd, so with `services.hyperhive.c0re.buildSlots > 1`, +sets a per-call cwd, so with `services.hyperhive.deploy.hive-controller.buildSlots > 1`, two concurrent calls (any mix of `create`/`update`) could share that one `.tmp`: one's `readlink(".tmp")` resolving to the _other's_ build output, handing an agent's container the wrong agent's closure — the diff --git a/hivectl/src/client.rs b/hivectl/src/client.rs index 54a434f1..0c2acd74 100644 --- a/hivectl/src/client.rs +++ b/hivectl/src/client.rs @@ -27,7 +27,8 @@ fn connect_hint(kind: ErrorKind, socket: &Path) -> String { ErrorKind::PermissionDenied => format!( "permission denied opening the host admin socket {path} — reaching it requires \ membership in the `hive-admin` group. Add your user to \ - `services.hyperhive.adminUsers`, then log out and back in: secondary group \ + `services.hyperhive.deploy.hive-controller.adminUsers`, then log out and back \ + in: secondary group \ membership is applied at login, so a shell opened before the change still \ cannot connect. `id -nG` shows what the running shell actually has; \ `newgrp hive-admin` picks the group up without a full re-login" @@ -128,7 +129,10 @@ mod tests { fn permission_denied_names_the_group_and_the_relogin() { let h = hint(ErrorKind::PermissionDenied); assert!(h.contains("hive-admin"), "{h}"); - assert!(h.contains("adminUsers"), "{h}"); + assert!( + h.contains("services.hyperhive.deploy.hive-controller.adminUsers"), + "{h}" + ); assert!(h.contains("log out"), "{h}"); assert!( !h.contains("not running"), diff --git a/nix/docs/default.nix b/nix/docs/default.nix index c8c0c028..20ca0360 100644 --- a/nix/docs/default.nix +++ b/nix/docs/default.nix @@ -35,7 +35,7 @@ let # heavy build inputs (matrix container, forge, etc.) stay out of # the eval — only option *declarations* matter for the doc walk. # Import the host-module aggregator from the content-addressed - # nixSrc; the package options (`services.hyperhive.c0re.package` + # nixSrc; the package options (`services.hyperhive.deploy.hive-controller.package` # etc.) carry no in-module defaults, but with hyperhive disabled # nothing reads them, so no stubs are needed. hostEval = nixosSystem { diff --git a/nix/host-modules/deploy.nix b/nix/host-modules/deploy.nix index d0c1bfce..eb0052d8 100644 --- a/nix/host-modules/deploy.nix +++ b/nix/host-modules/deploy.nix @@ -195,6 +195,65 @@ in [ "services" "hyperhive" "c0re" "dashboardPort" ] [ "services" "hyperhive" "deploy" "hive-controller" "dashboardPort" ] ) + + # The c0re daemon is the hive-controller service, so its knobs and package + # options sit under it. Same types, same meanings. + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "c0re" "package" ] + [ "services" "hyperhive" "deploy" "hive-controller" "package" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "c0re" "frontend" ] + [ "services" "hyperhive" "deploy" "hive-controller" "frontend" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "c0re" "assets" ] + [ "services" "hyperhive" "deploy" "hive-controller" "assets" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "c0re" "xdgIcons" ] + [ "services" "hyperhive" "deploy" "hive-controller" "xdgIcons" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "c0re" "hyperhiveRev" ] + [ "services" "hyperhive" "deploy" "hive-controller" "hyperhiveRev" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "c0re" "agentBaseToplevel" ] + [ "services" "hyperhive" "deploy" "hive-controller" "agentBaseToplevel" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "c0re" "preBuildAgentTemplates" ] + [ "services" "hyperhive" "deploy" "hive-controller" "preBuildAgentTemplates" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "c0re" "modelPrices" ] + [ "services" "hyperhive" "deploy" "hive-controller" "modelPrices" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "c0re" "agentCpuQuota" ] + [ "services" "hyperhive" "deploy" "hive-controller" "agentCpuQuota" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "c0re" "agentMemoryMax" ] + [ "services" "hyperhive" "deploy" "hive-controller" "agentMemoryMax" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "c0re" "agentCpuWeight" ] + [ "services" "hyperhive" "deploy" "hive-controller" "agentCpuWeight" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "c0re" "agentIoWeight" ] + [ "services" "hyperhive" "deploy" "hive-controller" "agentIoWeight" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "c0re" "buildSlots" ] + [ "services" "hyperhive" "deploy" "hive-controller" "buildSlots" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "c0re" "adminUsers" ] + [ "services" "hyperhive" "deploy" "hive-controller" "adminUsers" ] + ) (lib.mkRenamedOptionModule [ "services" "hyperhive" "otel" "extraResourceAttributes" ] [ "services" "hyperhive" "deploy" "hive-controller" "otel" "extraResourceAttributes" ] diff --git a/nix/host-modules/hive-c0re/default.nix b/nix/host-modules/hive-c0re/default.nix index fb4a1d66..8ee272a6 100644 --- a/nix/host-modules/hive-c0re/default.nix +++ b/nix/host-modules/hive-c0re/default.nix @@ -102,12 +102,12 @@ let else import ../../claude-code-no-alsa.nix lib controllerCfg.claudeCodePackage; dashboard_port = controllerCfg.dashboardPort; - agent_cpu_quota = cfg.agentCpuQuota; - agent_memory_max = cfg.agentMemoryMax; - agent_cpu_weight = cfg.agentCpuWeight; - agent_io_weight = cfg.agentIoWeight; - model_prices = cfg.modelPrices; - build_slots = cfg.buildSlots; + agent_cpu_quota = controllerCfg.agentCpuQuota; + agent_memory_max = controllerCfg.agentMemoryMax; + agent_cpu_weight = controllerCfg.agentCpuWeight; + agent_io_weight = controllerCfg.agentIoWeight; + model_prices = controllerCfg.modelPrices; + build_slots = controllerCfg.buildSlots; }; in { @@ -167,11 +167,11 @@ in ]; environment.systemPackages = [ - cfg.package + controllerCfg.package pkgs.git # XDG icons + .desktop entries so desktop environments can match # hyperhive processes to their icon (task managers, CPU monitors, etc.). - cfg.xdgIcons + controllerCfg.xdgIcons ]; # Serve config at a stable /etc path so hive-c0re's ExecStart @@ -185,8 +185,8 @@ in # builds them, they end up in /nix/store, and the first # nixos-container update + start for an agent has nothing left to # do. Gated because the closure is sizeable and pinned to x86_64. - system.extraDependencies = lib.optionals cfg.preBuildAgentTemplates [ - cfg.agentBaseToplevel + system.extraDependencies = lib.optionals controllerCfg.preBuildAgentTemplates [ + controllerCfg.agentBaseToplevel ]; # Unprivileged coordinator user. hive-c0re runs as this user; @@ -205,7 +205,7 @@ in # hive control (spawn/kill/destroy/deploy, docs/trust-boundary/boundary.md), so this is # an explicit opt-in allowlist — empty by default (root-only). users.groups.hive-admin = { - members = cfg.adminUsers; + members = controllerCfg.adminUsers; }; # Let `hive-admin` members run `hivectl agent choom` @@ -259,7 +259,7 @@ in ]; environment = import ./environment.nix { inherit lib config pkgs; }; serviceConfig = { - ExecStart = "${cfg.package}/bin/hive-c0re --socket /run/hyperhive/host.sock serve --config /etc/hyperhive/serve.json"; + ExecStart = "${controllerCfg.package}/bin/hive-c0re --socket /run/hyperhive/host.sock serve --config /etc/hyperhive/serve.json"; SyslogIdentifier = "hive-c0re"; # Migrate hive-c0re's *own* state to the service user after an # upgrade from a root-run install (systemd's StateDirectory only @@ -378,7 +378,7 @@ in # Must match the `--socket` arg passed to `hive-c0re serve`. ListenStream = "/run/hyperhive/host.sock"; # `0660 root:hive-admin` — group-owned by `hive-admin` so operators in - # that group (services.hyperhive.c0re.adminUsers) drive `hivectl` + # that group (services.hyperhive.deploy.hive-controller.adminUsers) drive `hivectl` # without root; an empty adminUsers list leaves the group memberless, # so it stays effectively root-only. See docs/trust-boundary/boundary.md. SocketMode = "0660"; diff --git a/nix/host-modules/hive-c0re/environment.nix b/nix/host-modules/hive-c0re/environment.nix index 44ce62f4..692e1ba7 100644 --- a/nix/host-modules/hive-c0re/environment.nix +++ b/nix/host-modules/hive-c0re/environment.nix @@ -8,7 +8,7 @@ pkgs, }: let - cfg = config.services.hyperhive.c0re; + controllerCfg = config.services.hyperhive.deploy.hive-controller; baoDeploy = config.services.hyperhive.deploy.bao; baoCfg = config.services.hyperhive.swarm.bao; # A reader is defined by holding a certificate the store accepts, not by @@ -64,7 +64,7 @@ in # nix store directly; dynamic API paths still proxied to the # agent daemon. nginx runs on the host, which is where this # store path already is, so it is reachable as written. - HIVE_AGENT_FRONTEND_DIR = "${cfg.servedFrontend}/agent"; + HIVE_AGENT_FRONTEND_DIR = "${controllerCfg.servedFrontend}/agent"; } // { # Identity env vars threaded into c0re's own service env and diff --git a/nix/host-modules/hive-c0re/options.nix b/nix/host-modules/hive-c0re/options.nix index 19acdce0..bea66975 100644 --- a/nix/host-modules/hive-c0re/options.nix +++ b/nix/host-modules/hive-c0re/options.nix @@ -1,11 +1,12 @@ -# Option declarations for `services.hyperhive.c0re.*` — the c0re -# daemon's knobs plus the package/source options the flake's -# `nixosModules.default` wires to its own outputs (they carry no -# in-module defaults; see ../../../flake.nix). The read-only +# Option declarations for the c0re daemon. `services.hyperhive.c0re.*` +# keeps the two flake-source options the swarm builder also reads +# (`hyperhiveFlake`, `nixpkgsFlake`); every other knob and the +# package/asset options the flake's `nixosModules.default` wires to its +# own outputs (they carry no in-module defaults; see ../../../flake.nix) +# live under `services.hyperhive.deploy.hive-controller`, alongside the +# values the meta-flake render hands the hive's agents. The read-only # `servedFrontend` option lives in ./theme.nix with the stylix wiring -# that computes it. The hive's own values the meta-flake render hands -# its agents are declared here too, under -# `services.hyperhive.deploy.hive-controller`. +# that computes it. { pkgs, lib, @@ -23,6 +24,33 @@ ]; options.services.hyperhive.c0re = { + hyperhiveFlake = lib.mkOption { + type = lib.types.str; + defaultText = lib.literalMD "the hyperhive flake's own filtered source store path"; + description = '' + Store path of the hyperhive flake source (no fragment). hive-c0re + reads it to tell which revision is running, and the swarm builder + builds on it. Wired by `nixosModules.default` to this flake's own + filtered source. + ''; + }; + nixpkgsFlake = lib.mkOption { + type = lib.types.str; + default = "path:${pkgs.path}"; + defaultText = lib.literalMD "`\"path:\${pkgs.path}\"`"; + description = '' + Store-path URL of the nixpkgs the swarm builder's check flake + builds on. The swarm meta flake does not use it: its `nixpkgs` + follows `hyperhive/nixpkgs`. + + Defaults to `"path:''${pkgs.path}"`, the nixpkgs the host NixOS + module was evaluated with. + ''; + }; + }; + + # The hive-controller's deployment options. + options.services.hyperhive.deploy.hive-controller = { package = lib.mkOption { type = lib.types.package; defaultText = lib.literalExpression "hyperhive.packages.\${system}.default"; @@ -71,16 +99,6 @@ processes to their icon. ''; }; - hyperhiveFlake = lib.mkOption { - type = lib.types.str; - defaultText = lib.literalMD "the hyperhive flake's own filtered source store path"; - description = '' - Store path of the hyperhive flake source (no fragment). hive-c0re - reads it to tell which revision is running, and the swarm builder - builds on it. Wired by `nixosModules.default` to this flake's own - filtered source. - ''; - }; hyperhiveRev = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; @@ -105,19 +123,6 @@ enabled. ''; }; - nixpkgsFlake = lib.mkOption { - type = lib.types.str; - default = "path:${pkgs.path}"; - defaultText = lib.literalMD "`\"path:\${pkgs.path}\"`"; - description = '' - Store-path URL of the nixpkgs the swarm builder's check flake - builds on. The swarm meta flake does not use it: its `nixpkgs` - follows `hyperhive/nixpkgs`. - - Defaults to `"path:''${pkgs.path}"`, the nixpkgs the host NixOS - module was evaluated with. - ''; - }; preBuildAgentTemplates = lib.mkOption { type = lib.types.bool; default = false; @@ -325,12 +330,7 @@ socket root-only, as before. ''; }; - }; - # What this hive hands its own agents at container start, through - # `/etc/hyperhive-hive-facts`. Per host, so two hives may set them - # differently. - options.services.hyperhive.deploy.hive-controller = { claudeCodePackage = lib.mkOption { type = lib.types.nullOr lib.types.package; default = null; diff --git a/nix/host-modules/hive-c0re/theme.nix b/nix/host-modules/hive-c0re/theme.nix index 0141446e..9fd7f2d9 100644 --- a/nix/host-modules/hive-c0re/theme.nix +++ b/nix/host-modules/hive-c0re/theme.nix @@ -6,7 +6,7 @@ # the entire swap contract — `theme.css` derives every semantic var # from the 16 base16 slots (see docs/web-ui/css-vars.md). The guarded # access makes this a clean no-op when stylix isn't imported into the -# host config. Exposed as the read-only `c0re.servedFrontend` option. +# host config. Exposed as the read-only `deploy.hive-controller.servedFrontend` option. # # Detection + CSS-generation itself lives in `../stylix-theme.nix` # (shared with `swarm-ui.nix`) — this file is the c0re-specific overlay @@ -18,7 +18,7 @@ ... }: let - cfg = config.services.hyperhive.c0re; + cfg = config.services.hyperhive.deploy.hive-controller; stylixTheme = import ../stylix-theme.nix { inherit lib config pkgs; }; inherit (stylixTheme) stylixThemeColors themedColorsCss; # Overlay the generated colors.css onto both dist subtrees. Both the @@ -42,7 +42,7 @@ let ''; in { - options.services.hyperhive.c0re.servedFrontend = lib.mkOption { + options.services.hyperhive.deploy.hive-controller.servedFrontend = lib.mkOption { type = lib.types.package; internal = true; readOnly = true; diff --git a/nix/host-modules/hive-c0re/vhosts.nix b/nix/host-modules/hive-c0re/vhosts.nix index 2e877a9d..abc3c143 100644 --- a/nix/host-modules/hive-c0re/vhosts.nix +++ b/nix/host-modules/hive-c0re/vhosts.nix @@ -17,7 +17,7 @@ let inherit (gatewayCfg.lib) errorPages; # Dashboard SPA dist, static-served by nginx. - dashboardDist = "${hyperhiveCfg.c0re.servedFrontend}/dashboard"; + dashboardDist = "${hyperhiveCfg.deploy.hive-controller.servedFrontend}/dashboard"; # `/agent/` catch-all 404 + the two internal error-page targets it # points at. Per-agent `location /agent//` blocks live in the diff --git a/nix/host-modules/hive-priv.nix b/nix/host-modules/hive-priv.nix index 0774b9c5..5f865f57 100644 --- a/nix/host-modules/hive-priv.nix +++ b/nix/host-modules/hive-priv.nix @@ -8,7 +8,7 @@ ... }: let - cfg = config.services.hyperhive.c0re; + controllerCfg = config.services.hyperhive.deploy.hive-controller; forgeGit = import ./lib/hive-forge-git.nix { inherit pkgs; @@ -114,7 +114,7 @@ in HIVE_PRIV_FORGE_URL = "http://${config.services.hyperhive.swarm.forge.domain}"; }; serviceConfig = { - ExecStart = "${cfg.package}/bin/hive-priv"; + ExecStart = "${controllerCfg.package}/bin/hive-priv"; SyslogIdentifier = "hive-priv"; Type = "simple"; User = "root"; diff --git a/nix/host-modules/lib/container-rootfs-check.nix b/nix/host-modules/lib/container-rootfs-check.nix index 5922c80a..6f9382b4 100644 --- a/nix/host-modules/lib/container-rootfs-check.nix +++ b/nix/host-modules/lib/container-rootfs-check.nix @@ -28,7 +28,7 @@ # Attributes for `systemd.services."container@"`. warnOnly = name: { serviceConfig.ExecStartPre = [ - "-${config.services.hyperhive.c0re.package}/bin/hive-priv check-rootfs --warn-only ${name}" + "-${config.services.hyperhive.deploy.hive-controller.package}/bin/hive-priv check-rootfs --warn-only ${name}" ]; }; } diff --git a/nix/host-modules/swarm-controller.nix b/nix/host-modules/swarm-controller.nix index 5890046e..6286b93f 100644 --- a/nix/host-modules/swarm-controller.nix +++ b/nix/host-modules/swarm-controller.nix @@ -209,7 +209,7 @@ let if deployCfg.swarm-controller.configOrgAvatarPng != null then "${deployCfg.swarm-controller.configOrgAvatarPng}" else - "${config.services.hyperhive.c0re.assets}/share/hyperhive/branding/agent-configs.png"; + "${config.services.hyperhive.deploy.hive-controller.assets}/share/hyperhive/branding/agent-configs.png"; }; # How the forge must address this controller to deliver a swarm-wide @@ -288,7 +288,7 @@ let github_disabled = !swarmCfg.github.enable; # The rev this controller runs, which `internal/meta` pins hyperhive # to once it descends from the committed pin; null on a dirty tree. - hyperhive_rev = config.services.hyperhive.c0re.hyperhiveRev; + hyperhive_rev = config.services.hyperhive.deploy.hive-controller.hyperhiveRev; }; }; @@ -540,7 +540,7 @@ in marker-guarded — delete `forge-config-org-avatar-set` under the controller's state directory to force a re-upload after changing this). Defaults (`null`) to the bundled `agent-configs.png` from - {option}`services.hyperhive.c0re.assets`. Set this to override just + {option}`services.hyperhive.deploy.hive-controller.assets`. Set this to override just the org avatar without replacing the whole `assets` package. ''; }; diff --git a/nix/module-eval/core-toggle.nix b/nix/module-eval/core-toggle.nix index cfe315c8..dafea191 100644 --- a/nix/module-eval/core-toggle.nix +++ b/nix/module-eval/core-toggle.nix @@ -280,7 +280,7 @@ let settings = builtins.fromJSON allLocal.systemd.services.swarm-controller.environment.SWARM_CONTROLLER_META_FLAKE; in settings ? hyperhive_rev - && settings.hyperhive_rev == allLocal.services.hyperhive.c0re.hyperhiveRev + && settings.hyperhive_rev == allLocal.services.hyperhive.deploy.hive-controller.hyperhiveRev && settings ? git_base; } { @@ -1412,6 +1412,106 @@ let && warned old && !(warned bare); } + { + # Every c0re option that moved under `deploy.hive-controller` still + # takes its old spelling, with one rename warning each, and lands at + # the new path. The packages are distinct stand-ins so a crossed + # forward shows. `bare` is the control that the warning is not always + # present, and the new path is what the rendered values below read. + name = "services.hyperhive.c0re.* options that moved still set deploy.hive-controller.*, with rename warnings"; + ok = + let + old = hiveWithController { + c0re.package = pkgs.hello; + c0re.frontend = pkgs.coreutils; + c0re.assets = pkgs.gnused; + c0re.xdgIcons = pkgs.gnugrep; + c0re.agentBaseToplevel = pkgs.findutils; + c0re.hyperhiveRev = "abc123"; + c0re.preBuildAgentTemplates = true; + c0re.modelPrices.sonnet = { + input = 1.0; + output = 2.0; + cache_read = 3.0; + cache_write = 4.0; + }; + c0re.agentCpuQuota = "300%"; + c0re.agentMemoryMax = "8G"; + c0re.agentCpuWeight = 50; + c0re.agentIoWeight = null; + c0re.buildSlots = 3; + c0re.adminUsers = [ "alice" ]; + }; + c = old.services.hyperhive.deploy.hive-controller; + warned = + m: name: + lib.any ( + w: lib.hasInfix "services.hyperhive.c0re.${name}" w && lib.hasInfix "has been renamed" w + ) m.warnings; + serve = builtins.fromJSON ( + builtins.unsafeDiscardStringContext old.environment.etc."hyperhive/serve.json".text + ); + path = p: builtins.unsafeDiscardStringContext "${p}"; + in + path c.package == path pkgs.hello + && path c.frontend == path pkgs.coreutils + && path c.assets == path pkgs.gnused + && path c.xdgIcons == path pkgs.gnugrep + && path c.agentBaseToplevel == path pkgs.findutils + && path c.servedFrontend == path pkgs.coreutils + && c.hyperhiveRev == "abc123" + && c.preBuildAgentTemplates + && c.modelPrices.sonnet.cache_write == 4.0 + && c.agentCpuQuota == "300%" + && c.agentMemoryMax == "8G" + && c.agentCpuWeight == 50 + && c.agentIoWeight == null + && c.buildSlots == 3 + && c.adminUsers == [ "alice" ] + && lib.all (warned old) [ + "package" + "frontend" + "assets" + "xdgIcons" + "agentBaseToplevel" + "hyperhiveRev" + "preBuildAgentTemplates" + "modelPrices" + "agentCpuQuota" + "agentMemoryMax" + "agentCpuWeight" + "agentIoWeight" + "buildSlots" + "adminUsers" + ] + && !(warned bare "package") + && !(warned bare "adminUsers") + && serve.agent_memory_max == "8G" + && serve.agent_cpu_quota == "300%" + && serve.build_slots == 3 + && old.users.groups.hive-admin.members == [ "alice" ]; + } + { + # What the moved options render with their defaults: the agent limits + # and build slots in serve.json, the sudoless-hivectl group's members, + # and the model price table. The same values the options held at their + # old paths. + name = "the agent limits, build slots, admin group and price table render their defaults unchanged"; + ok = + let + serve = builtins.fromJSON ( + builtins.unsafeDiscardStringContext allLocal.environment.etc."hyperhive/serve.json".text + ); + in + serve.agent_memory_max == "4G" + && serve.agent_cpu_quota == "200%" + && serve.agent_cpu_weight == 80 + && serve.agent_io_weight == 80 + && serve.build_slots == 1 + && serve.model_prices.opus.input == 5.0 + && serve.model_prices.haiku.cache_write == 2.0 + && allLocal.users.groups.hive-admin.members == [ ]; + } { # A hive's rendered identity, pinned so moving the option changes none of # it: the coordinator's name and client, the dashboard name, the CN of the @@ -1526,7 +1626,7 @@ let carries = m: name: checks m name == [ - (builtins.unsafeDiscardStringContext "-${m.services.hyperhive.c0re.package}/bin/hive-priv check-rootfs --warn-only ${name}") + (builtins.unsafeDiscardStringContext "-${m.services.hyperhive.deploy.hive-controller.package}/bin/hive-priv check-rootfs --warn-only ${name}") ]; names = lib.attrNames allLocal.containers; in diff --git a/nix/packages/default.nix b/nix/packages/default.nix index 22c7a0ec..55947c0e 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -140,7 +140,7 @@ in { # All workspace binaries in one derivation — a symlinkJoin over the # per-bin packages, pure assembly with no extra compilation. The - # host module's `services.hyperhive.c0re.package` and `nix build .#` + # host module's `services.hyperhive.deploy.hive-controller.package` and `nix build .#` # both land here. default = pkgs.symlinkJoin { name = "hyperhive"; @@ -152,7 +152,7 @@ in # Swarm-level controller daemon. Deliberately NOT in `daemonBins`: # that list is the core stack — the binaries hive-c0re and the agent # harness are made of — and it drives the `default` bundle that - # `services.hyperhive.c0re.package` points at. This daemon is a + # `services.hyperhive.deploy.hive-controller.package` points at. This daemon is a # separate swarm-scoped service with its own module and its own # `package` option, and one hive in a swarm runs it, so folding it # into the core bundle would put it in every hive's closure to no end. diff --git a/nix/vm-tests/agent-ephemeral-rootfs.nix b/nix/vm-tests/agent-ephemeral-rootfs.nix index 74a651aa..79ad40aa 100644 --- a/nix/vm-tests/agent-ephemeral-rootfs.nix +++ b/nix/vm-tests/agent-ephemeral-rootfs.nix @@ -46,7 +46,7 @@ let { lib, ... }: { options.services.hyperhive = { - c0re.package = lib.mkOption { type = lib.types.package; }; + deploy.hive-controller.package = lib.mkOption { type = lib.types.package; }; deploy.hive-controller.enable = lib.mkOption { type = lib.types.bool; default = true; @@ -56,7 +56,7 @@ let default = "forge.t.local"; }; }; - config.services.hyperhive.c0re.package = hivePriv; + config.services.hyperhive.deploy.hive-controller.package = hivePriv; # hive-priv.socket's group, which hive-c0re's module creates. config.users.groups.hive-core = { }; }; diff --git a/swarm-controller/README.md b/swarm-controller/README.md index 545ca62c..dca8e1cd 100644 --- a/swarm-controller/README.md +++ b/swarm-controller/README.md @@ -171,5 +171,5 @@ a **pair**: preserving the directory without the unlink means `bind` fails with Built by the workspace derivation and extracted as its own package (`nix build .#swarm-controller`). Deliberately **not** in `nix/packages`' `daemonBins` — that list is the core stack and drives the bundle -`services.hyperhive.c0re.package` points at, so folding this in would put a +`services.hyperhive.deploy.hive-controller.package` points at, so folding this in would put a swarm-scoped service into every hive's closure.