agents: read the hive's name and domain at runtime, not from the build
Part of #5081 (slice S0-1a). An agent's closure no longer depends on the name or domain of the hive that renders it. - hive-priv writes /etc/hyperhive-hive-facts (HYPERHIVE_HIVE_NAME, HYPERHIVE_HIVE_DOMAIN) into the container from the same WriteNspawnFlags call that writes /etc/hyperhive-bridge-dns. hive-c0re passes the values from its own unit env. hive-priv refuses a value outside [A-Za-z0-9._-]. - A systemd environment generator in the agent (hive-facts.nix) loads the file into the container manager's environment, so every unit sees it, as with the systemd.globalEnvironment entries the render used to emit. It also builds CLAUDE_REMOTE_CONTROL_SESSION_NAME_PREFIX (<hive>-<agent>), which left claude's managed settings. - meta.rs stops forwarding HYPERHIVE_HIVE_{NAME,DOMAIN} and stops setting services.hyperhive.agent.hiveName. The option is removed (both spellings now fail with a removal message). - otel.nix drops the agent's hive= resource label. The swarm collector upserts hive per authenticated receiver (swarm-otel.nix resource/<h>, on both the metrics and logs pipelines), so stored series keep it. The otel first hop, otel enable and the dashboard port are still baked; they are S0-1b.
This commit is contained in:
parent
80e8361da9
commit
c87a4d2c1a
14 changed files with 303 additions and 127 deletions
|
|
@ -508,8 +508,6 @@ const FORWARDED_VARS: &[&str] = &[
|
|||
"HIVE_FORGE_URL",
|
||||
"HIVE_FORGE_PUBLIC_URL",
|
||||
"HIVE_MATRIX_URL",
|
||||
"HYPERHIVE_HIVE_DOMAIN",
|
||||
"HYPERHIVE_HIVE_NAME",
|
||||
"HYPERHIVE_SWARM_NAME",
|
||||
];
|
||||
|
||||
|
|
@ -520,14 +518,11 @@ const FORWARDED_VARS: &[&str] = &[
|
|||
/// 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 value.
|
||||
///
|
||||
/// The hive/swarm display names are here for exactly that reason, learned the
|
||||
/// hard way: they were forwarded as runtime env only, while the agent's
|
||||
/// claude-settings module 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.
|
||||
/// The swarm display name is here for that reason: `otel.nix` reads the option
|
||||
/// at eval time for the `swarm=` resource label, and with the env var alone
|
||||
/// that label evaluates to `unknown` while the process env holds the right
|
||||
/// name. Half-wiring this map is a value that evaluates fine and is silently
|
||||
/// wrong.
|
||||
const FORWARDED_VAR_OPTIONS: &[(&str, &str)] = &[
|
||||
// Read at build time to decide whether the harness unit imports the hive's
|
||||
// queue credential at all — see `nix/agent-modules/queue.nix`. Both halves
|
||||
|
|
@ -558,7 +553,6 @@ const FORWARDED_VAR_OPTIONS: &[(&str, &str)] = &[
|
|||
),
|
||||
("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"),
|
||||
];
|
||||
|
||||
|
|
@ -1164,9 +1158,7 @@ where
|
|||
// → in-container accessors fall back to None / defaults gracefully.
|
||||
//
|
||||
// - HIVE_FORGE_URL: agents poll this for Forgejo notifications.
|
||||
// - HYPERHIVE_HIVE_DOMAIN: machine-readable hive DNS.
|
||||
// - HYPERHIVE_HIVE_NAME / HYPERHIVE_SWARM_NAME: human display
|
||||
// names for hive + swarm.
|
||||
// - HYPERHIVE_SWARM_NAME: human display name for the swarm.
|
||||
for (var, val) in forwarded_env_vars() {
|
||||
let escaped = val.replace('\\', "\\\\").replace('"', "\\\"");
|
||||
let _ = writeln!(out, " {var} = \"{escaped}\";");
|
||||
|
|
@ -1770,6 +1762,53 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
/// The hive's name and domain reach an agent at runtime
|
||||
/// (`/etc/hyperhive-hive-facts`), so the rendered flake does not change with
|
||||
/// either.
|
||||
#[test]
|
||||
fn render_flake_is_identical_across_hive_names_and_domains() {
|
||||
const VARS: [&str; 2] = ["HYPERHIVE_HIVE_NAME", "HYPERHIVE_HIVE_DOMAIN"];
|
||||
let _env = crate::test_env::lock();
|
||||
let saved = VARS.map(std::env::var_os);
|
||||
let render_on = |name: &str, domain: &str| {
|
||||
// SAFETY: serialised by the guard above; restored before returning.
|
||||
unsafe {
|
||||
std::env::set_var(VARS[0], name);
|
||||
std::env::set_var(VARS[1], domain);
|
||||
}
|
||||
render_flake(
|
||||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
.expect("render flake")
|
||||
};
|
||||
let alpha = render_on("alpha-hive", "alpha-hive.example.test");
|
||||
let beta = render_on("beta-hive", "beta-hive.example.test");
|
||||
for (var, value) in VARS.iter().zip(saved) {
|
||||
// SAFETY: as above.
|
||||
unsafe {
|
||||
match value {
|
||||
Some(v) => std::env::set_var(var, v),
|
||||
None => std::env::remove_var(var),
|
||||
}
|
||||
}
|
||||
}
|
||||
for hive in ["alpha-hive", "beta-hive"] {
|
||||
assert!(
|
||||
!alpha.contains(hive) && !beta.contains(hive),
|
||||
"the rendered flake must not name the hive ({hive}):\n{alpha}"
|
||||
);
|
||||
}
|
||||
assert_eq!(alpha, beta, "the render must not depend on the hive");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_url_options_render_from_forwarded_pairs() {
|
||||
// Pure over the pairs, deliberately: the render-level tests have to
|
||||
|
|
@ -1782,21 +1821,21 @@ mod tests {
|
|||
&[
|
||||
("HIVE_FORGE_URL", "http://forge.example.test".to_string()),
|
||||
("HIVE_MATRIX_URL", "http://matrix.example.test".to_string()),
|
||||
// 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()),
|
||||
// Mapped too: `otel.nix` reads the option at eval time.
|
||||
("HYPERHIVE_SWARM_NAME", "constellat1on".to_string()),
|
||||
// Forwarded but genuinely unmapped — proves the map is a
|
||||
// filter, not a pass-through.
|
||||
("HYPERHIVE_HIVE_DOMAIN", "pr1ma.darkest.space".to_string()),
|
||||
(
|
||||
"HIVE_FORGE_PUBLIC_URL",
|
||||
"https://forge.example.test".to_string(),
|
||||
),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
out,
|
||||
" 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",
|
||||
\x20 services.hyperhive.agent.swarmName = \"constellat1on\";\n",
|
||||
"expected exactly the mapped options, in input order, indented for the module block"
|
||||
);
|
||||
}
|
||||
|
|
@ -1836,7 +1875,7 @@ mod tests {
|
|||
fn require_service_urls_accepts_a_rendered_forge_url() {
|
||||
require_service_urls(&[
|
||||
("HIVE_FORGE_URL", "http://forge.example.test".to_string()),
|
||||
("HYPERHIVE_HIVE_NAME", "pr1ma".to_string()),
|
||||
("HYPERHIVE_SWARM_NAME", "constellat1on".to_string()),
|
||||
])
|
||||
.expect("a forwarded forge URL satisfies the requirement");
|
||||
}
|
||||
|
|
@ -1847,7 +1886,7 @@ mod tests {
|
|||
// every agent would simply come up with no forge login and no way to
|
||||
// tell that was unintended. The hive asserts its own requirement
|
||||
// because it is the only layer that knows a forge is mandatory.
|
||||
let err = require_service_urls(&[("HYPERHIVE_HIVE_NAME", "pr1ma".to_string())])
|
||||
let err = require_service_urls(&[("HYPERHIVE_SWARM_NAME", "constellat1on".to_string())])
|
||||
.expect_err("a missing forge URL must stop the write");
|
||||
assert!(
|
||||
err.to_string().contains("HIVE_FORGE_URL is unset"),
|
||||
|
|
@ -1862,13 +1901,16 @@ 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 —
|
||||
// `HIVE_FORGE_PUBLIC_URL` 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_forwarded_var_options(
|
||||
&mut out,
|
||||
&[("HYPERHIVE_HIVE_DOMAIN", "pr1ma.darkest.space".to_string())],
|
||||
&[(
|
||||
"HIVE_FORGE_PUBLIC_URL",
|
||||
"https://forge.example.test".to_string(),
|
||||
)],
|
||||
);
|
||||
assert!(
|
||||
out.is_empty(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue