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:
parent
351341e87c
commit
127846ef1b
2 changed files with 48 additions and 7 deletions
|
|
@ -1567,8 +1567,13 @@ impl Coordinator {
|
||||||
crate::paths::agent_runtime_dir(name).join("mcp.sock")
|
crate::paths::agent_runtime_dir(name).join("mcp.sock")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manager-editable proposed config repo. Bind-mounted into the manager
|
/// The *proposed* config repo: where a config change lands before it is
|
||||||
/// container as `/agents/<name>/config/`.
|
/// 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 {
|
pub fn agent_proposed_dir(name: &hive_types::Ident) -> PathBuf {
|
||||||
crate::paths::agent_state_dir(name).join("config")
|
crate::paths::agent_state_dir(name).join("config")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
//! network isolation, forwarded credentials), the systemd resource-limits
|
//! network isolation, forwarded credentials), the systemd resource-limits
|
||||||
//! drop-in, and the `write_dropins` verb that re-applies both.
|
//! 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 anyhow::{Context, Result};
|
||||||
use hive_priv_sock::{BindMount, CredentialMount};
|
use hive_priv_sock::{BindMount, CredentialMount};
|
||||||
|
|
@ -71,6 +71,19 @@ async fn systemd_daemon_reload() -> Result<()> {
|
||||||
/// inside the container.
|
/// inside the container.
|
||||||
pub const CONTAINER_MANAGER_APPLIED_MOUNT: &str = "/applied";
|
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`.
|
/// Append bind flags for `child`'s state and config dirs into `binds`.
|
||||||
/// See docs/persistence.md ("Parent access to child state") for what a
|
/// See docs/persistence.md ("Parent access to child state") for what a
|
||||||
/// parent may touch and why. Creates missing host-side directories so
|
/// 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;
|
return;
|
||||||
};
|
};
|
||||||
let child_root = crate::paths::agent_state_dir(&child);
|
let child_root = crate::paths::agent_state_dir(&child);
|
||||||
for (sub, read_only) in [("state", false), ("config", true)] {
|
for (sub, host, read_only) in [
|
||||||
let host = child_root.join(sub);
|
("state", child_root.join("state"), false),
|
||||||
|
("config", config_bind_source(child.as_str()), true),
|
||||||
|
] {
|
||||||
let _ = std::fs::create_dir_all(&host);
|
let _ = std::fs::create_dir_all(&host);
|
||||||
binds.push(BindMount {
|
binds.push(BindMount {
|
||||||
host_path: host.to_string_lossy().into_owned(),
|
host_path: host.to_string_lossy().into_owned(),
|
||||||
|
|
@ -249,9 +264,9 @@ async fn set_nspawn_flags(
|
||||||
read_only: false,
|
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}"))?;
|
.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)
|
std::fs::create_dir_all(&own_config)
|
||||||
.with_context(|| format!("create {}", own_config.display()))?;
|
.with_context(|| format!("create {}", own_config.display()))?;
|
||||||
binds.push(BindMount {
|
binds.push(BindMount {
|
||||||
|
|
@ -381,6 +396,27 @@ mod tests {
|
||||||
assert_eq!(paths, ["/agents/kiddo/state", "/agents/kiddo/config"]);
|
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
|
/// The regression this exists for. `harness` holds the child's own
|
||||||
/// runtime material and was only ever mounted because one loop
|
/// runtime material and was only ever mounted because one loop
|
||||||
/// treated all three dirs alike — re-adding it to that loop is a
|
/// treated all three dirs alike — re-adding it to that loop is a
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue