2 Commits
Author SHA1 Message Date
Omar SobhandClaude Opus 5 a0e6b16abc fix(missions): stop agents having to work around git ownership
ci / gates (push) Failing after 9s
ci / rust (push) Skipped
ci / frontend (push) Skipped
ci / e2e (push) Skipped
ci / publish (push) Skipped
The captured diff from mission 019fc3ba contained the deliverable and, beside
it, a file the agent had invented:

    +++ b/.gitconfig_temp
    +[safe]
    +	directory = /mission/repo

The server clones as uid 65532 and the mission container runs as root, so
every `git` an agent runs is refused with "detected dubious ownership". Agents
do not surface that as a failure — they improvise around it, and the
improvisation lands in the repository. Left alone it would have been committed
and pushed to the user's repo alongside the real work.

The judge got `GIT_CONFIG_*` for this in dd8dad2; the mission containers never
did. They do now — git's environment form of `-c`, inherited by subprocesses,
so it covers the agent's own git, the `git_operations` tool, and anything that
shells out. Scoped to the checkout, never `--global`.

`.gitconfig_temp` is also added to the capture exclusions. The cause is fixed,
but a stray workaround from some future agent should not reach a user's
repository, and the exclusion costs nothing.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-02 12:11:36 -07:00
Omar SobhandClaude Opus 5 3a383aede6 fix(missions): give the uncapturable marker a real file
The marker registered an artifact at a path with nothing behind it, so any
reader following it would get a bare 404. `_outputs` survives teardown even
when the checkout does not, so the file can and should be written — and it
says plainly what happened rather than leaving an operator to infer it from
an empty response.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-02 12:03:37 -07:00
2 changed files with 48 additions and 3 deletions
+30 -3
View File
@@ -52,6 +52,13 @@ const EXCLUDED_PATHS: &[&str] = &[
".pytest_cache", ".pytest_cache",
".mypy_cache", ".mypy_cache",
"vendor", "vendor",
// Agent workaround debris. `.gitconfig_temp` appeared on mission 019fc3ba
// when an agent hit git's ownership check and wrote its own safe.directory
// config into the repository root. The cause is fixed (mission containers
// now carry GIT_CONFIG_* env), but excluding the artefact keeps a stray
// workaround out of a user's repository if an agent invents another one.
".gitconfig_temp",
".gitconfig.tmp",
]; ];
/// Cap on the captured patch. Past this the diff is truncated with a marker /// Cap on the captured patch. Past this the diff is truncated with a marker
@@ -333,6 +340,17 @@ pub async fn record_uncapturable(
mission_id: Uuid, mission_id: Uuid,
phase_id: Uuid, phase_id: Uuid,
) -> Result<(), String> { ) -> Result<(), String> {
// Write a real file behind the artifact. `_outputs` survives teardown, so
// it is still writable even though the checkout is gone — and an artifact
// row pointing at a path with nothing behind it turns every reader into a
// 404 with no explanation.
let dir = outputs_root(mission_id).join(phase_id.to_string());
if std::fs::create_dir_all(&dir).is_ok() {
let _ = std::fs::write(
dir.join("diff.patch"),
"The mission checkout was removed before this phase's changes could be\n captured. Nothing was lost that had already been captured; this phase\n simply finished after its working tree had been reaped.\n",
);
}
let rel = format!("_outputs/{mission_id}/{phase_id}/diff.patch"); let rel = format!("_outputs/{mission_id}/{phase_id}/diff.patch");
cm_db::repo::missions::register_artifact( cm_db::repo::missions::register_artifact(
pool, pool,
@@ -386,11 +404,20 @@ mod tests {
assert_eq!(parse_diffstat("\n"), (0, 0, 0)); assert_eq!(parse_diffstat("\n"), (0, 0, 0));
} }
/// Build output must never reach a patch. A phase that ran `cargo build` /// Build output and agent workaround debris must never reach a patch. A
/// leaves a `target/` bigger than the repository. /// phase that ran `cargo build` leaves a `target/` bigger than the
/// repository, and an agent that fought git's ownership check left a
/// `.gitconfig_temp` beside the real work.
#[test] #[test]
fn build_output_is_excluded() { fn build_output_is_excluded() {
for p in ["target", "node_modules", ".venv", "dist", "__pycache__"] { for p in [
"target",
"node_modules",
".venv",
"dist",
"__pycache__",
".gitconfig_temp",
] {
assert!( assert!(
EXCLUDED_PATHS.contains(&p), EXCLUDED_PATHS.contains(&p),
"{p} must be excluded from capture" "{p} must be excluded from capture"
+18
View File
@@ -285,6 +285,24 @@ impl MissionRuntimeProvisioner {
let mut env = vec![ let mut env = vec![
format!("ZEROCLAW_GATEWAY_PORT={GATEWAY_PORT}"), format!("ZEROCLAW_GATEWAY_PORT={GATEWAY_PORT}"),
format!("CM_MISSION_ID={mission_id}"), format!("CM_MISSION_ID={mission_id}"),
// Let the agents' git read the checkout.
//
// The server clones as uid 65532; this container runs as root, so
// every `git` an agent runs hits "detected dubious ownership" and
// refuses the repository. Agents do not report that as a failure —
// they improvise. On mission 019fc3ba one wrote a `.gitconfig_temp`
// containing `[safe] directory = /mission/repo` into the repository
// root, which then showed up in the captured diff and would have
// been committed and pushed to the user's repo alongside the real
// work.
//
// `GIT_CONFIG_*` is git's environment form of `-c` and is
// inherited by subprocesses, so it covers the agent's own git, any
// tool that shells out to git, and the `git_operations` tool alike.
// Scoped to the checkout; never `--global`.
"GIT_CONFIG_COUNT=1".to_string(),
"GIT_CONFIG_KEY_0=safe.directory".to_string(),
"GIT_CONFIG_VALUE_0=/mission/repo".to_string(),
]; ];
// Provider credentials forwarded into the container. // Provider credentials forwarded into the container.
// //