Watch
0
0
Fork
You've already forked hyperhive
0

move dashboardPort, claudeCodePackage and extraResourceAttributes to deploy.hive-controller

These three are per-host choices that hive-c0re reads and renders into
its agents, so they belong under the hive controller's deploy namespace
rather than swarm-wide:

- c0re.dashboardPort -> deploy.hive-controller.dashboardPort
- c0re.claudeCodePackage -> deploy.hive-controller.claudeCodePackage
  (the claude-login and review-classifier containers on the same host
  keep following it)
- otel.extraResourceAttributes -> deploy.hive-controller.otel.extraResourceAttributes

The old paths stay as mkRenamedOptionModule aliases, now in deploy.nix
with the namespace's other renames. github.enable and the collector port
stay swarm-wide.
This commit is contained in:
atlas 2026-10-17 12:52:24 +02:00 • committed by mara
commit 5bb9d7c7e5
21 changed files with 92 additions and 83 deletions

View file

@ -129,7 +129,7 @@ The name sits under the swarm domain so authelia's session cookie reaches it, wh
nginx is the one external entry point. The per-agent web-port range (`8100`–`8999`) stays closed: agents serve their UI on a unix socket ([Per-agent unix-socket upstream](#per-agent-unix-socket-upstream)), and the hashed TCP port (`lifecycle::agent_web_port`) is only a fallback bind for an agent missing `HIVE_WEB_SOCKET` — the gateway never proxies through it.
The dashboard port (`services.hyperhive.swarm.dashboardPort`, default 7000) binds `127.0.0.1` only, so remote dashboard access goes through the gateway. The dashboard can stop, rebuild and destroy agents; don't expose it without a reverse proxy in front.
The dashboard port (`services.hyperhive.deploy.hive-controller.dashboardPort`, default 7000) binds `127.0.0.1` only, so remote dashboard access goes through the gateway. The dashboard can stop, rebuild and destroy agents; don't expose it without a reverse proxy in front.
nginx serves its `stub_status` counters on `127.0.0.1:<statusPort>` (`services.hyperhive.gateway.statusPort`, default 8085) at `/stub_status`, from a server of its own outside the vhost tree. The hive's collector reads them with its `nginx` receiver, and the `gateway` Grafana dashboard plots them. The receiver only dials `http://`, which is why this is loopback TCP rather than a unix socket.

View file

@ -11,7 +11,7 @@ exist because something already went wrong without them.
- `MAX_AGENT_NAME` in `hive-c0re/src/lifecycle/mod.rs` enforces the cap.
- Per-agent web UI port = `WEB_PORT_BASE + FNV1a(name) % WEB_PORT_RANGE`
(8100..8999) for every agent; dashboard
`services.hyperhive.swarm.dashboardPort` (default 7000).
`services.hyperhive.deploy.hive-controller.dashboardPort` (default 7000).
## Hive identity (label + domain + display names)

View file

@ -140,7 +140,7 @@ _different_ nixpkgs by itself: a container only ever sees the one
nixpkgs the meta flake injects, so an `agent.nix` naming the host's
`nixpkgs-unstable` has nothing to name. A release channel can trail
unstable by weeks on this package, which is what
`services.hyperhive.swarm.claudeCodePackage` is for — set it host-side
`services.hyperhive.deploy.hive-controller.claudeCodePackage` is for — set it host-side
and every agent runs that build.
What crosses is the **store path**, not the derivation. Containers
@ -228,7 +228,7 @@ agent (different hash → different port) and rebuilding. No state
file, no probing, no port-allocation drift — the value is
reproducible from just the name. Every agent hashes into
8100..8999 via the same FNV-1a; dashboard
at `services.hyperhive.swarm.dashboardPort` (default 7000).
at `services.hyperhive.deploy.hive-controller.dashboardPort` (default 7000).
### Restart races on TCP bind

View file

@ -179,12 +179,12 @@ To let agents reach some other host-local service of your own →
The nix module (`nix/host-modules/otel.nix`) generates every
`services.hyperhive.otel.*` option's full type, default, description and
example into the [options reference](https://hyperhive.darkest.space/options/)
(host options — `nix build .#docs-host` for a local render). The values every
hive and its agents share live in the same module under
`services.hyperhive.swarm.hiveOtel.*`: the collector port and
`extraResourceAttributes`, set once for the swarm. That's the reference;
this page covers what a per-option listing can't: the two-tier architecture,
the security model, and how the options interact.
(host options — `nix build .#docs-host` for a local render). The same module
declares the collector port every hive and its agents share,
`services.hyperhive.swarm.hiveOtel.collectorPort`, and each hive's own
`services.hyperhive.deploy.hive-controller.otel.extraResourceAttributes`.
That's the reference; this page covers what a per-option listing can't: the
two-tier architecture, the security model, and how the options interact.
## Built-in resource labels
@ -208,7 +208,7 @@ The swarm's collector adds `hive`, the name of the hive whose receiver accepted
the push (see [Authenticated ingest](#authenticated-ingest)). The container
doesn't set it.
Append additional swarm-wide labels via `extraResourceAttributes` (see
Append additional labels for one hive via `extraResourceAttributes` (see
option reference above); pass custom per-data-point labels with
`hive-metric --labels` (see below).
@ -324,8 +324,8 @@ The `hyperhive.`-prefixed metrics have no semconv equivalent (memory
limit + peak, on-disk footprint, and an instantaneous cpu percent kept
alongside the spec `container.cpu.time` counter for convenience). The `swarm`
label rides on the resource, and the swarm collector adds `hive` from the
receiver that accepted the sample. `extraResourceAttributes` is swarm-wide, so
it can't carry a per-hive label.
receiver that accepted the sample. Any other label goes in the hive's
`extraResourceAttributes`.
<!-- vale write-good.Passive = NO -->

View file

@ -7,8 +7,9 @@ agent's harness installs them from there.
Where it runs and how it's enabled: [`services.md`](services.md#claude-login-minter).
The service runs the `claude-code` that `services.hyperhive.swarm.claudeCodePackage`
pins for agents, and its container's own nixpkgs' where the swarm pins none.
The service runs the `claude-code` that its host's
`services.hyperhive.deploy.hive-controller.claudeCodePackage`
pins for agents, and its container's own nixpkgs' where the host pins none.
## What happens on a login

View file

@ -145,8 +145,9 @@ config) and an empty directory of its own. The prompt reaches it on stdin as
one user message in `--input-format stream-json`, so that it can carry content
blocks (see [Shared context](#shared-context)).
The `claude` is the one `services.hyperhive.swarm.claudeCodePackage` pins for
agents, or its container's own nixpkgs' where the swarm pins none.
The `claude` is the one its host's
`services.hyperhive.deploy.hive-controller.claudeCodePackage` pins for
agents, or its container's own nixpkgs' where the host pins none.
On the ACP runtime, each classification is one prompt on a new `opencode acp`
session. Its config and opencode's state live in a directory of its own,

View file

@ -32,8 +32,8 @@ to the bus.
harness unit's PATH. By default that's the `claude-code` in the agent's
own nixpkgs (the meta flake's `nixpkgs` input) via
`environment.systemPackages`. Since that's typically a release channel and
this package moves fast, the operator can pin one swarm-wide with
`services.hyperhive.swarm.claudeCodePackage`: hive-c0re writes its
this package moves fast, the operator can pin one per hive with
`services.hyperhive.deploy.hive-controller.claudeCodePackage`: hive-c0re writes its
store path into each agent's flake, and `claude` on PATH becomes a symlink to it
instead of the container's own `claude-code` — so there's only ever one
`claude` in the container. Agents pick up a new build on their

View file

@ -54,7 +54,8 @@ pub struct Coordinator {
pub nixpkgs_flake: String,
/// Store path of the `claude-code` build every agent runs, written
/// into each per-agent flake as `services.hyperhive.agent.claudeCodePath`. Set by
/// the NixOS module option `services.hyperhive.swarm.claudeCodePackage`
/// the NixOS module option
/// `services.hyperhive.deploy.hive-controller.claudeCodePackage`
/// (which resolves the package and hands us its path). `None` = every
/// agent keeps the `claude-code` from its own nixpkgs.
///

View file

@ -551,8 +551,9 @@ fn hive_ca_source() -> Option<String> {
}
/// Hive-wide OTEL config injected into every agent's build, read off
/// hive-c0re's own unit env (set from `services.hyperhive.otel.*` and
/// `services.hyperhive.swarm.hiveOtel.*`). A present, non-empty
/// hive-c0re's own unit env (set from `services.hyperhive.otel.*`,
/// `services.hyperhive.swarm.hiveOtel.*` and
/// `services.hyperhive.deploy.hive-controller.otel.*`). A present, non-empty
/// `HYPERHIVE_OTEL_ENDPOINT` is the enable signal — the host module
/// asserts the endpoint is set whenever `otel.enable` is true, so
/// "endpoint present" == "OTEL on". The optional fields map to the

View file

@ -347,8 +347,8 @@ fn attrs(c: &ContainerResource) -> Vec<KeyValue> {
/// Resource: `service.name = hyperhive-c0re`, the host architecture, the
/// running hyperhive revision, plus whatever the operator set in
/// `swarm.hiveOtel.extraResourceAttributes` (swarm-wide labels, same channel
/// the agent-side export uses; the `hive` label is stamped by the swarm
/// `deploy.hive-controller.otel.extraResourceAttributes` (same channel the
/// agent-side export uses; the `hive` label is stamped by the swarm
/// collector, not set here).
///
/// These belong on the resource rather than on each data point for the same

View file

@ -96,7 +96,7 @@ in
nixpkgs.
Set by the generated meta flake when the operator sets
`services.hyperhive.swarm.claudeCodePackage` host-side, so a swarm
`services.hyperhive.deploy.hive-controller.claudeCodePackage` host-side, so a hive
can run a claude built from a *different* nixpkgs than the one
its agents evaluate against — a release channel can trail
unstable by weeks on this one package. It arrives as a path and
@ -109,7 +109,7 @@ in
there is only ever one claude in the container. Note that neither
the symlink nor anything else in the container's closure *refers*
to the target — keeping it alive is the host's job, see
`services.hyperhive.swarm.claudeCodePackage`.
`services.hyperhive.deploy.hive-controller.claudeCodePackage`.
'';
};
@ -255,7 +255,7 @@ in
# The symlink registers no store reference — the target isn't
# among this derivation's inputs, so nothing here keeps the
# binary alive. That is deliberate and it is the host's job:
# see `services.hyperhive.swarm.claudeCodePackage`.
# see `services.hyperhive.deploy.hive-controller.claudeCodePackage`.
pkgs.runCommandLocal "claude-code-pinned" { } ''
mkdir -p "$out/bin"
ln -s ${config.services.hyperhive.agent.claudeCodePath}/bin/claude "$out/bin/claude"

View file

@ -125,7 +125,8 @@ in
Extra comma-separated entries appended to
`OTEL_RESOURCE_ATTRIBUTES` after the built-in
`service.name` / `agent` / `swarm` labels.
Host-driven via `services.hyperhive.swarm.hiveOtel.extraResourceAttributes`.
Host-driven via
`services.hyperhive.deploy.hive-controller.otel.extraResourceAttributes`.
'';
};

View file

@ -112,6 +112,21 @@ in
[ "services" "hyperhive" "deploy" "bao" "servicesIssuerClientKeyFile" ]
)
# What hive-c0re hands this hive's agents: a per-host choice, so it
# sits with the controller's other deployment options.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "c0re" "claudeCodePackage" ]
[ "services" "hyperhive" "deploy" "hive-controller" "claudeCodePackage" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "c0re" "dashboardPort" ]
[ "services" "hyperhive" "deploy" "hive-controller" "dashboardPort" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "otel" "extraResourceAttributes" ]
[ "services" "hyperhive" "deploy" "hive-controller" "otel" "extraResourceAttributes" ]
)
# The homeserver's port and its firewall switch, removed rather than
# renamed: tuwunel listens on a unix socket only the gateway reaches, so
# there is no host port to choose or open.

View file

@ -12,7 +12,7 @@
}:
let
cfg = config.services.hyperhive.c0re;
swarmCfg = config.services.hyperhive.swarm;
controllerCfg = config.services.hyperhive.deploy.hive-controller;
# `/run/hyperhive`'s mode. Held in one place because whichever of the two
# units below activates first creates the directory, so they cannot be
@ -111,8 +111,8 @@ let
# anything that drops it. The failure mode is a garbage-collected
# `claude` and a hive that can't take a turn, weeks after the commit
# that caused it.
claude_code_path = swarmCfg.claudeCodePackage;
dashboard_port = swarmCfg.dashboardPort;
claude_code_path = controllerCfg.claudeCodePackage;
dashboard_port = controllerCfg.dashboardPort;
agent_cpu_quota = cfg.agentCpuQuota;
agent_memory_max = cfg.agentMemoryMax;
agent_cpu_weight = cfg.agentCpuWeight;
@ -164,10 +164,10 @@ in
# `claude`, at whatever unrelated moment the gc runs. Cheap
# enough to just check.
assertion =
swarmCfg.claudeCodePackage == null
|| builtins.hasContext (builtins.toJSON swarmCfg.claudeCodePackage);
controllerCfg.claudeCodePackage == null
|| builtins.hasContext (builtins.toJSON controllerCfg.claudeCodePackage);
message = ''
services.hyperhive.swarm.claudeCodePackage lost its store
services.hyperhive.deploy.hive-controller.claudeCodePackage lost its store
context on the way into /etc/hyperhive/serve.json, so the
package is no longer gc-rooted by the system closure and
`nix-collect-garbage` may delete the claude every agent runs.

View file

@ -72,15 +72,16 @@ in
HYPERHIVE_GITHUB_DISABLED = "1";
}
// lib.optionalAttrs config.services.hyperhive.otel.enable (
# OTEL config (`swarm.hiveOtel`, gated on this hive's `otel.enable`) ->
# read by meta.rs::otel_config and injected as build-time
# `services.hyperhive.agent.otel.*` into every agent.
# OTEL config (`swarm.hiveOtel`, `deploy.hive-controller.otel`, gated on
# this hive's `otel.enable`) -> read by meta.rs::otel_config and injected
# as build-time `services.hyperhive.agent.otel.*` into every agent.
# Endpoint presence is the enable signal on the meta side; the
# optional fields are only emitted when set so absent values
# don't render no-op env lines.
let
otel = config.services.hyperhive.otel;
hiveOtel = config.services.hyperhive.swarm.hiveOtel;
controllerOtel = config.services.hyperhive.deploy.hive-controller.otel;
# The first hop, this hive's own collector, under the two spellings its
# two kinds of caller need. Agents get the swarm-wide name, so their
# builds carry nothing hive-specific; this daemon gets the bridge
@ -115,8 +116,8 @@ in
# what has to honour it (see nix/host-modules/otel.nix).
HYPERHIVE_OTEL_PROTOCOL = "http/protobuf";
}
// lib.optionalAttrs (hiveOtel.extraResourceAttributes != "") {
HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES = hiveOtel.extraResourceAttributes;
// lib.optionalAttrs (controllerOtel.extraResourceAttributes != "") {
HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES = controllerOtel.extraResourceAttributes;
}
# HYPERHIVE_OTEL_HEADERS_CREDENTIAL is deliberately NOT emitted, and
# its absence is the security half of this design: that variable would

View file

@ -3,29 +3,19 @@
# `nixosModules.default` wires to its own outputs (they carry no
# in-module defaults; see ../../../flake.nix). The read-only
# `servedFrontend` option lives in ./theme.nix with the stylix wiring
# that computes it. The swarm-wide values the meta-flake render hands
# every agent are declared here too, under `services.hyperhive.swarm`.
# 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`.
{
pkgs,
lib,
...
}:
let
renamedToSwarm =
name:
lib.mkRenamedOptionModule
[ "services" "hyperhive" "c0re" name ]
[ "services" "hyperhive" "swarm" name ];
in
{
imports = [
(lib.mkRemovedOptionModule [ "services" "hyperhive" "c0re" "managerToplevel" ] ''
Every agent, ruth included, builds from `agentBaseToplevel`. Remove this definition.
'')
]
++ map renamedToSwarm [
"claudeCodePackage"
"dashboardPort"
];
options.services.hyperhive.c0re = {
@ -340,19 +330,19 @@ in
};
};
# What every agent in the swarm is handed through the meta-flake render,
# whichever hive builds it. Swarm-wide so that no hive's agents differ.
options.services.hyperhive.swarm = {
# What this hive hands its own agents through the meta-flake render. 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;
example = lib.literalExpression "inputs.nixpkgs-unstable.legacyPackages.x86_64-linux.claude-code";
description = ''
The `claude-code` build every agent runs, or `null` (the
The `claude-code` build every agent on this hive runs, or `null` (the
default) to leave each agent on the `claude-code` from its own
nixpkgs — i.e. whatever `c0re.nixpkgsFlake` resolves to.
This is the one binary the whole swarm is built around, and it
This is the one binary the whole hive is built around, and it
moves fast enough that a release channel routinely trails
unstable by weeks on it. An agent cannot fix that for itself:
agents evaluate against the single nixpkgs hive-c0re injects,
@ -391,7 +381,7 @@ in
dashboardPort = lib.mkOption {
type = lib.types.port;
default = 7000;
description = "TCP port every hive's hive-c0re dashboard listens on.";
description = "TCP port this hive's hive-c0re dashboard listens on.";
};
};
}

View file

@ -62,7 +62,7 @@ in
description = ''
TCP port the gateway proxies non-static requests to. Defaults
to `7000` (hive-c0re's out-of-the-box dashboard port). Operators
who change `services.hyperhive.swarm.dashboardPort` should set
who change `services.hyperhive.deploy.hive-controller.dashboardPort` should set
`upstreamPort` to match — kept as a hardcoded default rather
than a cross-reference to keep this module's options eval
independent of c0re's option tree shape.

View file

@ -61,10 +61,6 @@ in
[ "services" "hyperhive" "otel" "collector" "port" ]
[ "services" "hyperhive" "swarm" "hiveOtel" "collectorPort" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "otel" "extraResourceAttributes" ]
[ "services" "hyperhive" "swarm" "hiveOtel" "extraResourceAttributes" ]
)
];
options.services.hyperhive.otel = {
@ -279,7 +275,7 @@ in
};
# Shared by every hive's otel tier and its agents, so an agent's build
# carries the same values whichever hive renders it.
# carries the same port whichever hive renders it.
options.services.hyperhive.swarm.hiveOtel = {
collectorPort = lib.mkOption {
type = lib.types.port;
@ -296,17 +292,19 @@ in
containers and not from the outside world.
'';
};
};
extraResourceAttributes = lib.mkOption {
type = lib.types.str;
default = "";
example = "deployment.environment=prod";
description = ''
Extra comma-separated entries appended to
`OTEL_RESOURCE_ATTRIBUTES` after the built-in
`service.name` / `agent` / `hive` / `swarm` labels.
'';
};
# Read by this hive's hive-c0re, which stamps it on its own export and
# renders it into its agents', so it is a per-host choice.
options.services.hyperhive.deploy.hive-controller.otel.extraResourceAttributes = lib.mkOption {
type = lib.types.str;
default = "";
example = "deployment.environment=prod";
description = ''
Extra comma-separated entries appended to
`OTEL_RESOURCE_ATTRIBUTES` of this hive's hive-c0re and its agents,
after the built-in `service.name` / `agent` / `hive` / `swarm` labels.
'';
};
config = lib.mkIf config.services.hyperhive.otel.enable (

View file

@ -23,12 +23,12 @@ let
hyperhiveCfg = config.services.hyperhive;
# The `claude-code` the agents run, so this container does not trail them on
# the CLI; `fallback`, the container's own nixpkgs', only where the swarm pins
# the CLI; `fallback`, the container's own nixpkgs', only where this host pins
# none.
claudeCode =
fallback:
if hyperhiveCfg.swarm.claudeCodePackage != null then
hyperhiveCfg.swarm.claudeCodePackage
if hyperhiveCfg.deploy.hive-controller.claudeCodePackage != null then
hyperhiveCfg.deploy.hive-controller.claudeCodePackage
else
fallback;
cfg = hyperhiveCfg.deploy.claude-login;

View file

@ -24,12 +24,12 @@ let
hyperhiveCfg = config.services.hyperhive;
# The `claude-code` the agents run, so this container does not trail them on
# the CLI; `fallback`, the container's own nixpkgs', only where the swarm pins
# the CLI; `fallback`, the container's own nixpkgs', only where this host pins
# none.
claudeCode =
fallback:
if hyperhiveCfg.swarm.claudeCodePackage != null then
hyperhiveCfg.swarm.claudeCodePackage
if hyperhiveCfg.deploy.hive-controller.claudeCodePackage != null then
hyperhiveCfg.deploy.hive-controller.claudeCodePackage
else
fallback;
cfg = hyperhiveCfg.deploy.review-classifier;

View file

@ -99,11 +99,11 @@ let
deploy.builder.enable = false;
};
# Same, with the claude-code build the agents run pinned for the swarm. Any
# Same, with the claude-code build the agents run pinned on this host. Any
# package serves: only the path the containers are pointed at is read.
swarmServicesPinnedClaude = hiveWithController {
deploy.allSwarmServices = true;
swarm.claudeCodePackage = pkgs.hello;
deploy.hive-controller.claudeCodePackage = pkgs.hello;
};
# Same, with one of those services placed on another host. Every derivation
@ -284,7 +284,7 @@ let
# The CLI is the one the agents run, not the container's own nixpkgs'.
# Unpinned is the control: it falls back to the container's own `claude-code`.
# serve.json is what hive-c0re renders every agent's `claudeCodePath` from.
name = "the agents, the claude login minter and the review classifier run the claude-code the swarm pins";
name = "the agents, the claude login minter and the review classifier run the claude-code this host pins";
ok =
let
claudeOf =