diff --git a/cli/src/run.rs b/cli/src/run.rs index 67a0be8e..df36c90f 100644 --- a/cli/src/run.rs +++ b/cli/src/run.rs @@ -253,6 +253,20 @@ fn run_compose( return Ok(()); } + // `services.yaml`'s `output` volume binds to `./output/{benchmark}/{task}` + // (compose/RULES.md rule 18) via a `driver_opts.device:` path — unlike a + // short-syntax host bind, that form does not auto-create the directory, + // so pre-create it here (as the invoking user, so the agent's uid-1002 + // process can still write into it — Docker would otherwise make it + // root-owned on first mount). + let task_id = envs + .iter() + .find(|(k, _)| *k == "EVAL_TASK_ID") + .map(|(_, v)| v.as_str()) + .unwrap_or("0"); + std::fs::create_dir_all(format!("output/{benchmark}/{task_id}")) + .map_err(|e| format!("failed to create host output dir: {e}"))?; + let mut cmd = Command::new("docker"); cmd.arg("compose").arg("-f").arg(&compose_ref); // `-y`: a published `oci://` stack prompts to confirm (and echoes) the diff --git a/containers/compose/services.yaml b/containers/compose/services.yaml index 4d504b54..db9c2990 100644 --- a/containers/compose/services.yaml +++ b/containers/compose/services.yaml @@ -55,4 +55,13 @@ networks: upstream: {} volumes: + # Bind to the host so results land at ./output/{benchmark}/{task-id}/ + # (compose/RULES.md rule 18) instead of an anonymous managed volume with no + # host-visible path. `${PWD}` resolves to the CLI's invocation directory, + # not this file's directory, regardless of `include:` nesting. output: + driver: local + driver_opts: + type: none + o: bind + device: ${PWD}/output/${EVAL_BENCHMARK:-aime}/${EVAL_TASK_ID:-0}