feat(missions): make copy-in/copy-out the default filesystem model
Copy mode shipped opt-in so that changing how every mission receives its
code required someone to type it. Four production missions and a
fail-closed harness later, opt-in is the riskier setting: the bind path
is the one with four documented work-loss incidents, and leaving it as
the default means the untested path runs whenever nobody sets the
variable. `CLAWMATES_MISSION_FS=bind` still selects it; anything else —
unset, empty, misspelt — gets copy mode, so a typo lands on the safer
path rather than the one being retired.
Also fixes a real leak found while scoping the deletion below: the git
helper built its `safe.directory` argument with `Box::leak`, justified as
"the process is short-lived". That is true of a CLI and false of cm-api,
which is a long-running server — so it leaked one allocation per git
call, growing with every phase of every mission.
The A5 deletion is NOT done here, and two of its items should never be
done:
- `scrub_remote_credentials` is a security control, not a uid
workaround. Copy mode uploads the whole `.git` into a container the
agent controls as root, which makes stripping the token from
`.git/config` more necessary, not less.
- `has_local_work` / `checkout_in_use` guard `fetch_and_reset` at every
phase launch and have nothing to do with who writes the checkout.
The host checkout still persists across phases under copy mode —
mission `019fcf62` shows the marker firing there. Deleting them
reintroduces PRIOR-PHASE-WORK-WAS-LOST.
The rest (`share_repository_across_uids`, `clear_stale_commit_editmsg`,
`-c safe.directory`) are genuinely obsolete under copy mode but stay
while `bind` remains selectable: a workaround may only be deleted once
the situation it works around can no longer be chosen.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3c91d0e172
commit
4f6719c80e
@@ -167,14 +167,20 @@ pub async fn copy_out(
|
||||
|
||||
/// 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.
|
||||
/// **Default since 2026-08-04.** It shipped opt-in, on the principle that
|
||||
/// silently changing how every mission receives its code should require
|
||||
/// someone to have typed it. Four production missions and a fail-closed
|
||||
/// harness later (`scripts/verify-mission-delivery.sh`), the opt-in is the
|
||||
/// riskier setting: the bind path is the one with four documented work-loss
|
||||
/// incidents, and leaving it as the default means the untested path is what
|
||||
/// runs when nobody sets the variable.
|
||||
///
|
||||
/// `CLAWMATES_MISSION_FS=bind` still selects the old behaviour, so a revert is
|
||||
/// one line in `.env` rather than a rollback. Anything else — unset, empty,
|
||||
/// misspelt — gets copy mode, because the failure mode of a typo should be the
|
||||
/// safer path, not the one being retired.
|
||||
pub fn copy_mode() -> bool {
|
||||
matches!(
|
||||
std::env::var("CLAWMATES_MISSION_FS").as_deref(),
|
||||
Ok("copy")
|
||||
)
|
||||
!matches!(std::env::var("CLAWMATES_MISSION_FS").as_deref(), Ok("bind"))
|
||||
}
|
||||
|
||||
/// Host directory holding a mission's checkout.
|
||||
@@ -305,12 +311,20 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The switch must be explicit — a near-miss value leaves production on
|
||||
/// the proven bind-mount path rather than silently changing it.
|
||||
/// Only the exact word `bind` opts out. A typo must land on copy mode —
|
||||
/// the path with a verification harness behind it — rather than silently
|
||||
/// selecting the one with four documented work-loss incidents.
|
||||
#[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");
|
||||
fn only_the_exact_word_bind_opts_out() {
|
||||
// Cannot set env vars in a test process without racing every other
|
||||
// test, so this asserts the predicate the function is built from.
|
||||
let opts_out = |v: &str| v == "bind";
|
||||
assert!(opts_out("bind"));
|
||||
for near_miss in ["Bind", "binds", "bound", "copy", "0", "false", ""] {
|
||||
assert!(
|
||||
!opts_out(near_miss),
|
||||
"{near_miss:?} must NOT select the bind path"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user