fix(#2898): carry hive/swarm display names as build-time options
The OTEL resource attributes are baked into every agent's
managed-settings.json at evaluation time, but claude-settings.nix read
the names from the container's environment.variables - where they are
never set. meta.rs forwards them as runtime env only, so the reader hit
its "unknown" fallback and every agent shipped
service.name=hyperhive-agent,agent=<a>,hive=unknown,swarm=unknown
on every metric, while the same process's env held pr1ma/constellation.
Measured in this container's /etc/claude-code/managed-settings.json.
The map from forwarded env var to agent option already existed for the
service URLs, with a doc comment naming this exact hazard: "setting only
one leaves the other on its default". The names were simply never added
to it. They are now, and the constant is renamed FORWARDED_VAR_OPTIONS
since it no longer holds only URLs.
hyperhive.hiveName / hyperhive.swarmName follow the forge.url shape:
nullOr str defaulting to null, where null means the hive did not name
itself and "unknown" is an honest label rather than a guess baked at
eval time.
Also fixes, unasked: CLAUDE_REMOTE_CONTROL_SESSION_NAME_PREFIX reads the
same value, so remote-control sessions were named "unknown-<agent>".
Gated with nix, not just cargo - the blast radius here is module eval,
which fmt/clippy/test cannot see:
agent-base assertions -> [] (no failures)
extendModules with both options -> hive=pr1ma,swarm=constellation
Note the value is baked, so every agent needs a rebuild before the new
label appears on its metrics.
This commit is contained in:
parent
4c37ce9150
commit
772482a52a
2 changed files with 79 additions and 21 deletions
|
|
@ -707,13 +707,24 @@ const FORWARDED_VARS: &[&str] = &[
|
|||
/// Both exist because they're consumed at different times: the option is baked
|
||||
/// into scripts at build time (tea-login bakes `FORGE_URL` from it), the env
|
||||
/// var is read at runtime. Setting only one leaves the other on its default,
|
||||
/// which is how a hive ends up with two disagreeing answers for one URL.
|
||||
const SERVICE_URL_OPTIONS: &[(&str, &str)] = &[
|
||||
/// which is how a hive ends up with two disagreeing answers for one value.
|
||||
///
|
||||
/// The hive/swarm display names are here for exactly that reason, learned the
|
||||
/// hard way: they were forwarded as runtime env only, while
|
||||
/// `claude-settings.nix` read them from the container's `environment.variables`
|
||||
/// at *eval* time — where they were never set. Every agent baked
|
||||
/// `hive=unknown,swarm=unknown` into its OTEL resource attributes and shipped
|
||||
/// that label on every metric, while the same process's env held the right
|
||||
/// answer. Half-wiring this map is not a missing nicety; it is a value that
|
||||
/// evaluates fine and is silently wrong.
|
||||
const FORWARDED_VAR_OPTIONS: &[(&str, &str)] = &[
|
||||
("HIVE_FORGE_URL", "hyperhive.forge.url"),
|
||||
("HIVE_MATRIX_URL", "hyperhive.matrix.url"),
|
||||
("HYPERHIVE_HIVE_NAME", "hyperhive.hiveName"),
|
||||
("HYPERHIVE_SWARM_NAME", "hyperhive.swarmName"),
|
||||
];
|
||||
|
||||
/// Render the service-URL option assignments for one agent's module block.
|
||||
/// Render the forwarded-var option assignments for one agent's module block.
|
||||
///
|
||||
/// Split out of `render_flake` so it can be tested without touching process
|
||||
/// env: the render-level tests have to `set_var`, which makes them race each
|
||||
|
|
@ -726,10 +737,10 @@ const SERVICE_URL_OPTIONS: &[(&str, &str)] = &[
|
|||
/// For a service the hive cannot run without, silence would instead produce a
|
||||
/// fleet of agents quietly missing an integration, so those are checked by
|
||||
/// [`require_service_urls`] before this is called.
|
||||
fn push_service_url_options(out: &mut String, vars: &[(&'static str, String)]) {
|
||||
fn push_forwarded_var_options(out: &mut String, vars: &[(&'static str, String)]) {
|
||||
use std::fmt::Write as _;
|
||||
for (var, val) in vars {
|
||||
let Some((_, option)) = SERVICE_URL_OPTIONS.iter().find(|(name, _)| name == var) else {
|
||||
let Some((_, option)) = FORWARDED_VAR_OPTIONS.iter().find(|(name, _)| name == var) else {
|
||||
continue;
|
||||
};
|
||||
let escaped = val.replace('\\', "\\\\").replace('"', "\\\"");
|
||||
|
|
@ -751,7 +762,7 @@ const REQUIRED_SERVICE_URL_VARS: &[&str] = &["HIVE_FORGE_URL"];
|
|||
/// `vars`.
|
||||
///
|
||||
/// Pure over the already-collected pairs so it can be tested without touching
|
||||
/// process env (the same reason [`push_service_url_options`] is split out).
|
||||
/// process env (the same reason [`push_forwarded_var_options`] is split out).
|
||||
///
|
||||
/// Checked by `sync_agents` — the point at which the hive commits a rendered
|
||||
/// flake to disk — rather than inside the renderer. Rendering is a pure string
|
||||
|
|
@ -1238,7 +1249,7 @@ where
|
|||
// missing `HIVE_FORGE_URL` before it writes anything (`require_service_urls`).
|
||||
// The check lives there rather than here because rendering is a pure string
|
||||
// operation the tests exercise directly.
|
||||
push_service_url_options(&mut out, &forwarded_env_vars());
|
||||
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.github.enable = false`, surfaced here as the
|
||||
|
|
@ -1939,21 +1950,27 @@ mod tests {
|
|||
// parallel runner — the first version of this test did exactly that
|
||||
// and failed for that reason rather than for a real defect.
|
||||
let mut out = String::new();
|
||||
push_service_url_options(
|
||||
push_forwarded_var_options(
|
||||
&mut out,
|
||||
&[
|
||||
("HIVE_FORGE_URL", "http://forge.example.test".to_string()),
|
||||
("HIVE_MATRIX_URL", "http://matrix.example.test".to_string()),
|
||||
// Forwarded but not a service URL — must be ignored here, it
|
||||
// belongs in globalEnvironment only.
|
||||
// Mapped too, and that is the fix this test now guards: the
|
||||
// display names used to be forwarded as runtime env *only*,
|
||||
// so the build-time reader fell back to "unknown" and every
|
||||
// agent shipped that label on every metric.
|
||||
("HYPERHIVE_HIVE_NAME", "pr1ma".to_string()),
|
||||
// Forwarded but genuinely unmapped — proves the map is a
|
||||
// filter, not a pass-through.
|
||||
("HYPERHIVE_HIVE_DOMAIN", "pr1ma.darkest.space".to_string()),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
out,
|
||||
" hyperhive.forge.url = \"http://forge.example.test\";\n\
|
||||
\x20 hyperhive.matrix.url = \"http://matrix.example.test\";\n",
|
||||
"expected exactly the two service URL options, indented for the agent module block"
|
||||
\x20 hyperhive.matrix.url = \"http://matrix.example.test\";\n\
|
||||
\x20 hyperhive.hiveName = \"pr1ma\";\n",
|
||||
"expected exactly the mapped options, in input order, indented for the module block"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1987,11 +2004,17 @@ mod tests {
|
|||
// so absence is an absent integration rather than a misdirected one.
|
||||
// Required services don't reach here: `require_service_urls` rejects
|
||||
// them first.
|
||||
// `HYPERHIVE_HIVE_DOMAIN` is forwarded as env but carries no option —
|
||||
// deliberately picked over a mapped var, since the whole point is a
|
||||
// forwarded var the option map does not know about.
|
||||
let mut out = String::new();
|
||||
push_service_url_options(&mut out, &[("HYPERHIVE_HIVE_NAME", "pr1ma".to_string())]);
|
||||
push_forwarded_var_options(
|
||||
&mut out,
|
||||
&[("HYPERHIVE_HIVE_DOMAIN", "pr1ma.darkest.space".to_string())],
|
||||
);
|
||||
assert!(
|
||||
out.is_empty(),
|
||||
"no option should be emitted without a forwarded URL, got:\n{out}"
|
||||
"no option should be emitted for an unmapped forwarded var, got:\n{out}"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -2001,7 +2024,7 @@ mod tests {
|
|||
// would end the string and change the surrounding config rather than
|
||||
// merely corrupting one value.
|
||||
let mut out = String::new();
|
||||
push_service_url_options(
|
||||
push_forwarded_var_options(
|
||||
&mut out,
|
||||
&[("HIVE_FORGE_URL", "http://x/\"; evil = \"".to_string())],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -14,12 +14,16 @@ let
|
|||
# Hive-wide OpenTelemetry config (host-driven; baked in per-agent by
|
||||
# meta.rs `otel_config`).
|
||||
otelCfg = config.hyperhive.otel;
|
||||
# Hive/swarm display names are forwarded into each agent's build by
|
||||
# meta.rs as `environment.variables` (per-agent, build-time strings),
|
||||
# so they can be baked into the resource attributes below without a
|
||||
# runtime shell. Absent (option unset) → "unknown".
|
||||
hiveDisplayName = config.environment.variables.HYPERHIVE_HIVE_NAME or "unknown";
|
||||
swarmDisplayName = config.environment.variables.HYPERHIVE_SWARM_NAME or "unknown";
|
||||
# Hive/swarm display names, read from the per-agent options meta.rs
|
||||
# renders (NOT from `environment.variables` — those carry the same names
|
||||
# at *runtime* only, so reading them here silently yielded "unknown" on
|
||||
# every agent while the process env held the right answer). `null` means
|
||||
# the hive did not name itself; "unknown" is then an honest label rather
|
||||
# than a guess.
|
||||
hiveDisplayName =
|
||||
if config.hyperhive.hiveName == null then "unknown" else config.hyperhive.hiveName;
|
||||
swarmDisplayName =
|
||||
if config.hyperhive.swarmName == null then "unknown" else config.hyperhive.swarmName;
|
||||
# Effective per-agent MemoryMax=, in bytes, injected by meta.rs's
|
||||
# per-agent flake render (`hyperhive.claudeMemoryMaxBytes`). `null`
|
||||
# when the effective cap is unbounded ("infinity") or a RAM
|
||||
|
|
@ -215,6 +219,37 @@ in
|
|||
# the agent's *last rebuild* — `set-limits` still applies the
|
||||
# cgroup cap live via a drop-in reload, but this derived heap ceiling
|
||||
# needs a rebuild to pick up a new value.
|
||||
options.hyperhive.hiveName = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
internal = true;
|
||||
description = ''
|
||||
Human-readable hive name, rendered per-agent by
|
||||
`meta.rs::render_flake` from the host's
|
||||
`services.hyperhive.hiveName`. Baked into the OTEL resource
|
||||
attributes at build time, which is why it is an option and not
|
||||
just the `HYPERHIVE_HIVE_NAME` env var: the env var is read at
|
||||
runtime, this is read during evaluation, and wiring only one of
|
||||
the two is how every agent ended up reporting `hive=unknown`.
|
||||
|
||||
`null` means the hive did not name itself.
|
||||
'';
|
||||
};
|
||||
|
||||
options.hyperhive.swarmName = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
internal = true;
|
||||
description = ''
|
||||
Human-readable swarm name, rendered per-agent by
|
||||
`meta.rs::render_flake` from the host's
|
||||
`services.hyperhive.swarmName`. Same build-time/runtime split as
|
||||
`hyperhive.hiveName`.
|
||||
|
||||
`null` means the hive is not part of a named swarm.
|
||||
'';
|
||||
};
|
||||
|
||||
options.hyperhive.claudeMemoryMaxBytes = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.ints.positive;
|
||||
default = null;
|
||||
|
|
|
|||
Loading…
Reference in a new issue