Watch
0
0
Fork
You've already forked hyperhive
0

agent permissions: drop hive-c0re's swarm-agent-permissions.json fallback, fail closed before the first read

Removes the HiveCopy hand-off: hive-c0re no longer writes each agent's
entry from swarm-agent-permissions.json into its runtime dir as
swarm-permissions.json, and the harness and subagent daemon no longer
read it. hive-c0re keeps the file as its own last read of the swarm for
the manage_root_agent mounts. Also removes permissions_import::pending(),
its only caller being the hand-off. No nix provisioned the copy, so no
nix option is removed and no tombstone is needed.

Rule after this change: until an agent's first successful read of its
key it holds no permissions (no execution, so no bash), and a subagent it
starts holds the same. After a read a set key wins, [] holds none and an
unset key is the default. A failed read keeps the last value, so an
unreachable queue at start keeps the agent closed until a read succeeds.
The harness logs once at warn that it starts closed.

Fixes #5315
This commit is contained in:
atlas 2026-10-21 01:56:37 +02:00 • committed by mara
commit 2f00f9041f
11 changed files with 121 additions and 283 deletions

View file

@ -11,14 +11,12 @@
//! hive-c0re enforces one member, [`Permission::ManageRootAgent`]: read by
//! `lifecycle::host_config::set_nspawn_flags` on every container start, which
//! is why a change to it takes a restart. The agent reads the tool members
//! itself, and holds its entry here until its own first read succeeds:
//! [`hand_at`] copies the entry into the agent's runtime dir.
//! itself.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use hive_types::Permission;
use swarm_queue_client::agent_permissions::{HIVE_COPY_FILE, HiveCopy};
use swarm_wire::agent_permissions::AgentPermissions;
const FILE: &str = "swarm-agent-permissions.json";
@ -58,30 +56,6 @@ pub fn holds(path: &Path, agent: &str, perm: Permission) -> std::io::Result<bool
Ok(effective(read_at(path)?.get(agent)).contains(&perm))
}
/// Write `agent`'s [`HiveCopy`] into `runtime_dir`: its entry in the file at
/// `stored` (normally [`path`]) and `import_pending`. Left alone when the
/// copy there already says the same.
///
/// # Errors
///
/// As [`read_at`], or the write failed.
pub fn hand_at(
stored: &Path,
import_pending: bool,
agent: &str,
runtime_dir: &Path,
) -> std::io::Result<()> {
let copy = HiveCopy {
stored: read_at(stored)?.remove(agent).unwrap_or_default(),
import_pending,
};
let dest = runtime_dir.join(HIVE_COPY_FILE);
if dest.exists() && HiveCopy::read_at(&dest) == copy {
return Ok(());
}
super::write_map(&dest, &copy)
}
#[cfg(test)]
mod tests {
use super::*;
@ -129,32 +103,6 @@ mod tests {
assert!(!holds(&path, "atlas", Permission::ManageRootAgent).expect("read"));
}
/// With the queue down nothing is read, so the agent starts on the copy
/// written here: the stored grant, not the default.
#[test]
fn the_agent_is_handed_its_stored_grant_and_whether_the_import_is_pending() {
let dir = tempfile::tempdir().expect("tempdir");
let stored = dir.path().join(FILE);
let runtime = dir.path().join("run");
write_at(
&stored,
&Stored::from([(
"atlas".to_owned(),
record(r#"{"permissions":["web_tools"]}"#),
)]),
)
.expect("seed");
hand_at(&stored, true, "atlas", &runtime).expect("hand");
let copy = HiveCopy::read_at(&runtime.join(HIVE_COPY_FILE));
assert!(copy.import_pending);
assert_eq!(copy.before_read(), vec![Permission::WebTools]);
hand_at(&stored, false, "iris", &runtime).expect("hand");
let copy = HiveCopy::read_at(&runtime.join(HIVE_COPY_FILE));
assert_eq!(copy, HiveCopy::default(), "no entry is the default");
}
#[test]
fn a_corrupt_file_is_an_error() {
let dir = tempfile::tempdir().expect("tempdir");