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
|
/// 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
|
/// 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,
|
/// 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.
|
/// which is how a hive ends up with two disagreeing answers for one value.
|
||||||
const SERVICE_URL_OPTIONS: &[(&str, &str)] = &[
|
///
|
||||||
|
/// 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_FORGE_URL", "hyperhive.forge.url"),
|
||||||
("HIVE_MATRIX_URL", "hyperhive.matrix.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
|
/// 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
|
/// 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
|
/// 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
|
/// fleet of agents quietly missing an integration, so those are checked by
|
||||||
/// [`require_service_urls`] before this is called.
|
/// [`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 _;
|
use std::fmt::Write as _;
|
||||||
for (var, val) in vars {
|
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;
|
continue;
|
||||||
};
|
};
|
||||||
let escaped = val.replace('\\', "\\\\").replace('"', "\\\"");
|
let escaped = val.replace('\\', "\\\\").replace('"', "\\\"");
|
||||||
|
|
@ -751,7 +762,7 @@ const REQUIRED_SERVICE_URL_VARS: &[&str] = &["HIVE_FORGE_URL"];
|
||||||
/// `vars`.
|
/// `vars`.
|
||||||
///
|
///
|
||||||
/// Pure over the already-collected pairs so it can be tested without touching
|
/// 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
|
/// 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
|
/// 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`).
|
// missing `HIVE_FORGE_URL` before it writes anything (`require_service_urls`).
|
||||||
// The check lives there rather than here because rendering is a pure string
|
// The check lives there rather than here because rendering is a pure string
|
||||||
// operation the tests exercise directly.
|
// 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
|
// GitHub integration is on by default in every agent
|
||||||
// (`hyperhive.github.enable`); the host turns it off hive-wide via
|
// (`hyperhive.github.enable`); the host turns it off hive-wide via
|
||||||
// `services.hyperhive.github.enable = false`, surfaced here as the
|
// `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
|
// parallel runner — the first version of this test did exactly that
|
||||||
// and failed for that reason rather than for a real defect.
|
// and failed for that reason rather than for a real defect.
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
push_service_url_options(
|
push_forwarded_var_options(
|
||||||
&mut out,
|
&mut out,
|
||||||
&[
|
&[
|
||||||
("HIVE_FORGE_URL", "http://forge.example.test".to_string()),
|
("HIVE_FORGE_URL", "http://forge.example.test".to_string()),
|
||||||
("HIVE_MATRIX_URL", "http://matrix.example.test".to_string()),
|
("HIVE_MATRIX_URL", "http://matrix.example.test".to_string()),
|
||||||
// Forwarded but not a service URL — must be ignored here, it
|
// Mapped too, and that is the fix this test now guards: the
|
||||||
// belongs in globalEnvironment only.
|
// 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()),
|
("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!(
|
assert_eq!(
|
||||||
out,
|
out,
|
||||||
" hyperhive.forge.url = \"http://forge.example.test\";\n\
|
" hyperhive.forge.url = \"http://forge.example.test\";\n\
|
||||||
\x20 hyperhive.matrix.url = \"http://matrix.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.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.
|
// so absence is an absent integration rather than a misdirected one.
|
||||||
// Required services don't reach here: `require_service_urls` rejects
|
// Required services don't reach here: `require_service_urls` rejects
|
||||||
// them first.
|
// 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();
|
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!(
|
assert!(
|
||||||
out.is_empty(),
|
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
|
// would end the string and change the surrounding config rather than
|
||||||
// merely corrupting one value.
|
// merely corrupting one value.
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
push_service_url_options(
|
push_forwarded_var_options(
|
||||||
&mut out,
|
&mut out,
|
||||||
&[("HIVE_FORGE_URL", "http://x/\"; evil = \"".to_string())],
|
&[("HIVE_FORGE_URL", "http://x/\"; evil = \"".to_string())],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,16 @@ let
|
||||||
# Hive-wide OpenTelemetry config (host-driven; baked in per-agent by
|
# Hive-wide OpenTelemetry config (host-driven; baked in per-agent by
|
||||||
# meta.rs `otel_config`).
|
# meta.rs `otel_config`).
|
||||||
otelCfg = config.hyperhive.otel;
|
otelCfg = config.hyperhive.otel;
|
||||||
# Hive/swarm display names are forwarded into each agent's build by
|
# Hive/swarm display names, read from the per-agent options meta.rs
|
||||||
# meta.rs as `environment.variables` (per-agent, build-time strings),
|
# renders (NOT from `environment.variables` — those carry the same names
|
||||||
# so they can be baked into the resource attributes below without a
|
# at *runtime* only, so reading them here silently yielded "unknown" on
|
||||||
# runtime shell. Absent (option unset) → "unknown".
|
# every agent while the process env held the right answer). `null` means
|
||||||
hiveDisplayName = config.environment.variables.HYPERHIVE_HIVE_NAME or "unknown";
|
# the hive did not name itself; "unknown" is then an honest label rather
|
||||||
swarmDisplayName = config.environment.variables.HYPERHIVE_SWARM_NAME or "unknown";
|
# 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
|
# Effective per-agent MemoryMax=, in bytes, injected by meta.rs's
|
||||||
# per-agent flake render (`hyperhive.claudeMemoryMaxBytes`). `null`
|
# per-agent flake render (`hyperhive.claudeMemoryMaxBytes`). `null`
|
||||||
# when the effective cap is unbounded ("infinity") or a RAM
|
# 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
|
# the agent's *last rebuild* — `set-limits` still applies the
|
||||||
# cgroup cap live via a drop-in reload, but this derived heap ceiling
|
# cgroup cap live via a drop-in reload, but this derived heap ceiling
|
||||||
# needs a rebuild to pick up a new value.
|
# 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 {
|
options.hyperhive.claudeMemoryMaxBytes = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.ints.positive;
|
type = lib.types.nullOr lib.types.ints.positive;
|
||||||
default = null;
|
default = null;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue