diff --git a/docs/trust-boundary/boundary.md b/docs/trust-boundary/boundary.md index eda24e83..0f401ada 100644 --- a/docs/trust-boundary/boundary.md +++ b/docs/trust-boundary/boundary.md @@ -145,9 +145,7 @@ would require opening a netns anyway. see [`docs/trust-boundary/security.md`](security.md#hive-c0re-privilege-separation) for how `PrivRequest::ReloadGatewayNginx`'s containment works. -⚠️ Contrast `/shared`, which _is_ sticky world-writable (`1777`): it has -many legitimate writers, so sticky is the best available answer there. -This dir has exactly one writer, so it needs no world write at all. +⚠️ This dir has exactly one writer, so it needs no world write at all. ### host admin socket access (`hivectl`) diff --git a/hive-agent/prompts/system.md b/hive-agent/prompts/system.md index 26a8acde..5d63c498 100644 --- a/hive-agent/prompts/system.md +++ b/hive-agent/prompts/system.md @@ -37,7 +37,7 @@ Durable knowledge: Claude session (OAuth credentials) lives at `/root/.claude/` and persists across restarts. -**Shared space**: `/shared` is accessible to all agents (read/write). Only put things here you're willing to lose — other agents may delete them. Use for explicit cross-agent communication or shared artifacts when appropriate. +**Files between agents**: there is no shared directory. Cross-agent files go through the forge (attach them to an issue or PR); small files for the operator go over matrix (`send_file`). **Hive knowledge**: `/knowledge` is a read-only bind-mount (inside your container, at the literal path `/knowledge`) of the `internal/knowledge` repo on the forge — hive-wide reference docs shared across all agents: conventions, runbooks, shared notes. **Start at `/knowledge/README.md`**, its table of contents, then read whatever's relevant for context. The README also covers how to contribute a doc. @@ -45,7 +45,7 @@ Claude session (OAuth credentials) lives at `/root/.claude/` and persists across The `hive-forge` CLI is the supported interface to the Forgejo — issues, PRs, comments, labels, reviews, CI status, attachments, triage (`lint`). **Discover the verb list and each verb's full signature with `hive-forge --help` and `hive-forge --help`** rather than memorising them. The active repo is whichever git checkout you're standing in (inferred from the `origin` remote), or pass `-r ` (global flag, works before or after the verb) to target a different one explicitly — there's no single hardcoded default repo, so don't assume one. A few conventions `--help` won't surface: **never `curl` the forge** — the CLI handles auth and is the only supported path; to check a PR's CI + mergeability use `hive-forge pr status ` (or `--sha ` for a CI-only fast path; exit code is a merge-readiness verdict), not curl. `--body-file -` reads the body from stdin, so a HEREDOC works for multi-line comments/issues: `hive-forge comment --body-file - < [--push]` opens a PR and prints its URL; `--push` `git push`es the head branch first (default remote `forge`). Forge notifications are delivered via the internal message daemon (sender `forge`), not polling. A `forge` notification stays unread **on the forge** until you actually read its thread — viewing the referenced issue/PR with `hive-forge comments ` or `view ` marks that notification read (it's the forge's own read-state, not a local mirror). So when a `forge` message points you at a thread, read the thread to clear the notification instead of letting the same one linger and re-surface. (`hive-forge comment` does the opposite — it _refuses_ to post to a thread with unread activity until you've read it, so read first, then comment.) -Keep messages short — a few sentences each. For anything big (file listings, long diffs, transcripts, analysis), don't hand anyone a filesystem path — neither your state dir nor `/shared`; the operator can't open either and peers can't read your state. If it's about an issue or PR, attach it there (`hive-forge attach-issue` / `attach-comment`) or inline it in the comment. If it's a non-forge matter for the operator, send it over matrix (inline it; `send_file` for attachments). For a peer, inline it in the `send` or put it on the relevant forge thread, then keep the message to a short headline ("cluster audit attached to the PR, headline: 3 nodes over 80% mem"). Name any issue or PR to the operator as a full clickable URL, never a bare number. +Keep messages short — a few sentences each. For anything big (file listings, long diffs, transcripts, analysis), don't hand anyone a filesystem path — not your state dir; the operator can't open it and peers can't read your state. If it's about an issue or PR, attach it there (`hive-forge attach-issue` / `attach-comment`) or inline it in the comment. If it's a non-forge matter for the operator, send it over matrix (inline it; `send_file` for attachments). For a peer, inline it in the `send` or put it on the relevant forge thread, then keep the message to a short headline ("cluster audit attached to the PR, headline: 3 nodes over 80% mem"). Name any issue or PR to the operator as a full clickable URL, never a bare number. When your inbox has a message, handle it and stop. Don't narrate intent — act. diff --git a/hive-agent/src/disk_watch.rs b/hive-agent/src/disk_watch.rs index 9e3a4dc3..ace96551 100644 --- a/hive-agent/src/disk_watch.rs +++ b/hive-agent/src/disk_watch.rs @@ -249,7 +249,7 @@ fn big_dirs() -> Vec { let mut big = Vec::new(); for root in roots() { // Stay on the filesystem we measured: bind mounts (`/nix`, - // `/shared`, `/knowledge`) aren't this agent's to clean up. + // `/knowledge`) aren't this agent's to clean up. let Ok(md) = std::fs::metadata(&root) else { continue; }; diff --git a/hive-c0re/src/lifecycle/host_config.rs b/hive-c0re/src/lifecycle/host_config.rs index d7b81dfa..637d26bb 100644 --- a/hive-c0re/src/lifecycle/host_config.rs +++ b/hive-c0re/src/lifecycle/host_config.rs @@ -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 = 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 { + 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 { let mut binds = Vec::new(); bind_child_agent_dirs("kiddo", &mut binds); diff --git a/hive-c0re/src/lifecycle/mod.rs b/hive-c0re/src/lifecycle/mod.rs index e3d7bdb1..82fee621 100644 --- a/hive-c0re/src/lifecycle/mod.rs +++ b/hive-c0re/src/lifecycle/mod.rs @@ -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`. diff --git a/hive-c0re/src/paths.rs b/hive-c0re/src/paths.rs index 4042db5e..4b039be2 100644 --- a/hive-c0re/src/paths.rs +++ b/hive-c0re/src/paths.rs @@ -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. diff --git a/hive-forge/src/verbs/artifact_get.rs b/hive-forge/src/verbs/artifact_get.rs index ba78cc8d..7a3e145a 100644 --- a/hive-forge/src/verbs/artifact_get.rs +++ b/hive-forge/src/verbs/artifact_get.rs @@ -16,7 +16,7 @@ //! artifact is served as a zip; the default output path reflects that. //! //! Use case: an agent pulling a CI-built artifact (e.g. a paper PDF) into -//! `/shared` for delivery or review without host access. +//! `work/` for delivery or review without host access. use std::io::Write as _; use std::path::PathBuf; diff --git a/hive-priv/src/container_config.rs b/hive-priv/src/container_config.rs index 2cf88691..074bc05a 100644 --- a/hive-priv/src/container_config.rs +++ b/hive-priv/src/container_config.rs @@ -539,7 +539,7 @@ mod tests { fn non_repo_mounts_are_left_alone() { let flags = git_overlay_flags(&[ bind("/agents/atlas/state"), - bind("/shared"), + bind("/knowledge"), bind("/applied"), bind("/agents/atlas/config-notes"), ]);