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")
|
||||
}
|
||||
|
||||
/// 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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue