hand agents the dashboard port, pinned claude and otel labels at container start
The three values that differ per host (deploy.hive-controller.dashboardPort, claudeCodePackage and otel.extraResourceAttributes) no longer go into the meta flake. hive-priv writes them into the container's /etc/hyperhive-hive-facts with the hive name and domain, and the agent reads them at boot: - HIVE_DASHBOARD_PORT reaches every unit through the hive-facts generator; the web UI still defaults to 7000 without it. - `claude` on PATH is a wrapper that runs HYPERHIVE_CLAUDE_CODE_PATH's claude when it is there, and the container's own claude-code otherwise. The host's serve.json keeps gc-rooting the pinned build. - OTEL_RESOURCE_ATTRIBUTES moves out of the units' Environment= into a boot-time generator, which appends HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES. The agent options services.hyperhive.agent.claudeCodePath (now a removed option) and otel.extraResourceAttributes go; only the meta render set them. render_flake takes no HiveEnv any more, so the memory-cap render test and render_hive_flake_at, which only existed to vary HiveEnv into the render, go with it. Refs #4857
This commit is contained in:
parent
a32c144b80
commit
9113651c10
23 changed files with 408 additions and 335 deletions
|
|
@ -367,24 +367,54 @@ fn container_rootfs(container: &str) -> PathBuf {
|
|||
/// Render [`HiveFacts`] as the systemd environment file the agent loads,
|
||||
/// one `KEY=value` line per present fact.
|
||||
///
|
||||
/// A value is refused unless it is DNS-name characters only, so it can carry
|
||||
/// neither a newline (a second assignment) nor anything an environment file
|
||||
/// or a shell would interpret.
|
||||
/// Each value is refused, never escaped, unless it is made only of the
|
||||
/// characters its fact can hold. None of those sets admits a newline (a
|
||||
/// second assignment), whitespace, a quote, `\` or `$`: the agent hands each
|
||||
/// line to systemd and its shells, which would read those as syntax.
|
||||
fn render_hive_facts(facts: &HiveFacts) -> Result<String> {
|
||||
use std::fmt::Write as _;
|
||||
let dns = |b: u8| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'-' | b'_');
|
||||
let port = facts.dashboard_port.map(|p| p.to_string());
|
||||
let mut out = String::new();
|
||||
for (key, value) in [
|
||||
("HYPERHIVE_HIVE_NAME", &facts.name),
|
||||
("HYPERHIVE_HIVE_DOMAIN", &facts.domain),
|
||||
for (key, value, max_len, allowed) in [
|
||||
(
|
||||
"HYPERHIVE_HIVE_NAME",
|
||||
facts.name.as_ref(),
|
||||
253,
|
||||
&dns as &dyn Fn(u8) -> bool,
|
||||
),
|
||||
("HYPERHIVE_HIVE_DOMAIN", facts.domain.as_ref(), 253, &dns),
|
||||
("HIVE_DASHBOARD_PORT", port.as_ref(), 5, &|b: u8| {
|
||||
b.is_ascii_digit()
|
||||
}),
|
||||
// A store path's name characters, plus the `/` of its prefix; the
|
||||
// prefix itself is checked below.
|
||||
(
|
||||
"HYPERHIVE_CLAUDE_CODE_PATH",
|
||||
facts.claude_code_path.as_ref(),
|
||||
256,
|
||||
&|b: u8| dns(b) || matches!(b, b'+' | b'?' | b'=' | b'/'),
|
||||
),
|
||||
// `k=v,k=v`, values percent-encoded as the OTEL spec has them.
|
||||
(
|
||||
"HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES",
|
||||
facts.otel_extra_resource_attributes.as_ref(),
|
||||
1024,
|
||||
&|b: u8| dns(b) || matches!(b, b'=' | b',' | b'/' | b':' | b'%' | b'+' | b'@'),
|
||||
),
|
||||
] {
|
||||
let Some(value) = value else { continue };
|
||||
if value.is_empty()
|
||||
|| value.len() > 253
|
||||
|| !value
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'-' | b'_'))
|
||||
if value.is_empty() || value.len() > max_len || !value.bytes().all(allowed) {
|
||||
bail!(
|
||||
"invalid {key} value {value:?}: must be 1-{max_len} chars of its fact's character set"
|
||||
);
|
||||
}
|
||||
if key == "HYPERHIVE_CLAUDE_CODE_PATH"
|
||||
&& !value
|
||||
.strip_prefix("/nix/store/")
|
||||
.is_some_and(|name| !name.is_empty() && !name.contains('/'))
|
||||
{
|
||||
bail!("invalid {key} value {value:?}: must be 1-253 chars of [A-Za-z0-9._-]");
|
||||
bail!("invalid {key} value {value:?}: must be a top-level /nix/store path");
|
||||
}
|
||||
let _ = writeln!(out, "{key}={value}");
|
||||
}
|
||||
|
|
@ -711,14 +741,15 @@ mod tests {
|
|||
let both = HiveFacts {
|
||||
name: Some("pr1ma".to_owned()),
|
||||
domain: Some("pr1ma.darkest.space".to_owned()),
|
||||
..HiveFacts::default()
|
||||
};
|
||||
assert_eq!(
|
||||
render_hive_facts(&both).unwrap(),
|
||||
"HYPERHIVE_HIVE_NAME=pr1ma\nHYPERHIVE_HIVE_DOMAIN=pr1ma.darkest.space\n"
|
||||
);
|
||||
let domain_only = HiveFacts {
|
||||
name: None,
|
||||
domain: Some("h1.example.test".to_owned()),
|
||||
..HiveFacts::default()
|
||||
};
|
||||
assert_eq!(
|
||||
render_hive_facts(&domain_only).unwrap(),
|
||||
|
|
@ -734,7 +765,66 @@ mod tests {
|
|||
for bad in ["", "pr1ma\nLD_PRELOAD=/x", "a b", "a\"b", "$(x)"] {
|
||||
let facts = HiveFacts {
|
||||
name: Some(bad.to_owned()),
|
||||
domain: None,
|
||||
..HiveFacts::default()
|
||||
};
|
||||
assert!(
|
||||
render_hive_facts(&facts).is_err(),
|
||||
"{bad:?} must be refused"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The three per-host values agents used to get at build time arrive
|
||||
/// here, one line each.
|
||||
#[test]
|
||||
fn hive_facts_render_the_per_host_values() {
|
||||
let facts = HiveFacts {
|
||||
dashboard_port: Some(7123),
|
||||
claude_code_path: Some("/nix/store/0abc-claude-code-2.1.220".to_owned()),
|
||||
otel_extra_resource_attributes: Some(
|
||||
"deployment.environment=prod,team=a%2Fb".to_owned(),
|
||||
),
|
||||
..HiveFacts::default()
|
||||
};
|
||||
assert_eq!(
|
||||
render_hive_facts(&facts).unwrap(),
|
||||
"HIVE_DASHBOARD_PORT=7123\n\
|
||||
HYPERHIVE_CLAUDE_CODE_PATH=/nix/store/0abc-claude-code-2.1.220\n\
|
||||
HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES=deployment.environment=prod,team=a%2Fb\n"
|
||||
);
|
||||
}
|
||||
|
||||
/// The claude path is a top-level store path or nothing, and neither
|
||||
/// value may carry generator syntax.
|
||||
#[test]
|
||||
fn hive_facts_refuse_bad_claude_paths_and_resource_attributes() {
|
||||
for bad in [
|
||||
"/nix/store/",
|
||||
"/nix/store/abc-claude/bin/claude",
|
||||
"/tmp/claude",
|
||||
"nix/store/abc-claude",
|
||||
"/nix/store/abc-claude\nLD_PRELOAD=/x",
|
||||
] {
|
||||
let facts = HiveFacts {
|
||||
claude_code_path: Some(bad.to_owned()),
|
||||
..HiveFacts::default()
|
||||
};
|
||||
assert!(
|
||||
render_hive_facts(&facts).is_err(),
|
||||
"{bad:?} must be refused"
|
||||
);
|
||||
}
|
||||
for bad in [
|
||||
"a=b\nPATH=/x",
|
||||
"a=${HOME}",
|
||||
"a=b c",
|
||||
"a=\"b\"",
|
||||
"a=b\\c",
|
||||
"",
|
||||
] {
|
||||
let facts = HiveFacts {
|
||||
otel_extra_resource_attributes: Some(bad.to_owned()),
|
||||
..HiveFacts::default()
|
||||
};
|
||||
assert!(
|
||||
render_hive_facts(&facts).is_err(),
|
||||
|
|
@ -752,10 +842,7 @@ mod tests {
|
|||
bridge: "hive0".to_owned(),
|
||||
gateway_ip: "10.42.0.1".to_owned(),
|
||||
};
|
||||
let facts = HiveFacts {
|
||||
name: None,
|
||||
domain: None,
|
||||
};
|
||||
let facts = HiveFacts::default();
|
||||
for name in ["hive-forge", "hive-matrix"] {
|
||||
let errs = [
|
||||
handle_write_nspawn_flags(name, &[], &isolation, &[], &facts).unwrap_err(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue