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
|
|
@ -212,11 +212,23 @@ pub async fn ensure_root_agent(coord: &Arc<Coordinator>) -> Result<()> {
|
|||
///
|
||||
/// Skips a name that already has an entry: a destroy+recreate under the
|
||||
/// same name must not silently reset an operator's chosen group set back
|
||||
/// to the default.
|
||||
/// to the default. An unreadable file is logged and left alone: ruth has
|
||||
/// already been spawned, and seeding stays best-effort like the write
|
||||
/// below.
|
||||
fn seed_manager_tool_groups() {
|
||||
if !tool_groups::groups_for(MANAGER_NAME).is_empty() {
|
||||
tracing::debug!("manager tool groups already set — leaving as-is");
|
||||
return;
|
||||
match tool_groups::groups_for(MANAGER_NAME) {
|
||||
Ok(groups) if groups.is_empty() => {}
|
||||
Ok(_) => {
|
||||
tracing::debug!("manager tool groups already set — leaving as-is");
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
error = ?e,
|
||||
"tool-groups file unreadable — not seeding ruth's tool groups"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let all_groups: Vec<String> = hive_sh4re::permissions::ToolGroup::MANAGER_DEFAULT
|
||||
.iter()
|
||||
|
|
@ -263,7 +275,7 @@ fn should_seed_manager_caps(store_written: bool) -> bool {
|
|||
/// entry that has been emptied rather than tombstoning it, so "the manager
|
||||
/// has no entry" cannot tell a fresh hive apart from a deliberate revoke.
|
||||
/// File existence can: every grant and revoke goes through
|
||||
/// `meta::commit_capabilities` → `capabilities::set_caps` → `write`, which
|
||||
/// `meta::commit_capabilities` → `capabilities::set_caps`, which
|
||||
/// writes the file even when the result is an empty `{}`. So while the file
|
||||
/// is absent nobody has ever had a say, and once it exists this is inert
|
||||
/// forever — including on the destroy+recreate path, matching
|
||||
|
|
|
|||
Loading…
Reference in a new issue