Watch
0
0
Fork
You've already forked hyperhive
0

remove the /shared bind from agent containers

This commit is contained in:
atlas 2026-10-26 21:33:04 +01:00
commit 7dd5d30da8
3 changed files with 52 additions and 71 deletions

View file

@ -12,8 +12,7 @@ use hive_types::Permission;
use crate::coordinator::{AgentPaths, HiveEnv};
use super::{
AGENT_PREFIX, CONTAINER_RUNTIME_MOUNT, CONTAINER_SHARED_MOUNT, container_claude_mount,
container_name, validate,
AGENT_PREFIX, CONTAINER_RUNTIME_MOUNT, container_claude_mount, container_name, validate,
};
/// Re-apply the per-container host-side config: nspawn flags (bind
@ -420,26 +419,6 @@ async fn set_nspawn_flags(
claude_dir: &Path,
notes_dir: &Path,
) -> Result<()> {
// Ensure /shared directory exists before binding. systemd-nspawn requires the bind source to exist.
let shared_root = crate::paths::shared_root();
std::fs::create_dir_all(&shared_root)
.with_context(|| format!("create {}", shared_root.display()))?;
// Make /shared writable by every agent. Containers share host uids (no
// PrivateUsers), but each agent is a distinct unix user, so a root-owned
// 0755 dir leaves them unable to write — the documented "read/write for
// all agents" contract was broken. A setgid group would need a
// pinned GID declared in every container plus all agent users joined to
// it (cross-container coordination + a rebuild cascade); instead we use
// the /tmp model — sticky world-writable (1777). The sticky bit lets any
// agent create files while protecting each agent's entries from deletion
// by the others, and matches /shared's documented "free-for-all, may be
// deleted/lost" semantics without touching any per-agent config.
{
use std::os::unix::fs::PermissionsExt as _;
let perms = std::fs::Permissions::from_mode(0o1777);
std::fs::set_permissions(&shared_root, perms)
.with_context(|| format!("chmod 1777 {}", shared_root.display()))?;
}
// Ensure /knowledge dir exists. It may be empty until forge seeds it;
// nspawn refuses to start if the bind source is missing entirely.
std::fs::create_dir_all(crate::knowledge::LOCAL_DIR)
@ -454,7 +433,6 @@ async fn set_nspawn_flags(
// harness service's environment sets `HOME` to the same path
// (`agent.nix` template), so no `--setenv` plumbing
// is needed here — the bind alone is enough.
let claude_mount = container_claude_mount(agent_name);
// The agent's own identity *at* the swarm secret store, collected under
// this hive's certificate. Staged here rather than at spawn because this
@ -462,28 +440,7 @@ async fn set_nspawn_flags(
// `super::agent_identity` for the whole hop.
let load_creds = super::agent_identity::stage(agent_name).await;
let mut binds: Vec<BindMount> = vec![
BindMount {
host_path: runtime_dir.to_string_lossy().into_owned(),
container_path: CONTAINER_RUNTIME_MOUNT.to_owned(),
read_only: false,
},
BindMount {
host_path: claude_dir.to_string_lossy().into_owned(),
container_path: claude_mount,
read_only: false,
},
BindMount {
host_path: shared_root.to_string_lossy().into_owned(),
container_path: CONTAINER_SHARED_MOUNT.to_owned(),
read_only: false,
},
BindMount {
host_path: crate::knowledge::LOCAL_DIR.to_owned(),
container_path: crate::knowledge::CONTAINER_MOUNT.to_owned(),
read_only: true,
},
];
let mut binds = base_binds(runtime_dir, claude_dir, container_claude_mount(agent_name));
// Own state, harness, work, and config dirs — same for every agent
// including the manager. Config is RO: an agent must not edit its own
@ -587,14 +544,55 @@ async fn set_nspawn_flags(
.await
}
/// The binds every agent container gets, before its own state, config and
/// role-specific dirs.
fn base_binds(runtime_dir: &Path, claude_dir: &Path, claude_mount: String) -> Vec<BindMount> {
vec![
BindMount {
host_path: runtime_dir.to_string_lossy().into_owned(),
container_path: CONTAINER_RUNTIME_MOUNT.to_owned(),
read_only: false,
},
BindMount {
host_path: claude_dir.to_string_lossy().into_owned(),
container_path: claude_mount,
read_only: false,
},
BindMount {
host_path: crate::knowledge::LOCAL_DIR.to_owned(),
container_path: crate::knowledge::CONTAINER_MOUNT.to_owned(),
read_only: true,
},
]
}
#[cfg(test)]
mod tests {
use super::{
BindMount, HostMemory, MEMORY_CAP_FILE, bind_child_agent_dirs, bind_own_agent_dirs,
holds_manage_root_agent, limits_to_apply, memory_cap_value, stage_limits,
BindMount, HostMemory, MEMORY_CAP_FILE, base_binds, bind_child_agent_dirs,
bind_own_agent_dirs, holds_manage_root_agent, limits_to_apply, memory_cap_value,
stage_limits,
};
use crate::coordinator::HiveEnv;
/// No agent container gets a cross-agent scratch dir; `/knowledge` is the
/// control that the lookup finds a bind that is there.
#[test]
fn base_binds_have_no_shared_dir() {
let binds = base_binds(
std::path::Path::new("/run/x"),
std::path::Path::new("/var/lib/x/claude"),
"/home/a/.claude".to_owned(),
);
let targets: Vec<&str> = binds.iter().map(|b| b.container_path.as_str()).collect();
assert!(targets.contains(&"/knowledge"), "control: {targets:?}");
assert!(!targets.contains(&"/shared"), "{targets:?}");
assert!(
binds.iter().all(|b| !b.host_path.ends_with("/shared")),
"{binds:?}"
);
}
fn child_binds() -> Vec<BindMount> {
let mut binds = Vec::new();
bind_child_agent_dirs("kiddo", &mut binds);

View file

@ -51,11 +51,6 @@ pub fn container_claude_mount(name: &str) -> String {
format!("/home/{name}/.claude")
}
/// Mount point of the shared directory accessible to all agents.
/// All agents can read/write here; agents should only put things they're
/// willing to lose (other agents may delete them).
pub const CONTAINER_SHARED_MOUNT: &str = "/shared";
pub use hive_meta_flake::agent_web_port;
/// Extract the bridge gateway IP from `HIVE_NETWORK_SUBNET`.

View file

@ -6,7 +6,7 @@
//! …). This module is the **single Rust-side source** for every host
//! path — the strictly host-side ones grouped into subdirs (`db/`,
//! `matrix/`, `run/`), plus the **nix-coupled** roots
//! (`agents/`, `applied/`, `meta/`, `shared/`, `gateway/`, `knowledge/`,
//! (`agents/`, `applied/`, `meta/`, `gateway/`, `knowledge/`,
//! the `/run` runtime dirs).
//!
//! Nix-coupled paths are NOT excluded (the old stance) — they're moved
@ -192,16 +192,6 @@ pub fn meta_rev_dir() -> PathBuf {
state_root().join("meta-rev")
}
/// `shared/` — the cross-agent `/shared` scratch space. A `&str`, so it
/// stays a const; [`shared_root`] wraps it.
// nix: bind-mounted into every agent container as `/shared` (the harness nix modules) — must match.
pub const SHARED_ROOT: &str = "/var/lib/hyperhive/shared";
#[must_use]
pub fn shared_root() -> PathBuf {
PathBuf::from(SHARED_ROOT)
}
/// `knowledge/` — local checkout of the `internal/knowledge` repo. A
/// `&str` (used as a git `-C` arg / clone target throughout the knowledge
/// worker), so it stays a const rather than a `PathBuf` fn.