hive-c0re: render the new agent option paths into generated agent flakes

meta.rs writes each agent's flake, and it still named the pre-move
`hyperhive.*` paths — so every agent rebuild would print a rename
deprecation warning about a line no human wrote and no operator could fix.
A warning nobody can act on trains everyone to ignore the ones that matter,
which is the whole value of the alias shims.

Repoints the FORWARDED_VAR_OPTIONS table and every other emitted option
assignment (otel.*, docs.source, claudeCodePath, github.enable, user.name,
claudeMemoryMaxBytes) to `services.hyperhive.agent.*`, with the test
expectations that pin the rendered text. The flake input named `hyperhive`
(`hyperhive.url`, `hyperhive.inputs.nixpkgs.follows`,
`hyperhive.nixosConfigurations.*`), hive-tier `services.hyperhive.*` paths,
and the `@hyperhive.local` git identity share the word and are untouched.

Also repoints the same option paths where they appear in comments, rustdoc
and runtime message strings across the other crates — a refusal message
naming `hyperhive.allowedRecipients` sends an operator to a path that will
stop existing. Prose under docs/ is deliberately not in this commit.

Refs #4473
This commit is contained in:
atlas 2026-09-17 20:19:30 +02:00
commit 6ea81aae65
30 changed files with 138 additions and 121 deletions

View file

@ -202,7 +202,7 @@ pub async fn build_all(hive: &crate::coordinator::HiveEnv) -> Vec<ContainerView>
// stale sentinel state is not meaningful when the harness isn't up.
//
// The first half doesn't apply to an api-key agent
// (`hyperhive.useApiKey`, stamped as `api_key_mode` in the
// (`services.hyperhive.agent.useApiKey`, stamped as `api_key_mode` in the
// consolidated state file by `hive_agent::harness_state::write_api_key_mode`):
// its `~/.claude/` is empty by design (no OAuth flow to complete),
// so an empty dir there means nothing — only the auth-failed

View file

@ -47,7 +47,7 @@ pub struct Coordinator {
pub hyperhive_flake: String,
/// URL of the narrow `docs/` source (no fragment). Inlined into the
/// meta `flake.nix` as `inputs.hyperhive-docs.url` and threaded to
/// each agent as `hyperhive.docs.source`. Its own store path
/// each agent as `services.hyperhive.agent.docs.source`. Its own store path
/// so doc edits don't re-hash `hyperhive_flake`.
pub hyperhive_docs_flake: String,
/// Store-path URL of the nixpkgs to wire into the meta flake as
@ -59,7 +59,7 @@ pub struct Coordinator {
/// `follows = "hyperhive/nixpkgs"` behaviour.
pub nixpkgs_flake: String,
/// Store path of the `claude-code` build every agent runs, written
/// into each per-agent flake as `hyperhive.claudeCodePath`. Set by
/// into each per-agent flake as `services.hyperhive.agent.claudeCodePath`. Set by
/// the NixOS module option `services.hyperhive.c0re.claudeCodePackage`
/// (which resolves the package and hands us its path). `None` = every
/// agent keeps the `claude-code` from its own nixpkgs.
@ -228,13 +228,13 @@ pub struct HiveEnv {
pub hyperhive_flake: String,
/// Store-path URL of the narrow `docs/` source, wired into the meta
/// flake as `inputs.hyperhive-docs.url` and threaded to each agent as
/// `hyperhive.docs.source`. Separate from `hyperhive_flake`
/// `services.hyperhive.agent.docs.source`. Separate from `hyperhive_flake`
/// so a doc edit only re-locks this input, not the whole source.
pub hyperhive_docs_flake: String,
pub nixpkgs_flake: String,
/// Store path of the `claude-code` agents run, or `None` for "each
/// agent keeps the one out of its own nixpkgs". Travels into the
/// container as `hyperhive.claudeCodePath` — a plain string, kept
/// container as `services.hyperhive.agent.claudeCodePath` — a plain string, kept
/// alive host-side by the module that resolved it.
pub claude_code_path: Option<String>,
pub dashboard_port: u16,

View file

@ -739,21 +739,27 @@ const FORWARDED_VAR_OPTIONS: &[(&str, &str)] = &[
// whose option says no queue coordinates and whose env says otherwise logs the
// partial-config error rather than half-connecting, which is the same
// failure this map's doc describes and the reason both halves are wired.
("HIVE_AGENT_NATS_URL", "hyperhive.queue.natsUrl"),
(
"HIVE_AGENT_NATS_URL",
"services.hyperhive.agent.queue.natsUrl",
),
(
"HIVE_AGENT_OIDC_TOKEN_ENDPOINT",
"hyperhive.queue.tokenEndpoint",
"services.hyperhive.agent.queue.tokenEndpoint",
),
// Read at build time to decide whether `swarm-logs` is installed at all —
// see `nix/agent-modules/logs.nix`. Both halves wired for this map's own
// reason: an agent whose option says no log store and whose env says
// otherwise has a binary that is not on PATH and coordinates that claim
// it is.
("HIVE_AGENT_LOGS_QUERY_URL", "hyperhive.logs.queryUrl"),
("HIVE_FORGE_URL", "hyperhive.forge.url"),
("HIVE_MATRIX_URL", "hyperhive.matrix.url"),
("HYPERHIVE_HIVE_NAME", "hyperhive.hiveName"),
("HYPERHIVE_SWARM_NAME", "hyperhive.swarmName"),
(
"HIVE_AGENT_LOGS_QUERY_URL",
"services.hyperhive.agent.logs.queryUrl",
),
("HIVE_FORGE_URL", "services.hyperhive.agent.forge.url"),
("HIVE_MATRIX_URL", "services.hyperhive.agent.matrix.url"),
("HYPERHIVE_HIVE_NAME", "services.hyperhive.agent.hiveName"),
("HYPERHIVE_SWARM_NAME", "services.hyperhive.agent.swarmName"),
];
/// Render the forwarded-var option assignments for one agent's module block.
@ -874,17 +880,17 @@ struct OtelConfig {
protocol: String,
extra_resource_attributes: Option<String>,
metric_interval_ms: Option<u64>,
/// `HYPERHIVE_OTEL_DEBUG=1` → `hyperhive.otel.debug = true` →
/// `HYPERHIVE_OTEL_DEBUG=1` → `services.hyperhive.agent.otel.debug = true` →
/// `CLAUDE_CODE_OTEL_DIAG_STDERR=1` in every agent's env.
debug: bool,
}
/// Read the hive-wide OTEL config from env, or `None` when OTEL is off.
/// Mirrors `hive_ca_source` — host state surfaced to the meta renderer
/// so it can bake build-time `hyperhive.otel.*` config into each agent
/// so it can bake build-time `services.hyperhive.agent.otel.*` config into each agent
/// (the per-agent options the harness modules consume). Returns `None`
/// when the endpoint signal is absent so the renderer emits no
/// `hyperhive.otel.*` lines and agents keep the disabled default.
/// `services.hyperhive.agent.otel.*` lines and agents keep the disabled default.
fn otel_config() -> Option<OtelConfig> {
let endpoint = std::env::var("HYPERHIVE_OTEL_ENDPOINT")
.ok()
@ -1101,9 +1107,9 @@ where
}
// Narrow `docs/` source as its own input so a doc edit only
// re-locks THIS input instead of re-hashing the whole `hyperhive`
// source. Threaded to each agent below as `hyperhive.docs.source`.
// source. Threaded to each agent below as `services.hyperhive.agent.docs.source`.
// Empty = hive-c0re not built with the option wired up (legacy);
// agents then keep the harness default (`hyperhive.packages.reference-docs`).
// agents then keep the harness default (`services.hyperhive.agent.packages.reference-docs`).
if !docs_flake.is_empty() {
// `flake = false`: the docs/ tree is a plain source (no flake.nix),
// so nix must treat it as raw source, not evaluate it as a flake.
@ -1169,11 +1175,11 @@ where
);
// Point the in-container docs dir (`$HIVE_DOCS_DIR`) at the narrow
// `hyperhive-docs` input instead of the harness default
// (`hyperhive.packages.reference-docs`, built from the now-docs-stripped source).
// (`services.hyperhive.agent.packages.reference-docs`, built from the now-docs-stripped source).
// `inputs."hyperhive-docs"` is reachable via the outputs `@inputs`
// capture. Emitted only when the input exists (docs_flake non-empty).
if !docs_flake.is_empty() {
out.push_str(" hyperhive.docs.source = inputs.\"hyperhive-docs\".outPath;\n");
out.push_str(" services.hyperhive.agent.docs.source = inputs.\"hyperhive-docs\".outPath;\n");
}
// The `claude-code` agents run, as a bare store path rather than a
// flake input: containers share the host's `/nix/store`, so the
@ -1185,7 +1191,10 @@ where
// spelled out here is text and references nothing.
// `None` = no override; agents keep their own nixpkgs' `claude-code`.
if let Some(path) = claude_code_path {
let _ = writeln!(out, " hyperhive.claudeCodePath = \"{path}\";");
let _ = writeln!(
out,
" services.hyperhive.agent.claudeCodePath = \"{path}\";"
);
}
// CA trust: embed the hive's trust anchors so each agent validates
// them at build time — the hive's own self-signed CA when active,
@ -1209,41 +1218,44 @@ where
);
}
// Hive-wide OTEL stats export (`services.hyperhive.otel.*`): inject the
// build-time `hyperhive.otel.*` config the harness modules consume (its
// build-time `services.hyperhive.agent.otel.*` config the harness modules consume (its
// otelEnv + otelExecStart wrapper + LoadCredential). Host-driven, so
// the same config lands on every agent; emitted only when enabled.
// Mirrors the CA-cert injection above — host state -> build-time agent
// module config.
if let Some(otel) = otel_config() {
let esc = |s: &str| s.replace('\\', "\\\\").replace('"', "\\\"");
out.push_str(" hyperhive.otel.enable = true;\n");
out.push_str(" services.hyperhive.agent.otel.enable = true;\n");
let _ = writeln!(
out,
" hyperhive.otel.endpoint = \"{}\";",
" services.hyperhive.agent.otel.endpoint = \"{}\";",
esc(&otel.endpoint)
);
let _ = writeln!(
out,
" hyperhive.otel.protocol = \"{}\";",
" services.hyperhive.agent.otel.protocol = \"{}\";",
esc(&otel.protocol)
);
if let Some(attrs) = &otel.extra_resource_attributes {
let _ = writeln!(
out,
" hyperhive.otel.extraResourceAttributes = \"{}\";",
" services.hyperhive.agent.otel.extraResourceAttributes = \"{}\";",
esc(attrs)
);
}
if let Some(ms) = otel.metric_interval_ms {
// Int option — emit a bare numeric literal (no quotes). `ms` is a
// parsed u64, so it can't inject anything into the rendered nix.
let _ = writeln!(out, " hyperhive.otel.metricIntervalMs = {ms};");
let _ = writeln!(
out,
" services.hyperhive.agent.otel.metricIntervalMs = {ms};"
);
}
if otel.debug {
out.push_str(" hyperhive.otel.debug = true;\n");
out.push_str(" services.hyperhive.agent.otel.debug = true;\n");
}
}
// Agent-facing service URLs (`hyperhive.forge.url`, `hyperhive.matrix.url`):
// Agent-facing service URLs (`services.hyperhive.agent.forge.url`, `services.hyperhive.agent.matrix.url`):
// emit the host's real values as build-time agent config, the same
// host-state -> agent-module shape as the otel block above.
//
@ -1271,21 +1283,21 @@ where
// operation the tests exercise directly.
push_forwarded_var_options(&mut out, &forwarded_env_vars());
// GitHub integration is on by default in every agent
// (`hyperhive.github.enable`); the host turns it off hive-wide via
// (`services.hyperhive.agent.github.enable`); the host turns it off hive-wide via
// `services.hyperhive.github.enable = false`, surfaced here as the
// `HYPERHIVE_GITHUB_DISABLED` env on hive-c0re's unit. Only the OFF
// override is propagated — the enabled default needs no per-agent line.
if std::env::var_os("HYPERHIVE_GITHUB_DISABLED").is_some() {
out.push_str(" hyperhive.github.enable = false;\n");
out.push_str(" services.hyperhive.agent.github.enable = false;\n");
}
out.push_str(
r#" # The harness service inside the container runs as a
# non-root unix user named after the agent (`damocles`,
# `iris`, `root`, ). UID auto-assigned by NixOS; the
# per-agent override here is what makes
# `hyperhive.user.name` match the agent's identity
# `services.hyperhive.agent.user.name` match the agent's identity
# instead of the harness default of `"agent"`.
hyperhive.user.name = name;
services.hyperhive.agent.user.name = name;
# Without this an agent container is called `nixos` and ships
# every log line under that: nixpkgs sets the hostname from the
# attr name in the `containers.<name>` submodule, and an
@ -1295,7 +1307,7 @@ where
# nspawn itself already call this container, so logs join to
# both without a prefix transform.
networking.hostName = "h-${name}";
hyperhive.claudeMemoryMaxBytes = memoryMaxBytes;
services.hyperhive.agent.claudeMemoryMaxBytes = memoryMaxBytes;
programs.git.config.user = {
name = name;
email = "${name}@hyperhive.local";
@ -1418,7 +1430,7 @@ where
// Effective `MemoryMax=` for this agent (per-agent override, else
// the hive-wide default), turned into a raw byte count so
// the agent's claude-settings module can derive a JSC heap ceiling
// (see `hyperhive.claudeMemoryMaxBytes`). `null` when the
// (see `services.hyperhive.agent.claudeMemoryMaxBytes`). `null` when the
// effective value is `"infinity"` or a RAM percentage — no
// byte count to derive, dependent env var stays unset, same as
// today's no-cap behavior.
@ -1759,7 +1771,7 @@ mod tests {
"old-style follows must not appear when flake args are set:\n{out}"
);
// the narrow docs source is its own non-flake input, and each
// agent's docs dir resolves from it rather than hyperhive.packages.reference-docs.
// agent's docs dir resolves from it rather than services.hyperhive.agent.packages.reference-docs.
assert!(
out.contains("hyperhive-docs.url = \"path:/nix/store/bbbb-hyperhive-docs-source\""),
"expected hyperhive-docs input url:\n{out}"
@ -1769,7 +1781,9 @@ mod tests {
"docs source is not a flake, must be flake = false:\n{out}"
);
assert!(
out.contains("hyperhive.docs.source = inputs.\"hyperhive-docs\".outPath;"),
out.contains(
"services.hyperhive.agent.docs.source = inputs.\"hyperhive-docs\".outPath;"
),
"expected per-agent docs source wired to the input:\n{out}"
);
}
@ -1777,7 +1791,7 @@ mod tests {
#[test]
fn render_flake_omits_docs_input_when_docs_flake_empty() {
// Legacy / not-wired-up: empty docs_flake emits no docs input and
// leaves each agent on the harness default (hyperhive.packages.reference-docs).
// leaves each agent on the harness default (services.hyperhive.agent.packages.reference-docs).
let out = render_flake(
"github:example/hyperhive",
"",
@ -1850,7 +1864,9 @@ mod tests {
&[sample_spec("alice", false, 9001)],
);
assert!(
out.contains("hyperhive.claudeCodePath = \"/nix/store/cccc-claude-code-2.1.220\";"),
out.contains(
"services.hyperhive.agent.claudeCodePath = \"/nix/store/cccc-claude-code-2.1.220\";"
),
"claude path assigned as a plain string literal:\n{out}"
);
assert!(
@ -2046,9 +2062,9 @@ mod tests {
);
assert_eq!(
out,
" hyperhive.forge.url = \"http://forge.example.test\";\n\
\x20 hyperhive.matrix.url = \"http://matrix.example.test\";\n\
\x20 hyperhive.hiveName = \"pr1ma\";\n",
" services.hyperhive.agent.forge.url = \"http://forge.example.test\";\n\
\x20 services.hyperhive.agent.matrix.url = \"http://matrix.example.test\";\n\
\x20 services.hyperhive.agent.hiveName = \"pr1ma\";\n",
"expected exactly the mapped options, in input order, indented for the module block"
);
}
@ -2079,8 +2095,8 @@ mod tests {
);
assert_eq!(
out,
" hyperhive.queue.natsUrl = \"nats://10.42.0.1:4222\";\n\
\x20 hyperhive.queue.tokenEndpoint = \"https://auth.t.local/api/oidc/token\";\n"
" services.hyperhive.agent.queue.natsUrl = \"nats://10.42.0.1:4222\";\n\
\x20 services.hyperhive.agent.queue.tokenEndpoint = \"https://auth.t.local/api/oidc/token\";\n"
);
}
@ -2184,11 +2200,11 @@ mod tests {
std::env::remove_var("HIVE_MATRIX_URL");
}
let forge_opt_at = out
.find("hyperhive.forge.url = \"http://forge.example.test\"")
.expect("hyperhive.forge.url must be rendered from HIVE_FORGE_URL");
.find("services.hyperhive.agent.forge.url = \"http://forge.example.test\"")
.expect("services.hyperhive.agent.forge.url must be rendered from HIVE_FORGE_URL");
let matrix_opt_at = out
.find("hyperhive.matrix.url = \"http://matrix.example.test\"")
.expect("hyperhive.matrix.url must be rendered from HIVE_MATRIX_URL");
.find("services.hyperhive.agent.matrix.url = \"http://matrix.example.test\"")
.expect("services.hyperhive.agent.matrix.url must be rendered from HIVE_MATRIX_URL");
let env_block_at = out
.find("environment.variables = {")
.expect("per-agent environment.variables block must exist");
@ -2332,8 +2348,8 @@ mod tests {
#[test]
fn render_flake_injects_otel_when_signalled() {
// services.hyperhive.otel.* -> HYPERHIVE_OTEL_* on hive-c0re's unit
// -> injected as build-time hyperhive.otel.* into every agent. With
// no endpoint signal, no hyperhive.otel lines are emitted (agents
// -> injected as build-time services.hyperhive.agent.otel.* into every agent. With
// no endpoint signal, no services.hyperhive.agent.otel lines are emitted (agents
// keep the the harness modules disabled default).
//
// Serialised against every other env-mutating test in the crate.
@ -2376,32 +2392,33 @@ mod tests {
let off = render();
assert!(
on_minimal.contains("hyperhive.otel.enable = true;"),
on_minimal.contains("services.hyperhive.agent.otel.enable = true;"),
"otel enable must be injected:\n{on_minimal}"
);
assert!(
on_minimal.contains("hyperhive.otel.endpoint = \"https://c.example/otel\";"),
on_minimal
.contains("services.hyperhive.agent.otel.endpoint = \"https://c.example/otel\";"),
"otel endpoint must be injected:\n{on_minimal}"
);
assert!(
on_minimal.contains("hyperhive.otel.protocol = \"grpc\";"),
on_minimal.contains("services.hyperhive.agent.otel.protocol = \"grpc\";"),
"otel protocol must be injected:\n{on_minimal}"
);
// Optional fields absent when unset.
assert!(
!on_minimal.contains("hyperhive.otel.extraResourceAttributes"),
!on_minimal.contains("services.hyperhive.agent.otel.extraResourceAttributes"),
"extraResourceAttributes must not appear when unset:\n{on_minimal}"
);
assert!(
on_full.contains(
"hyperhive.otel.extraResourceAttributes = \"deployment.environment=prod\";"
"services.hyperhive.agent.otel.extraResourceAttributes = \"deployment.environment=prod\";"
),
"extraResourceAttributes must be injected when set:\n{on_full}"
);
assert!(
!off.contains("hyperhive.otel"),
!off.contains("services.hyperhive.agent.otel"),
"no otel lines when disabled:\n{off}"
);
}
@ -2409,7 +2426,7 @@ mod tests {
#[test]
fn render_flake_injects_github_disable_only_when_signalled() {
// services.hyperhive.github.enable = false -> HYPERHIVE_GITHUB_DISABLED
// on hive-c0re's unit -> `hyperhive.github.enable = false` injected into
// on hive-c0re's unit -> `services.hyperhive.agent.github.enable = false` injected into
// every agent. On by default, so nothing is emitted unless disabled.
//
// Serialised against every other env-mutating test in the crate — the
@ -2441,11 +2458,11 @@ mod tests {
std::env::remove_var("HYPERHIVE_GITHUB_DISABLED");
}
assert!(
!on_default.contains("hyperhive.github.enable"),
!on_default.contains("services.hyperhive.agent.github.enable"),
"github.enable must not be emitted by default (agents keep the true default):\n{on_default}"
);
assert!(
disabled.contains("hyperhive.github.enable = false;"),
disabled.contains("services.hyperhive.agent.github.enable = false;"),
"github.enable = false must be injected when the host disables it:\n{disabled}"
);
}

View file

@ -403,7 +403,7 @@ pub async fn write_agent_matrix_token(
/// (running as root). Writes `<state>/github-token` 0600, chowned to the agent
/// user so the `gh` wrapper / git credential helper can read it from inside the
/// container. Single account per agent — no account suffix. The token value is
/// operator-supplied (for the agent's GitHub integration, `hyperhive.github.enable`).
/// operator-supplied (for the agent's GitHub integration, `services.hyperhive.agent.github.enable`).
///
/// # Errors
///