drop the config and /applied mounts: an agent reads its config by cloning agent-configs, and the prompt says so
This commit is contained in:
parent
a781addcfe
commit
3c09e14a76
1 changed files with 103 additions and 288 deletions
|
|
@ -288,75 +288,34 @@ async fn systemd_daemon_reload() -> Result<()> {
|
|||
crate::priv_client::daemon_reload().await
|
||||
}
|
||||
|
||||
/// Where the manager sees the applied trees of every agent, read-only.
|
||||
/// Manager runs `git fetch /applied/<n>/.git refs/tags/*:refs/tags/applied/*`
|
||||
/// to learn what hive-c0re deployed (or rejected, or failed to
|
||||
/// build); the RO bind makes accidental writes impossible from
|
||||
/// inside the container.
|
||||
pub const CONTAINER_MANAGER_APPLIED_MOUNT: &str = "/applied";
|
||||
|
||||
/// Host path behind every `/agents/<name>/config` mount: the **applied**
|
||||
/// (deployed) repo, not the working clone at `agents/<name>/config`. That
|
||||
/// clone is where a config change is staged, so it can hold a proposal
|
||||
/// that is still under review or was rejected outright — mounting it shows
|
||||
/// an agent a config which does not govern it. Both mounts (an agent's own
|
||||
/// and a `ManageRootAgent` holder's view of another agent's) go through
|
||||
/// here so they cannot drift.
|
||||
/// Append the bind flag for `child`'s `state` dir into `binds`. See
|
||||
/// docs/agent-lifecycle/persistence.md ("Cross-agent access to state") for what
|
||||
/// the holder may touch and why. Creates the missing host-side directory so
|
||||
/// nspawn doesn't refuse to start; a missing dir is non-fatal.
|
||||
///
|
||||
/// For a custom agent it is never empty under a live container:
|
||||
/// `provision_container` runs `setup_applied` before `create_only` makes the
|
||||
/// container at all. An archetype agent (`crate::archetypes`) has no applied
|
||||
/// repo, so its mount is the empty directory the caller creates.
|
||||
fn config_bind_source(name: &str) -> PathBuf {
|
||||
crate::paths::applied_dir(name)
|
||||
}
|
||||
|
||||
/// Append bind flags for `child`'s state and config dirs into `binds`.
|
||||
/// See docs/agent-lifecycle/persistence.md ("Cross-agent access to state")
|
||||
/// for what the holder may touch and why. Creates missing host-side
|
||||
/// directories so nspawn doesn't refuse to start; missing dirs are
|
||||
/// non-fatal.
|
||||
///
|
||||
/// **Four dirs, three different answers** — the uniformity of the
|
||||
/// original loop is what hid that:
|
||||
///
|
||||
/// - `state` is **read-write**: the holder reads and writes another
|
||||
/// agent's notes to recover it, which is the one case that needs to
|
||||
/// work while that agent is down.
|
||||
/// - `config` is **read-only**, and read-only for the *holder* is the
|
||||
/// point: a config change is a PR against the other agent's repo on the
|
||||
/// forge, reviewed and merged, never an edit in place. A writable mount
|
||||
/// here is a second path to the same file that skips the review — the
|
||||
/// boundary would then be a convention rather than a permission.
|
||||
/// - `harness` is **absent entirely**. It holds that agent's own runtime
|
||||
/// material — `bash-tasks/`, turn-stats and event sqlite dbs — none of
|
||||
/// which the holder has a stated reason to read, let alone write.
|
||||
/// hive-c0re still reads it directly on the host (`stats::hive_stats`),
|
||||
/// which needs no bind mount into anyone else's container.
|
||||
/// - `work` is **absent entirely**, like `harness`: the agent's own, often
|
||||
/// very large, worktrees and build trees.
|
||||
///
|
||||
/// ⚠️ The config-repo seeding hive-c0re does at spawn is **not** affected by
|
||||
/// the `config` flag and must not be read as a reason to widen it: that runs
|
||||
/// as hive-c0re against the host path (see `lifecycle::setup_applied`), and
|
||||
/// `read_only` on a bind constrains writers *inside* the container only.
|
||||
/// **One dir, one answer.** `state` is **read-write**: the holder reads and
|
||||
/// writes another agent's notes to recover it, which is the one case that
|
||||
/// needs to work while that agent is down. The agent's `config` is not mounted
|
||||
/// at all: a config change is a PR against its repo on the forge, reviewed and
|
||||
/// merged, and the holder reads the config by cloning it from there. Its
|
||||
/// `harness` and `work` are absent too: they hold that agent's own runtime
|
||||
/// material (`bash-tasks/`, turn-stats and event sqlite dbs) and its, often
|
||||
/// very large, worktrees and build trees, none of which the holder has a stated
|
||||
/// reason to read, let alone write. hive-c0re still reads `harness` directly on
|
||||
/// the host (`stats::hive_stats`), which needs no bind mount into anyone else's
|
||||
/// container.
|
||||
fn bind_child_agent_dirs(child: &str, binds: &mut Vec<BindMount>) {
|
||||
let Ok(child) = hive_types::Ident::parse(child) else {
|
||||
tracing::warn!(%child, "skipping child bind: invalid agent name");
|
||||
return;
|
||||
};
|
||||
let child_root = crate::paths::agent_state_dir(&child);
|
||||
for (sub, host, read_only) in [
|
||||
("state", child_root.join("state"), false),
|
||||
("config", config_bind_source(child.as_str()), true),
|
||||
] {
|
||||
let _ = std::fs::create_dir_all(&host);
|
||||
binds.push(BindMount {
|
||||
host_path: host.to_string_lossy().into_owned(),
|
||||
container_path: format!("/agents/{child}/{sub}"),
|
||||
read_only,
|
||||
});
|
||||
}
|
||||
let host = crate::paths::agent_state_dir(&child).join("state");
|
||||
let _ = std::fs::create_dir_all(&host);
|
||||
binds.push(BindMount {
|
||||
host_path: host.to_string_lossy().into_owned(),
|
||||
container_path: format!("/agents/{child}/state"),
|
||||
read_only: false,
|
||||
});
|
||||
}
|
||||
|
||||
/// Append RW bind flags for `agent_name`'s own `state`, `harness` and `work`
|
||||
|
|
@ -442,25 +401,18 @@ async fn set_nspawn_flags(
|
|||
|
||||
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
|
||||
// config; changes only ever flow through a config PR on the forge.
|
||||
// Own state, harness and work dirs - same for every agent including the
|
||||
// manager. There is no config mount: an agent reads its config by cloning
|
||||
// `agent-configs/<name>` from the forge, and changes only ever flow
|
||||
// through a config PR there.
|
||||
bind_own_agent_dirs(agent_name, notes_dir, &mut binds);
|
||||
hive_types::Ident::parse(agent_name)
|
||||
.map_err(|e| anyhow::anyhow!("invalid agent name {agent_name:?}: {e}"))?;
|
||||
let own_config = config_bind_source(agent_name);
|
||||
std::fs::create_dir_all(&own_config)
|
||||
.with_context(|| format!("create {}", own_config.display()))?;
|
||||
binds.push(BindMount {
|
||||
host_path: own_config.to_string_lossy().into_owned(),
|
||||
container_path: format!("/agents/{agent_name}/config"),
|
||||
read_only: true,
|
||||
});
|
||||
|
||||
// `ManageRootAgent` permission: mount *every* agent in the hive as a
|
||||
// virtual child. Enables recovery — the holder can update another
|
||||
// agent's config even when that agent is down. Also grants RO access
|
||||
// to /applied and /meta.
|
||||
// virtual child. Enables recovery: the holder can read and write another
|
||||
// agent's notes even when that agent is down. Also grants RO access
|
||||
// to /meta.
|
||||
//
|
||||
// ⚠️ "every agent" is the definition of this permission, not an
|
||||
// accident of how the set is computed. The grant used to hang off a
|
||||
|
|
@ -477,9 +429,8 @@ async fn set_nspawn_flags(
|
|||
// no permission now sees its own dirs and nothing else.
|
||||
if holds_manage_root_agent(agent_name, &crate::permissions::path()) {
|
||||
// Skipping self is a no-op, not a narrowing: `agent_notes_dir` is
|
||||
// `agent_state_dir/state` and `config_bind_source` is shared, so
|
||||
// binding the holder as its own virtual child reproduced the two
|
||||
// own-dir mounts pushed above, byte for byte.
|
||||
// `agent_state_dir/state`, so binding the holder as its own virtual
|
||||
// child reproduced the own-dir mount pushed above, byte for byte.
|
||||
for other in crate::topology::all_agents() {
|
||||
if other != agent_name {
|
||||
bind_child_agent_dirs(&other, &mut binds);
|
||||
|
|
@ -493,11 +444,6 @@ async fn set_nspawn_flags(
|
|||
let meta_root = crate::paths::meta_root();
|
||||
std::fs::create_dir_all(&meta_root)
|
||||
.with_context(|| format!("create {}", meta_root.display()))?;
|
||||
binds.push(BindMount {
|
||||
host_path: crate::paths::applied_root().to_string_lossy().into_owned(),
|
||||
container_path: CONTAINER_MANAGER_APPLIED_MOUNT.to_owned(),
|
||||
read_only: true,
|
||||
});
|
||||
binds.push(BindMount {
|
||||
host_path: meta_root.to_string_lossy().into_owned(),
|
||||
container_path: crate::meta::CONTAINER_MANAGER_META_MOUNT.to_owned(),
|
||||
|
|
@ -600,36 +546,15 @@ mod tests {
|
|||
}
|
||||
|
||||
/// The boundary, asserted as a whole rather than per-dir: a
|
||||
/// `ManageRootAgent` holder sees another agent's `state` and
|
||||
/// `config`, and nothing else.
|
||||
/// `ManageRootAgent` holder sees another agent's `state`, and nothing
|
||||
/// else. In particular no `config`: it is read by cloning the repo.
|
||||
#[test]
|
||||
fn holder_sees_only_other_agents_state_and_config() {
|
||||
fn holder_sees_only_other_agents_state() {
|
||||
let paths: Vec<String> = child_binds()
|
||||
.into_iter()
|
||||
.map(|b| b.container_path)
|
||||
.collect();
|
||||
assert_eq!(paths, ["/agents/kiddo/state", "/agents/kiddo/config"]);
|
||||
}
|
||||
|
||||
/// The `config` mount names the **deployed** tree, not the working
|
||||
/// clone the proposal is staged in. Asserted as "outside the child's
|
||||
/// own dir" rather than by equality: the point is that the two are
|
||||
/// different objects, which is what makes the mount unable to show a
|
||||
/// config that was never approved. Equality with `applied_dir` would
|
||||
/// restate the implementation and pass under any future relocation.
|
||||
#[test]
|
||||
fn child_config_mount_is_the_deployed_tree_not_the_working_clone() {
|
||||
let working_clone =
|
||||
crate::paths::agent_state_dir(&hive_types::Ident::parse("kiddo").expect("valid ident"));
|
||||
let config = child_binds()
|
||||
.into_iter()
|
||||
.find(|b| b.container_path.ends_with("/config"))
|
||||
.expect("a config bind");
|
||||
assert!(
|
||||
!std::path::Path::new(&config.host_path).starts_with(&working_clone),
|
||||
"config mount must not come from the child's working clone: {}",
|
||||
config.host_path
|
||||
);
|
||||
assert_eq!(paths, ["/agents/kiddo/state"]);
|
||||
}
|
||||
|
||||
/// The regression this exists for. `harness` holds the other agent's
|
||||
|
|
|
|||
Loading…
Reference in a new issue