diff --git a/hive-priv/src/container_config.rs b/hive-priv/src/container_config.rs index 8c3e083a..a012c85a 100644 --- a/hive-priv/src/container_config.rs +++ b/hive-priv/src/container_config.rs @@ -346,20 +346,25 @@ fn validate_bind_path(path: &str) -> Result<()> { /// Config mounts are matched by shape, not by a name list: the set is /// dynamic, growing with each child bound into a parent. /// -/// Only a bind whose host source has a `.git` entry gets the overlay: nspawn -/// must create the mount point inside the read-only bind, which fails -/// (`EROFS`) where there is nothing to mount over. An archetype agent's -/// config is an empty directory. The entry is looked up without following a -/// symlink, and a symlink named `.git` counts as present, so a path that -/// leads somewhere else is still hidden. +/// `/knowledge` is overlaid unconditionally: its directory is created empty +/// and cloned into later, so a container started before the clone must +/// already hide the `.git` that arrives. A config mount gets the overlay only +/// when its host source has a `.git` entry: nspawn must create the mount +/// point inside the read-only bind, which fails (`EROFS`) where there is +/// nothing to mount over, as in an archetype agent's empty config directory. +/// The entry is looked up without following a symlink, and a symlink named +/// `.git` counts as present, so a path that leads somewhere else is still +/// hidden. fn git_overlay_flags(binds: &[BindMount]) -> Vec { binds .iter() .filter(|b| { let p = b.container_path.as_str(); - p == "/knowledge" || (p.starts_with("/agents/") && p.ends_with("/config")) + p == "/knowledge" + || (p.starts_with("/agents/") + && p.ends_with("/config") + && std::fs::symlink_metadata(Path::new(&b.host_path).join(".git")).is_ok()) }) - .filter(|b| std::fs::symlink_metadata(Path::new(&b.host_path).join(".git")).is_ok()) .map(|b| format!("--tmpfs={}/.git", b.container_path)) .collect() } @@ -712,12 +717,20 @@ mod tests { let empty = scratch().to_string_lossy().into_owned(); let flags = git_overlay_flags(&[ bind_from(&empty, "/agents/updo/config"), - bind_from(&empty, "/knowledge"), bind_from(&repo_source(), "/agents/atlas/config"), ]); assert_eq!(flags, ["--tmpfs=/agents/atlas/config/.git"]); } + /// `/knowledge` is cloned into after the container may have started, so + /// its overlay does not wait for a `.git` to exist. + #[test] + fn an_empty_knowledge_source_still_gets_the_overlay() { + let empty = scratch().to_string_lossy().into_owned(); + let flags = git_overlay_flags(&[bind_from(&empty, "/knowledge")]); + assert_eq!(flags, ["--tmpfs=/knowledge/.git"]); + } + /// A symlink named `.git` is not followed: it counts as present, even /// dangling, so the path it leads to is hidden rather than skipped. #[test]