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
|
|
@ -643,7 +643,15 @@ impl Coordinator {
|
|||
.iter()
|
||||
.map(|c| (<&str>::from(*c), c.description()))
|
||||
.collect();
|
||||
let assignments = crate::capabilities::read();
|
||||
// An empty snapshot would show every agent as having lost its
|
||||
// capabilities; emit nothing and let the HTTP refetch report it.
|
||||
let assignments = match crate::capabilities::read() {
|
||||
Ok(map) => map,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = ?e, "capabilities snapshot skipped");
|
||||
return;
|
||||
}
|
||||
};
|
||||
// Best-effort roster (sync path); on a contended cache miss we
|
||||
// emit explicit keys only — the HTTP refetch fills the rest in.
|
||||
let roster = self.live_container_names_blocking().unwrap_or_default();
|
||||
|
|
@ -670,7 +678,13 @@ impl Coordinator {
|
|||
.iter()
|
||||
.map(|g| (<&str>::from(*g), g.description()))
|
||||
.collect();
|
||||
let assignments = crate::tool_groups::read();
|
||||
let assignments = match crate::tool_groups::read() {
|
||||
Ok(map) => map,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = ?e, "tool-groups snapshot skipped");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let roster = self.live_container_names_blocking().unwrap_or_default();
|
||||
let (agents, effective) = crate::dashboard::permissions::roster_and_effective(
|
||||
roster,
|
||||
|
|
|
|||
Loading…
Reference in a new issue