hive-c0re: fail on an unparseable permission file, write it atomically
tool_groups::read and capabilities::read returned an empty map when
their file existed but didn't parse. Every set_*/remove_agent is a
read-modify-write, and write() rewrote the file in place, so a crash or
ENOSPC mid-write left a truncated file, and the next write (e.g. the
manager-spawn seed of ruth's tool groups) replaced it with a map holding
only one agent. The scheduling and approval gates then denied every
other agent, recoverable only from meta git history.
- Both registries now read through agent_config::read_map: a missing
file is still the empty map, any other read failure or a parse
failure is an io::Error. set_groups / set_caps / remove_agent fail
without writing.
- Writes go through agent_config::write_map: temp file in the same
directory, fsync, rename, fsync the directory. hive-c0re had no
shared atomic-write helper (the existing tmp+rename sites are inline
and don't fsync).
- Callers of read / groups_for / has_cap now handle the error:
* dashboard GET /api/tool-groups, /api/capabilities,
/api/permissions/stale return 500 instead of an empty table;
* the SSE permission snapshots are skipped with a warn;
* render_flake returns Result, so sync_agents fails instead of
rendering every agent without its tool groups / capabilities;
* set_nspawn_flags propagates has_cap's error;
* the socket tool-group gates deny with the read error as message;
* seed_manager_tool_groups logs and does not seed.
- capabilities::write had no callers left once set_caps writes through
write_map, and is removed.
Closes #4719
This commit is contained in:
parent
4f3209d8c7
commit
e0b08fe362
9 changed files with 341 additions and 129 deletions
|
|
@ -102,7 +102,7 @@ pub async fn sync_agents(hive: &HiveEnv, agents: &[AgentSpec]) -> Result<()> {
|
|||
&hive.context_window_tokens,
|
||||
&hive.agent_memory_max,
|
||||
agents,
|
||||
);
|
||||
)?;
|
||||
let flake_path = dir.join("flake.nix");
|
||||
let on_disk = std::fs::read_to_string(&flake_path).unwrap_or_default();
|
||||
let initial = !dir.join(".git").exists();
|
||||
|
|
@ -585,7 +585,7 @@ fn render_flake(
|
|||
context_window_tokens: &std::collections::HashMap<String, u64>,
|
||||
hive_memory_max: &str,
|
||||
agents: &[AgentSpec],
|
||||
) -> String {
|
||||
) -> Result<String> {
|
||||
render_flake_with_lookup(
|
||||
hyperhive_flake,
|
||||
docs_flake,
|
||||
|
|
@ -1000,7 +1000,7 @@ fn render_flake_with_lookup<F>(
|
|||
hive_memory_max: &str,
|
||||
agents: &[AgentSpec],
|
||||
lookup: F,
|
||||
) -> String
|
||||
) -> Result<String>
|
||||
where
|
||||
F: Fn(&str) -> Vec<&'static str>,
|
||||
{
|
||||
|
|
@ -1316,8 +1316,8 @@ where
|
|||
nixosConfigurations = {
|
||||
"#,
|
||||
);
|
||||
let tool_groups_map = crate::tool_groups::read();
|
||||
let capabilities_map = crate::capabilities::read();
|
||||
let tool_groups_map = crate::tool_groups::read()?;
|
||||
let capabilities_map = crate::capabilities::read()?;
|
||||
let resource_limits_map = crate::resource_limits::read();
|
||||
for spec in agents {
|
||||
// Emit `toolGroups = "group1,group2"` when the operator has
|
||||
|
|
@ -1370,7 +1370,7 @@ where
|
|||
);
|
||||
}
|
||||
out.push_str(" };\n };\n}\n");
|
||||
out
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Return the list of file names that are currently staged (index differs
|
||||
|
|
@ -1671,7 +1671,8 @@ mod tests {
|
|||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
)
|
||||
.expect("render flake");
|
||||
// nixpkgs is a top-level input with an explicit URL; hyperhive
|
||||
// follows it.
|
||||
assert!(
|
||||
|
|
@ -1718,7 +1719,8 @@ mod tests {
|
|||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
)
|
||||
.expect("render flake");
|
||||
assert!(
|
||||
!out.contains("hyperhive-docs"),
|
||||
"no docs input/source when docs_flake is empty:\n{out}"
|
||||
|
|
@ -1737,7 +1739,8 @@ mod tests {
|
|||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
)
|
||||
.expect("render flake");
|
||||
assert!(
|
||||
out.contains(r#"networking.hostName = "h-${name}";"#),
|
||||
"an agent container with no hostname of its own is called `nixos`, \
|
||||
|
|
@ -1778,7 +1781,8 @@ mod tests {
|
|||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
)
|
||||
.expect("render flake");
|
||||
assert!(
|
||||
out.contains(
|
||||
"services.hyperhive.agent.claudeCodePath = \"/nix/store/cccc-claude-code-2.1.220\";"
|
||||
|
|
@ -1807,7 +1811,8 @@ mod tests {
|
|||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
)
|
||||
.expect("render flake");
|
||||
assert!(
|
||||
!out.contains("claudeCodePath"),
|
||||
"no claude assignment when unpinned:\n{out}"
|
||||
|
|
@ -1828,7 +1833,8 @@ mod tests {
|
|||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
)
|
||||
.expect("render flake");
|
||||
assert!(
|
||||
out.contains("nixpkgs.follows = \"hyperhive/nixpkgs\""),
|
||||
"expected fallback follows:\n{out}"
|
||||
|
|
@ -1864,7 +1870,8 @@ mod tests {
|
|||
sample_spec("dmatrix", false, 9003),
|
||||
],
|
||||
lookup,
|
||||
);
|
||||
)
|
||||
.expect("render flake");
|
||||
// bitburner declares nixpkgs → follows emitted.
|
||||
assert!(
|
||||
out.contains("agent-bitburner.inputs.nixpkgs.follows = \"nixpkgs\""),
|
||||
|
|
@ -1894,7 +1901,8 @@ mod tests {
|
|||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
|_| Vec::new(),
|
||||
);
|
||||
)
|
||||
.expect("render flake");
|
||||
// No agent-side follows when the lookup reports nothing
|
||||
// declared — protects agents whose flake.lock can't be read
|
||||
// (missing / unparsable) from being broken by a follows on a
|
||||
|
|
@ -1932,7 +1940,8 @@ mod tests {
|
|||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
)
|
||||
.expect("render flake");
|
||||
unsafe {
|
||||
std::env::remove_var("HIVE_FORGE_URL");
|
||||
}
|
||||
|
|
@ -2110,7 +2119,8 @@ mod tests {
|
|||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
)
|
||||
.expect("render flake");
|
||||
unsafe {
|
||||
std::env::remove_var("HIVE_FORGE_URL");
|
||||
std::env::remove_var("HIVE_MATRIX_URL");
|
||||
|
|
@ -2147,7 +2157,8 @@ mod tests {
|
|||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
)
|
||||
.expect("render flake");
|
||||
let want = format!(
|
||||
"agent-alice.url = \"git+file://{}\"",
|
||||
crate::paths::applied_dir("alice").display()
|
||||
|
|
@ -2193,6 +2204,7 @@ mod tests {
|
|||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
.expect("render flake")
|
||||
};
|
||||
|
||||
// A leftover peer-CA file + the env var that used to name it. Both
|
||||
|
|
@ -2286,6 +2298,7 @@ mod tests {
|
|||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
.expect("render flake")
|
||||
};
|
||||
unsafe {
|
||||
std::env::remove_var("HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES");
|
||||
|
|
@ -2361,6 +2374,7 @@ mod tests {
|
|||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
.expect("render flake")
|
||||
};
|
||||
unsafe {
|
||||
std::env::remove_var("HYPERHIVE_GITHUB_DISABLED");
|
||||
|
|
@ -2403,7 +2417,8 @@ mod tests {
|
|||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
)
|
||||
.expect("render flake");
|
||||
let want_bytes = 4u64 * 1024 * 1024 * 1024;
|
||||
assert!(
|
||||
out.contains(&format!("memoryMaxBytes = {want_bytes};")),
|
||||
|
|
|
|||
Loading…
Reference in a new issue