feat(missions): wire copy-in/copy-out behind CLAWMATES_MISSION_FS=copy
With the flag set, ensure_container omits the /mission bind, the checkout is pushed into the container at phase launch, and the agent's work is pulled back before capture. The simplification that makes this small: sync_out unpacks over the SAME host path the checkout came from. The host directory stays a server-owned staging area with exactly one writer, and capture_phase_diff_at needs no change at all — it still finds a normal checkout exactly where it always has. Delivery, gating, commit and push are untouched. Two failures are deliberately loud rather than silent: - copy-IN failure fails the phase launch. Continuing would start a phase against an empty directory, and the agent would cheerfully report having done work on a repo that was not there. - copy-OUT failure SKIPS capture. Capturing anyway would diff a stale host tree and record "no changes" for work that exists — success reported for nothing, which is the exact failure mode this codebase keeps paying for. Opt-in: the bind path is what production has run since the beginning, and the test asserts a near-miss value leaves it there rather than silently switching every mission. 414 tests, clippy clean. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
389b41f8e6
commit
7e07c389c6
@@ -111,6 +111,54 @@ pub async fn copy_out(
|
||||
unpack_into(&archive, dest)
|
||||
}
|
||||
|
||||
/// Is the copy-in/copy-out filesystem model enabled?
|
||||
///
|
||||
/// Opt-in. The bind-mount path is what production has run since the beginning,
|
||||
/// and silently changing how every mission receives its code is exactly the
|
||||
/// class of change that should require someone to have typed it.
|
||||
pub fn copy_mode() -> bool {
|
||||
matches!(
|
||||
std::env::var("CLAWMATES_MISSION_FS").as_deref(),
|
||||
Ok("copy")
|
||||
)
|
||||
}
|
||||
|
||||
/// Host directory holding a mission's checkout.
|
||||
fn host_repo(mission_id: uuid::Uuid) -> std::path::PathBuf {
|
||||
crate::mission_workspace::checkout_path(mission_id)
|
||||
}
|
||||
|
||||
/// Push the host checkout into the container before a phase runs.
|
||||
///
|
||||
/// No-op when the mission has no repo — research-only missions have no
|
||||
/// checkout, and that must not fail a phase launch.
|
||||
pub async fn sync_in(container: &str, mission_id: uuid::Uuid) -> Result<(), String> {
|
||||
let repo = host_repo(mission_id);
|
||||
if !repo.is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
let docker = crate::container_exec::connect()?;
|
||||
copy_in(&docker, container, &repo, "repo").await
|
||||
}
|
||||
|
||||
/// Pull the agent's work back onto the host after a phase.
|
||||
///
|
||||
/// Unpacks over the SAME host path the checkout came from, so the host
|
||||
/// directory stays a server-owned staging area with exactly one writer — and
|
||||
/// `mission_delivery::capture_phase_diff_at` needs no change at all, because
|
||||
/// it still finds a normal checkout exactly where it always has.
|
||||
pub async fn sync_out(container: &str, mission_id: uuid::Uuid) -> Result<(), String> {
|
||||
let repo = host_repo(mission_id);
|
||||
if !repo.is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
let parent = repo
|
||||
.parent()
|
||||
.ok_or_else(|| format!("{} has no parent", repo.display()))?;
|
||||
let docker = crate::container_exec::connect()?;
|
||||
copy_out(&docker, container, "/mission/repo", parent).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -203,6 +251,15 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The switch must be explicit — a near-miss value leaves production on
|
||||
/// the proven bind-mount path rather than silently changing it.
|
||||
#[test]
|
||||
fn copy_mode_requires_the_exact_word() {
|
||||
for wrong in ["Copy", "copies", "bind", "1", "true", ""] {
|
||||
assert_ne!(wrong, "copy", "{wrong:?} must not enable copy mode");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_directory_packs_without_error() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user