mount the applied config repo, not the proposed one

/agents/<name>/config bound the working clone a config change is staged
in, so an agent could see a proposal that was never approved -- a config
that does not govern its container. Both objects already exist; this
repoints the bind at the deployed one.

Both mounts (own + child) now resolve through config_bind_source() so
they cannot drift, and agent_proposed_dir's doc-comment is corrected:
it claimed to be manager-editable and bind-mounted, and neither is true.
This commit is contained in:
atlas 2026-08-13 18:41:04 +02:00 committed by mara
commit 127846ef1b
2 changed files with 48 additions and 7 deletions

View file

@ -1567,8 +1567,13 @@ impl Coordinator {
crate::paths::agent_runtime_dir(name).join("mcp.sock")
}
/// Manager-editable proposed config repo. Bind-mounted into the manager
/// container as `/agents/<name>/config/`.
/// The *proposed* config repo: where a config change lands before it is
/// applied, and what an approved deploy promotes into `applied_dir`.
///
/// **Not bind-mounted into any container.** An agent that edits a config
/// clones it from the forge itself; `/agents/<name>/config` shows the
/// applied (deployed) tree instead — see `config_bind_source` in
/// `lifecycle/host_config.rs`.
pub fn agent_proposed_dir(name: &hive_types::Ident) -> PathBuf {
crate::paths::agent_state_dir(name).join("config")
}

View file

@ -2,7 +2,7 @@
//! network isolation, forwarded credentials), the systemd resource-limits
//! drop-in, and the `write_dropins` verb that re-applies both.
use std::path::Path;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use hive_priv_sock::{BindMount, CredentialMount};
@ -71,6 +71,19 @@ async fn systemd_daemon_reload() -> Result<()> {
/// 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 parent's view of a child's) go through here so they cannot drift.
///
/// Never empty under a live container: `provision_container` runs
/// `setup_applied` before `create_only` makes the container at all.
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/persistence.md ("Parent access to child state") for what a
/// parent may touch and why. Creates missing host-side directories so
@ -104,8 +117,10 @@ fn bind_child_agent_dirs(child: &str, binds: &mut Vec<BindMount>) {
return;
};
let child_root = crate::paths::agent_state_dir(&child);
for (sub, read_only) in [("state", false), ("config", true)] {
let host = child_root.join(sub);
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(),
@ -249,9 +264,9 @@ async fn set_nspawn_flags(
read_only: false,
});
}
let agent_id = hive_types::Ident::parse(agent_name)
hive_types::Ident::parse(agent_name)
.map_err(|e| anyhow::anyhow!("invalid agent name {agent_name:?}: {e}"))?;
let own_config = crate::paths::agent_state_dir(&agent_id).join("config");
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 {
@ -381,6 +396,27 @@ mod tests {
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
);
}
/// The regression this exists for. `harness` holds the child's own
/// runtime material and was only ever mounted because one loop
/// treated all three dirs alike — re-adding it to that loop is a