feat: add template setup executable support

Templates can now include a `setup` file that runs in the workdir
before agent execution starts. Used for workspace initialization
like pulling tool binaries or installing dependencies.
This commit is contained in:
Fam Zheng
2026-03-09 17:02:12 +00:00
parent c70fbc49f0
commit f214b67f92
2 changed files with 69 additions and 0 deletions

View File

@@ -327,6 +327,22 @@ async fn agent_loop(
ensure_workspace(&exec, &workdir).await; ensure_workspace(&exec, &workdir).await;
let _ = tokio::fs::write(format!("{}/requirement.md", workdir), &requirement).await; let _ = tokio::fs::write(format!("{}/requirement.md", workdir), &requirement).await;
// Run template setup if present
if let Some(ref tid) = template_id {
let template_dir = if template::is_repo_template(tid) {
template::extract_repo_template(tid, mgr.template_repo.as_ref())
.await
.ok()
} else {
Some(std::path::Path::new(template::templates_dir()).join(tid))
};
if let Some(ref tdir) = template_dir {
if let Err(e) = template::run_setup(tdir, &workdir).await {
tracing::error!("Template setup failed for {}: {}", tid, e);
}
}
}
let instructions = if let Some(ref t) = loaded_template { let instructions = if let Some(ref t) = loaded_template {
t.instructions.clone() t.instructions.clone()
} else { } else {

View File

@@ -402,6 +402,20 @@ pub async fn extract_repo_template(template_id: &str, repo_cfg: Option<&Template
} }
} }
// Make setup executable if present
let setup_file = dest.join("setup");
if setup_file.is_file() {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = tokio::fs::metadata(&setup_file).await {
let mut perms = meta.permissions();
perms.set_mode(perms.mode() | 0o111);
let _ = tokio::fs::set_permissions(&setup_file, perms).await;
}
}
}
tracing::info!("Extracted repo template '{}' to {}", template_id, dest.display()); tracing::info!("Extracted repo template '{}' to {}", template_id, dest.display());
Ok(dest) Ok(dest)
} }
@@ -483,6 +497,44 @@ pub async fn select_template(llm: &LlmClient, requirement: &str, repo_cfg: Optio
// --- Template loading --- // --- Template loading ---
/// Run the template's `setup` executable with cwd set to workdir.
/// The template directory is read-only; setup runs in the workdir to initialize
/// the workspace environment (e.g. pulling binaries, installing deps).
pub async fn run_setup(template_dir: &Path, workdir: &str) -> anyhow::Result<()> {
let setup = template_dir.join("setup");
if !setup.exists() {
return Ok(());
}
tracing::info!("Running template setup in workdir: {}", workdir);
let output = tokio::process::Command::new(&setup)
.current_dir(workdir)
.output()
.await
.map_err(|e| anyhow::anyhow!("Failed to run setup: {}", e))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if !stdout.is_empty() {
tracing::info!("setup stdout: {}", stdout.trim_end());
}
if !stderr.is_empty() {
tracing::warn!("setup stderr: {}", stderr.trim_end());
}
if !output.status.success() {
anyhow::bail!(
"Template setup failed (exit {}): {}",
output.status.code().unwrap_or(-1),
stderr.trim_end()
);
}
tracing::info!("Template setup completed successfully");
Ok(())
}
/// Copy template contents to workdir (excluding template.json, tools/, kb/, INSTRUCTIONS.md). /// Copy template contents to workdir (excluding template.json, tools/, kb/, INSTRUCTIONS.md).
pub async fn apply_template(template_dir: &Path, workdir: &str) -> anyhow::Result<()> { pub async fn apply_template(template_dir: &Path, workdir: &str) -> anyhow::Result<()> {
if !template_dir.is_dir() { if !template_dir.is_dir() {
@@ -507,6 +559,7 @@ async fn copy_dir_recursive(src: &Path, dst: &Path) -> anyhow::Result<()> {
|| name_str == "tools" || name_str == "tools"
|| name_str == "kb" || name_str == "kb"
|| name_str == "examples" || name_str == "examples"
|| name_str == "setup"
|| name_str == "INSTRUCTIONS.md") || name_str == "INSTRUCTIONS.md")
{ {
continue; continue;