Watch
0
0
Fork
You've already forked hyperhive
0

hive-priv: never leave an agent dir or the agents root root-owned on a fresh host

This commit is contained in:
damocles 2026-11-09 13:53:08 +01:00
commit 24c5c02942

View file

@ -244,17 +244,31 @@ async fn ensure_agent_subvolume(
let root = env.state_root;
let agent_root = root.join(agent_name);
// Progressive: existing agents (plain dir OR already a subvol) are left
// untouched — never auto-migrated.
// Progressive: existing agents (plain dir OR already a subvol) are never
// auto-migrated. The one exception is an agent dir this op itself left
// root-owned on a fresh host: hive-c0re cannot create below it.
if agent_root.exists() {
if let (Ok(agent), Ok(root_meta)) = (
std::fs::symlink_metadata(&agent_root),
std::fs::metadata(root),
) && is_stranded_root_dir(agent.uid(), root_meta.uid())
{
std::os::unix::fs::lchown(&agent_root, Some(root_meta.uid()), Some(root_meta.gid()))
.with_context(|| {
format!(
"chown stranded {} to agents-root owner",
agent_root.display()
)
})?;
tracing::warn!(agent = %agent_name, path = %agent_root.display(), "handed a root-owned agent dir back to the agents-root owner");
}
return Ok((String::new(), String::new()));
}
// Only btrfs supports subvolumes; on anything else hive-c0re's normal
// `create_dir_all` makes a plain dir (the pre-subvolume behaviour). The
// parent must exist for both statfs and `btrfs subvolume create`.
std::fs::create_dir_all(root)
.with_context(|| format!("create agents root {}", root.display()))?;
ensure_agents_root(root)?;
if !env.probes.on_btrfs(root)? {
return Ok((String::new(), String::new()));
}
@ -306,6 +320,30 @@ async fn ensure_agent_subvolume(
Ok((String::new(), String::new()))
}
/// Make the agents root if it is missing. Made by root here, so it is handed
/// to the owner of its parent (hive-core's state dir); the subvolume takes the
/// root's owner, and a root-owned root would strand every agent dir under it.
/// One made meanwhile by the tmpfiles rule is left as that rule made it.
fn ensure_agents_root(root: &Path) -> Result<()> {
use std::os::unix::fs::MetadataExt as _;
match std::fs::create_dir(root) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => return Ok(()),
Err(e) => return Err(e).with_context(|| format!("create agents root {}", root.display())),
}
let parent = std::fs::metadata(root.parent().unwrap_or(root))
.with_context(|| format!("stat parent of {}", root.display()))?;
std::os::unix::fs::chown(root, Some(parent.uid()), Some(parent.gid()))
.with_context(|| format!("chown agents root {} to its parent's owner", root.display()))
}
/// An agent dir owned by root under an agents root that is not: hive-c0re
/// (hive-core) cannot create `claude/` or `state/` in it.
fn is_stranded_root_dir(agent_uid: u32, root_uid: u32) -> bool {
agent_uid == 0 && root_uid != 0
}
/// `DeleteAgentSubvolume` — delete the agent's state root iff it's a btrfs
/// subvolume (purge path only). No-op for plain dirs / missing paths;
/// hive-c0re's own `remove_dir_all` covers those. See the wire doc.
@ -1506,6 +1544,37 @@ esac"#;
assert!(!fx.agent("iris").exists());
}
#[test]
fn the_agents_root_is_made_once_and_a_racing_creator_is_not_an_error() {
use super::ensure_agents_root;
use std::os::unix::fs::MetadataExt as _;
let fx = Fx::new();
let root = fx.state.join("made");
ensure_agents_root(&root).unwrap();
let (dir, parent) = (
std::fs::metadata(&root).unwrap(),
std::fs::metadata(&fx.state).unwrap(),
);
assert_eq!((dir.uid(), dir.gid()), (parent.uid(), parent.gid()));
ensure_agents_root(&root).unwrap();
assert!(ensure_agents_root(&root.join("no/parent")).is_err());
}
#[test]
fn only_a_root_owned_dir_under_a_non_root_root_is_stranded() {
use super::is_stranded_root_dir;
assert!(is_stranded_root_dir(0, 998));
assert!(
!is_stranded_root_dir(0, 0),
"a root-run host is not stranded"
);
assert!(!is_stranded_root_dir(998, 998));
assert!(
!is_stranded_root_dir(1000, 998),
"never touch an agent-owned dir"
);
}
#[tokio::test]
async fn ensure_creates_the_subvolume_owned_like_the_root() {
use std::os::unix::fs::MetadataExt as _;