use anyhow::Result;
use std::sync::Arc;
use crate::{agent, store::Store, types::TaskId};
use crate::cmd::run::RunArgs;
use super::BackgroundRunSpec;
pub(super) fn prepare_launch(
store: &Arc<Store>, spec: &BackgroundRunSpec, args: &RunArgs,
agent: &dyn agent::Agent, home_guard: &agent::home_isolation::IsolatedHomeGuard,
) -> Result<(std::process::Command, Option<String>)> {
let opts = run_opts(spec, args, agent.kind());
let task = store.get_task(&spec.task_id)?;
let launch_prompt = task.as_ref().and_then(|task| task.resolved_prompt.as_deref())
.unwrap_or(&spec.prompt);
let worktree_branch = task.as_ref().and_then(|task| task.worktree_branch.clone());
let mut cargo_target_dir = agent::rust_build_cache_target_dir(args.dir.as_deref(), worktree_branch.as_deref());
let uses_durable_codex_home = agent::should_use_durable_codex_home(agent.kind(), args.sandbox, args.container.is_some());
let mut temp_dir = None;
let mut writable_roots = Vec::new();
if args.container.is_none() && !args.sandbox {
let temp = if crate::cmd::run::read_only_delivery::enabled(args, agent.kind()) {
std::path::PathBuf::from(args.artifact_staging.as_deref()
.ok_or_else(|| anyhow::anyhow!("Read-only artifact staging is unavailable"))?)
} else {
cargo_target_dir = agent::scratch::prepare_cargo_target(cargo_target_dir.as_deref())?;
agent::scratch::create_temp_dir(home_guard.path())?
};
let (roots, warnings) = agent::scratch::prepare_launch_roots(
agent.kind(), args.dir.as_deref(), cargo_target_dir.as_deref(), &temp, &TaskId(spec.task_id.clone()),
args.read_only,
)?;
for warning in warnings {
store.insert_event(&warning)?;
}
temp_dir = Some(temp);
writable_roots = roots;
}
let mut std_cmd = agent
.build_command_with_context(
launch_prompt,
&opts,
agent::CommandContext { durable_codex_home: uses_durable_codex_home, writable_roots },
)
.map_err(|err| anyhow::anyhow!("Failed to build agent command: {err:#}"))?;
if args.container.is_none() && !args.sandbox {
let program = std_cmd.get_program().to_string_lossy();
agent::ensure_resolved_binary_available(&args.agent_name, &program)?;
}
agent::apply_run_env(&mut std_cmd, &opts, home_guard);
crate::remote_build::configure_task(store, &spec.task_id, &mut std_cmd, home_guard.path())?;
if let Some(temp_dir) = temp_dir { std_cmd.env("TMPDIR", temp_dir); }
finish_env(store, spec, args, agent.kind(), &mut std_cmd, cargo_target_dir.as_deref())?;
wrap_command(spec, args, agent.kind(), std_cmd)
}
fn run_opts(spec: &BackgroundRunSpec, args: &RunArgs, kind: crate::types::AgentKind) -> agent::RunOpts {
let (output, result_file) = crate::cmd::run::read_only_delivery::agent_paths(args, kind);
agent::RunOpts {
dir: args.dir.clone(),
output,
result_file,
model: args.model.clone(),
budget: args.budget,
read_only: args.read_only,
sandbox: args.sandbox,
context_files: vec![],
session_id: args.session_id.clone(),
env: agent::env_with_agent_log(
args.env.clone(),
&spec.task_id,
args.container.is_none() && !args.sandbox,
),
env_forward: args.env_forward.clone(),
}
}
fn finish_env(
store: &Store, spec: &BackgroundRunSpec, args: &RunArgs,
agent_kind: crate::types::AgentKind, cmd: &mut std::process::Command,
cargo_target_dir: Option<&str>,
) -> Result<()> {
if agent::should_use_durable_codex_home(agent_kind, args.sandbox, args.container.is_some()) {
if args.session_id.as_deref().is_some_and(agent::codex::resume_fallback_needed) {
store.insert_event(&agent::codex::resume_fallback_event(&TaskId(spec.task_id.clone())))?;
}
agent::apply_codex_home_env(cmd)?;
}
if let Some(ref dir) = args.dir {
agent::set_git_ceiling(cmd, dir);
}
if let Some(ref group) = args.group {
cmd.env("AID_GROUP", group);
}
cmd.env("AID_TASK_ID", &spec.task_id);
let depth = crate::cmd::run::task_depth(store, &spec.task_id).unwrap_or(0);
cmd.env("AID_TASK_DEPTH", depth.to_string());
agent::apply_cargo_target_env(cmd, cargo_target_dir);
Ok(())
}
fn wrap_command(
spec: &BackgroundRunSpec, args: &RunArgs, agent_kind: crate::types::AgentKind,
cmd: std::process::Command,
) -> Result<(std::process::Command, Option<String>)> {
let container_name = if let Some(image) = args.container.as_deref() {
let project_dir = args
.dir
.as_deref()
.map(std::path::Path::new)
.unwrap_or_else(|| std::path::Path::new("."));
let project_id = crate::project::detect_project_in(project_dir)
.map(|project| project.id)
.unwrap_or_else(|| spec.task_id.clone());
Some(crate::container::start_or_reuse(image, project_dir, &project_id)?)
} else {
None
};
let cmd = if let Some(container_name) = container_name.as_deref() {
crate::container::exec_in_container(&cmd, container_name)
} else if args.sandbox && crate::sandbox::can_sandbox(agent_kind) {
if !crate::sandbox::is_available() {
anyhow::bail!("--sandbox requires container CLI");
}
crate::sandbox::wrap_command(&cmd, &spec.task_id, agent_kind, args.read_only)
} else {
cmd
};
Ok((cmd, container_name))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn worker_launch_preservation_uses_resolved_prompt_with_raw_lifecycle_args() {
let home = tempfile::tempdir().unwrap();
let _guard = crate::paths::AidHomeGuard::set(home.path());
crate::paths::ensure_dirs().unwrap();
let store = Arc::new(Store::open_memory().unwrap());
let id = "t-launch-preservation";
let resolved = "Resolved launch context\nCaller brief";
store
.insert_waiting_task(
id,
"codex",
"Caller brief",
Some(resolved),
None,
None,
None,
None,
None,
false,
false,
)
.unwrap();
store
.update_task_dispatch_args(id, &RunArgs::default().dispatch_args_json().unwrap())
.unwrap();
let spec = serde_json::from_value(serde_json::json!({
"task_id": id, "worker_pid": null, "agent_name": "codex", "prompt": "Stale spec",
"dir": home.path().to_str(), "output": null, "model": null, "verify": null,
"retry": 0, "group": null, "interactive": false,
}))
.unwrap();
let args = RunArgs::for_worker(&store, &spec).unwrap();
let agent = capture_agent();
let isolated =
agent::home_isolation::IsolatedHomeGuard::create_from_home(Some(home.path()), Some(id))
.unwrap();
let (command, _) = prepare_launch(&store, &spec, &args, &agent, &isolated).unwrap();
assert_eq!(args.prompt, "Caller brief");
assert_eq!(command.get_args().last().unwrap(), resolved);
}
fn capture_agent() -> agent::custom::CustomAgent {
agent::custom::CustomAgent {
config: agent::custom::parse_config(concat!(
"[agent]\nid = \"capture\"\ndisplay_name = \"Capture\"\n",
"command = \"/bin/echo\"\nprompt_mode = \"arg\"\n"
))
.unwrap(),
}
}
#[test]
fn fix_round_one_read_only_launch_stages_explicit_report_and_output_separately() {
let temp = tempfile::tempdir().unwrap();
let _home = crate::paths::AidHomeGuard::set(temp.path());
let spec: BackgroundRunSpec = serde_json::from_value(serde_json::json!({
"task_id": "t-routing", "agent_name": "codex", "prompt": "audit",
"dir": null, "output": null, "verify": null, "retry": 0, "group": null
})).unwrap();
let staging = agent::scratch::create_temp_dir(temp.path()).unwrap();
let args = RunArgs {
agent_name: "codex".into(), read_only: true,
artifact_staging: Some(staging.display().to_string()),
output: Some("/outside/final.txt".into()),
result_file: Some("reports/explicit.md".into()), ..Default::default()
};
let opts = run_opts(&spec, &args, crate::types::AgentKind::Codex);
let task = std::path::Path::new(args.artifact_staging.as_ref().unwrap());
assert_eq!(opts.result_file.as_deref(), task.join("agent-result.md").to_str());
assert_eq!(opts.output.as_deref(), task.join("agent-output.txt").to_str());
assert_eq!(args.result_file.as_deref(), Some("reports/explicit.md"));
assert_eq!(args.output.as_deref(), Some("/outside/final.txt"));
}
}