mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Centralize sandbox variant handling
This commit is contained in:
parent
e1e433ea17
commit
d97b82df65
16 changed files with 463 additions and 430 deletions
|
|
@ -40,12 +40,11 @@ use crate::sessions as sessions_mod;
|
|||
use crate::sessions::{SessionStore, new_session_store};
|
||||
use fabro_interview::{Answer, Interviewer, QuestionType, WebInterviewer};
|
||||
use fabro_retro::RetroExt;
|
||||
use fabro_sandbox::SandboxSpec;
|
||||
use fabro_workflows::context::Context;
|
||||
use fabro_workflows::event::{EventEmitter, WorkflowRunEvent};
|
||||
use fabro_workflows::operations::{self, CreateRunInput, WorkflowInput};
|
||||
use fabro_workflows::pipeline::{
|
||||
self, InitOptions, LlmSpec, Persisted, SandboxEnvSpec, SandboxSpec,
|
||||
};
|
||||
use fabro_workflows::pipeline::{self, InitOptions, LlmSpec, Persisted, SandboxEnvSpec};
|
||||
use fabro_workflows::records::{Checkpoint, CheckpointExt};
|
||||
use fabro_workflows::run_options::LifecycleOptions;
|
||||
use fabro_workflows::run_options::RunOptions;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ use std::path::Path;
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::bail;
|
||||
use fabro_agent::{DockerSandbox, DockerSandboxConfig, LocalSandbox, Sandbox};
|
||||
use fabro_config::cli::load_cli_config;
|
||||
use fabro_config::project::{
|
||||
ResolveSettingsInput, resolve_settings, resolve_workflow_path, resolve_working_directory,
|
||||
|
|
@ -11,9 +10,11 @@ use fabro_config::{FabroConfig, FabroSettings};
|
|||
use fabro_graphviz::graph::{Graph, is_llm_handler_type};
|
||||
use fabro_llm::client::Client as LlmClient;
|
||||
use fabro_model::{Catalog, Provider};
|
||||
use fabro_sandbox::SandboxProvider;
|
||||
use fabro_sandbox::daytona::{DaytonaConfig, DaytonaSandbox, detect_repo_info};
|
||||
use fabro_sandbox::ssh::{GitCloneParams as SshGitCloneParams, SshConfig, SshSandbox};
|
||||
use fabro_sandbox::daytona::{DaytonaConfig, detect_repo_info};
|
||||
use fabro_sandbox::ssh::SshConfig;
|
||||
use fabro_sandbox::{
|
||||
DockerSandboxConfig, Sandbox, SandboxProvider, SandboxSpec, detect_clone_params,
|
||||
};
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflows::git::{GitSyncStatus, sync_status};
|
||||
use fabro_workflows::operations::{ValidateInput, WorkflowInput, validate};
|
||||
|
|
@ -150,37 +151,12 @@ fn resolve_exe_config(settings: &FabroSettings) -> Option<fabro_sandbox::exe::Ex
|
|||
.and_then(|sandbox| sandbox.exe.clone())
|
||||
}
|
||||
|
||||
#[cfg(feature = "exedev")]
|
||||
fn resolve_exe_clone_params(cwd: &Path) -> Option<fabro_sandbox::exe::GitCloneParams> {
|
||||
let (detected_url, branch) = match detect_repo_info(cwd) {
|
||||
Ok(info) => info,
|
||||
Err(err) => {
|
||||
tracing::warn!("No git repo detected for exe.dev clone: {err}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let url = fabro_github::ssh_url_to_https(&detected_url);
|
||||
Some(fabro_sandbox::exe::GitCloneParams { url, branch })
|
||||
}
|
||||
|
||||
fn resolve_ssh_config(settings: &FabroSettings) -> Option<SshConfig> {
|
||||
settings
|
||||
.sandbox_settings()
|
||||
.and_then(|sandbox| sandbox.ssh.clone())
|
||||
}
|
||||
|
||||
fn resolve_ssh_clone_params(cwd: &Path) -> Option<SshGitCloneParams> {
|
||||
let (detected_url, branch) = match detect_repo_info(cwd) {
|
||||
Ok(info) => info,
|
||||
Err(err) => {
|
||||
tracing::warn!("No git repo detected for SSH clone: {err}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let url = fabro_github::ssh_url_to_https(&detected_url);
|
||||
Some(SshGitCloneParams { url, branch })
|
||||
}
|
||||
|
||||
async fn mint_github_token(
|
||||
creds: &fabro_github::GitHubAppCredentials,
|
||||
origin_url: &str,
|
||||
|
|
@ -279,53 +255,55 @@ async fn run_preflight(
|
|||
let ssh_config = resolve_ssh_config(settings);
|
||||
|
||||
let sandbox_result: Result<Arc<dyn Sandbox>, String> = match sandbox_provider {
|
||||
SandboxProvider::Docker => {
|
||||
let config = DockerSandboxConfig {
|
||||
SandboxProvider::Local => SandboxSpec::Local {
|
||||
working_directory: working_directory.to_path_buf(),
|
||||
}
|
||||
.build(None)
|
||||
.await
|
||||
.map_err(|e| e.to_string()),
|
||||
SandboxProvider::Docker => SandboxSpec::Docker {
|
||||
config: DockerSandboxConfig {
|
||||
host_working_directory: working_directory.to_string_lossy().to_string(),
|
||||
..DockerSandboxConfig::default()
|
||||
};
|
||||
DockerSandbox::new(config)
|
||||
.map(|env| Arc::new(env) as Arc<dyn Sandbox>)
|
||||
.map_err(|e| format!("Docker sandbox creation failed: {e}"))
|
||||
},
|
||||
}
|
||||
SandboxProvider::Daytona => {
|
||||
let config = daytona_config.unwrap_or_default();
|
||||
match DaytonaSandbox::new(config, github_app.clone(), None, None).await {
|
||||
Ok(env) => Ok(Arc::new(env) as Arc<dyn Sandbox>),
|
||||
Err(e) => Err(format!("Daytona sandbox creation failed: {e}")),
|
||||
}
|
||||
.build(None)
|
||||
.await
|
||||
.map_err(|e| e.to_string()),
|
||||
SandboxProvider::Daytona => SandboxSpec::Daytona {
|
||||
config: daytona_config.unwrap_or_default(),
|
||||
github_app: github_app.clone(),
|
||||
run_id: None,
|
||||
clone_branch: None,
|
||||
}
|
||||
.build(None)
|
||||
.await
|
||||
.map_err(|e| format!("Daytona sandbox creation failed: {e}")),
|
||||
#[cfg(feature = "exedev")]
|
||||
SandboxProvider::Exe => {
|
||||
match fabro_sandbox::exe::OpensshRunner::connect_raw("exe.dev").await {
|
||||
Ok(mgmt_ssh) => {
|
||||
let config = exe_config.unwrap_or_default();
|
||||
let clone_params = resolve_exe_clone_params(working_directory);
|
||||
let env = fabro_sandbox::exe::ExeSandbox::new(
|
||||
Box::new(mgmt_ssh),
|
||||
config,
|
||||
clone_params,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
Ok(Arc::new(env) as Arc<dyn Sandbox>)
|
||||
}
|
||||
Err(e) => Err(format!("exe.dev SSH connection failed: {e}")),
|
||||
}
|
||||
SandboxProvider::Exe => SandboxSpec::Exe {
|
||||
config: exe_config.unwrap_or_default(),
|
||||
clone_params: detect_clone_params(working_directory),
|
||||
run_id: None,
|
||||
github_app: None,
|
||||
mgmt_destination: "exe.dev".to_string(),
|
||||
}
|
||||
.build(None)
|
||||
.await
|
||||
.map_err(|e| format!("exe sandbox creation failed: {e}")),
|
||||
#[cfg(not(feature = "exedev"))]
|
||||
SandboxProvider::Exe => Err("exe sandbox requires the exedev feature".to_string()),
|
||||
SandboxProvider::Ssh => match ssh_config {
|
||||
Some(config) => {
|
||||
let clone_params = resolve_ssh_clone_params(working_directory);
|
||||
let env = SshSandbox::new(config, clone_params, None, None);
|
||||
Ok(Arc::new(env) as Arc<dyn Sandbox>)
|
||||
Some(config) => SandboxSpec::Ssh {
|
||||
config,
|
||||
clone_params: detect_clone_params(working_directory),
|
||||
run_id: None,
|
||||
github_app: None,
|
||||
}
|
||||
.build(None)
|
||||
.await
|
||||
.map_err(|e| e.to_string()),
|
||||
None => Err("SSH sandbox requires [sandbox.ssh] config".to_string()),
|
||||
},
|
||||
SandboxProvider::Local => {
|
||||
Ok(Arc::new(LocalSandbox::new(working_directory.to_path_buf())) as Arc<dyn Sandbox>)
|
||||
}
|
||||
};
|
||||
|
||||
let sandbox_ok = match sandbox_result {
|
||||
|
|
|
|||
|
|
@ -722,6 +722,10 @@ impl Sandbox for ExeSandbox {
|
|||
self.ssh_command().map(Some)
|
||||
}
|
||||
|
||||
fn data_host(&self) -> Option<&str> {
|
||||
self.data_host.get().map(String::as_str)
|
||||
}
|
||||
|
||||
fn origin_url(&self) -> Option<&str> {
|
||||
self.origin_url.get().map(String::as_str)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod sandbox;
|
||||
pub mod sandbox_spec;
|
||||
|
||||
pub mod read_guard;
|
||||
|
||||
|
|
@ -38,6 +39,7 @@ pub use sandbox::{
|
|||
DirEntry, ExecResult, GitRunInfo, GrepOptions, Sandbox, SandboxEvent, SandboxEventCallback,
|
||||
format_lines_numbered, git_push_via_exec, setup_git_via_exec, shell_quote,
|
||||
};
|
||||
pub use sandbox_spec::{SandboxSpec, WorkdirStrategy};
|
||||
|
||||
pub use read_guard::ReadBeforeWriteSandbox;
|
||||
|
||||
|
|
@ -52,3 +54,6 @@ pub use local::LocalSandbox;
|
|||
pub use docker::{DockerSandbox, DockerSandboxConfig};
|
||||
|
||||
pub use sandbox_record::{SandboxRecord, SandboxRecordExt};
|
||||
|
||||
#[cfg(all(feature = "ssh", feature = "daytona"))]
|
||||
pub use ssh_common::detect_clone_params;
|
||||
|
|
|
|||
|
|
@ -136,6 +136,10 @@ macro_rules! delegate_sandbox {
|
|||
self.$field.host_git_dir()
|
||||
}
|
||||
|
||||
fn data_host(&self) -> Option<&str> {
|
||||
self.$field.data_host()
|
||||
}
|
||||
|
||||
fn parallel_worktree_path(
|
||||
&self,
|
||||
run_dir: &std::path::Path,
|
||||
|
|
@ -457,6 +461,12 @@ pub trait Sandbox: Send + Sync {
|
|||
None
|
||||
}
|
||||
|
||||
/// The remote host for reconnection (e.g. SSH destination, exe.dev data plane).
|
||||
/// Default is None; Exe and Ssh override.
|
||||
fn data_host(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Compute the filesystem path for a parallel branch worktree.
|
||||
fn parallel_worktree_path(
|
||||
&self,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,13 @@ pub enum SandboxProvider {
|
|||
Ssh,
|
||||
}
|
||||
|
||||
impl SandboxProvider {}
|
||||
impl SandboxProvider {
|
||||
/// True only for Local. Used by dry-run to force local execution.
|
||||
/// NOT the same as "runs on the host" (Docker is host-adjacent but not dry-run compatible).
|
||||
pub fn is_local(&self) -> bool {
|
||||
matches!(self, Self::Local)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SandboxProvider {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
|
|
|
|||
281
lib/crates/fabro-sandbox/src/sandbox_spec.rs
Normal file
281
lib/crates/fabro-sandbox/src/sandbox_spec.rs
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use fabro_types::settings::WorktreeMode;
|
||||
|
||||
use crate::sandbox_record::SandboxRecord;
|
||||
use crate::{Sandbox, SandboxEventCallback};
|
||||
|
||||
#[cfg(feature = "daytona")]
|
||||
use crate::daytona::{DaytonaConfig, DaytonaSandbox, DaytonaSnapshotConfig};
|
||||
#[cfg(feature = "docker")]
|
||||
use crate::docker::{DockerSandbox, DockerSandboxConfig};
|
||||
#[cfg(feature = "exe")]
|
||||
use crate::exe::{ExeConfig, ExeSandbox, GitCloneParams as ExeGitCloneParams, OpensshRunner};
|
||||
use crate::local::LocalSandbox;
|
||||
#[cfg(feature = "ssh")]
|
||||
use crate::ssh::{GitCloneParams as SshGitCloneParams, SshConfig, SshSandbox};
|
||||
|
||||
#[cfg(any(feature = "daytona", feature = "exe", feature = "ssh"))]
|
||||
use fabro_github::GitHubAppCredentials;
|
||||
|
||||
/// Options for sandbox initialization and construction.
|
||||
pub enum SandboxSpec {
|
||||
Local {
|
||||
working_directory: PathBuf,
|
||||
},
|
||||
#[cfg(feature = "docker")]
|
||||
Docker {
|
||||
config: DockerSandboxConfig,
|
||||
},
|
||||
#[cfg(feature = "daytona")]
|
||||
Daytona {
|
||||
config: DaytonaConfig,
|
||||
github_app: Option<GitHubAppCredentials>,
|
||||
run_id: Option<String>,
|
||||
clone_branch: Option<String>,
|
||||
},
|
||||
#[cfg(feature = "exe")]
|
||||
Exe {
|
||||
config: ExeConfig,
|
||||
clone_params: Option<ExeGitCloneParams>,
|
||||
run_id: Option<String>,
|
||||
github_app: Option<GitHubAppCredentials>,
|
||||
mgmt_destination: String,
|
||||
},
|
||||
#[cfg(feature = "ssh")]
|
||||
Ssh {
|
||||
config: SshConfig,
|
||||
clone_params: Option<SshGitCloneParams>,
|
||||
run_id: Option<String>,
|
||||
github_app: Option<GitHubAppCredentials>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum WorkdirStrategy {
|
||||
LocalDirectory,
|
||||
LocalWorktree,
|
||||
Cloud,
|
||||
}
|
||||
|
||||
impl SandboxSpec {
|
||||
pub fn provider_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Local { .. } => "local",
|
||||
#[cfg(feature = "docker")]
|
||||
Self::Docker { .. } => "docker",
|
||||
#[cfg(feature = "daytona")]
|
||||
Self::Daytona { .. } => "daytona",
|
||||
#[cfg(feature = "exe")]
|
||||
Self::Exe { .. } => "exe",
|
||||
#[cfg(feature = "ssh")]
|
||||
Self::Ssh { .. } => "ssh",
|
||||
}
|
||||
}
|
||||
|
||||
/// Host-accessible repo path for git status / worktree decisions.
|
||||
/// Only Local and Docker have one.
|
||||
pub fn host_repo_path(&self) -> Option<PathBuf> {
|
||||
match self {
|
||||
Self::Local { working_directory } => Some(working_directory.clone()),
|
||||
#[cfg(feature = "docker")]
|
||||
Self::Docker { config } => Some(PathBuf::from(&config.host_working_directory)),
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a SandboxRecord for persistence.
|
||||
pub fn to_sandbox_record(&self, sandbox: &dyn Sandbox) -> SandboxRecord {
|
||||
let working_directory = sandbox.working_directory().to_string();
|
||||
let identifier = {
|
||||
let info = sandbox.sandbox_info();
|
||||
if info.is_empty() { None } else { Some(info) }
|
||||
};
|
||||
|
||||
match self {
|
||||
#[cfg(feature = "docker")]
|
||||
Self::Docker { config } => SandboxRecord {
|
||||
provider: self.provider_name().to_string(),
|
||||
working_directory: working_directory.clone(),
|
||||
identifier,
|
||||
host_working_directory: Some(config.host_working_directory.clone()),
|
||||
container_mount_point: Some(working_directory),
|
||||
data_host: None,
|
||||
},
|
||||
#[cfg(feature = "ssh")]
|
||||
Self::Ssh { .. } => SandboxRecord {
|
||||
provider: self.provider_name().to_string(),
|
||||
working_directory,
|
||||
identifier,
|
||||
host_working_directory: None,
|
||||
container_mount_point: None,
|
||||
data_host: sandbox.data_host().map(ToOwned::to_owned),
|
||||
},
|
||||
#[cfg(feature = "exe")]
|
||||
Self::Exe { .. } => SandboxRecord {
|
||||
provider: self.provider_name().to_string(),
|
||||
working_directory,
|
||||
identifier,
|
||||
host_working_directory: None,
|
||||
container_mount_point: None,
|
||||
data_host: sandbox.data_host().map(ToOwned::to_owned),
|
||||
},
|
||||
_ => SandboxRecord {
|
||||
provider: self.provider_name().to_string(),
|
||||
working_directory,
|
||||
identifier,
|
||||
host_working_directory: None,
|
||||
container_mount_point: None,
|
||||
data_host: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply devcontainer snapshot config. Only Daytona uses this.
|
||||
#[cfg(feature = "daytona")]
|
||||
pub fn apply_devcontainer_snapshot(&mut self, snapshot: DaytonaSnapshotConfig) {
|
||||
if let Self::Daytona { config, .. } = self {
|
||||
config.snapshot = Some(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn workdir_strategy(
|
||||
&self,
|
||||
worktree_mode: WorktreeMode,
|
||||
git_is_clean: bool,
|
||||
checkpoint_present: bool,
|
||||
) -> WorkdirStrategy {
|
||||
if checkpoint_present {
|
||||
return match self {
|
||||
Self::Local { .. } => WorkdirStrategy::LocalDirectory,
|
||||
#[cfg(feature = "docker")]
|
||||
Self::Docker { .. } => WorkdirStrategy::LocalDirectory,
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => WorkdirStrategy::Cloud,
|
||||
};
|
||||
}
|
||||
|
||||
match self {
|
||||
Self::Local { .. } => match worktree_mode {
|
||||
WorktreeMode::Always => WorkdirStrategy::LocalWorktree,
|
||||
WorktreeMode::Clean => {
|
||||
if git_is_clean {
|
||||
WorkdirStrategy::LocalWorktree
|
||||
} else {
|
||||
WorkdirStrategy::LocalDirectory
|
||||
}
|
||||
}
|
||||
WorktreeMode::Dirty => {
|
||||
if git_is_clean {
|
||||
WorkdirStrategy::LocalDirectory
|
||||
} else {
|
||||
WorkdirStrategy::LocalWorktree
|
||||
}
|
||||
}
|
||||
WorktreeMode::Never => WorkdirStrategy::LocalDirectory,
|
||||
},
|
||||
#[cfg(feature = "docker")]
|
||||
Self::Docker { .. } => WorkdirStrategy::LocalDirectory,
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => WorkdirStrategy::Cloud,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build(
|
||||
&self,
|
||||
event_callback: Option<SandboxEventCallback>,
|
||||
) -> Result<Arc<dyn Sandbox>, anyhow::Error> {
|
||||
match self {
|
||||
Self::Local { working_directory } => {
|
||||
let mut sandbox = LocalSandbox::new(working_directory.clone());
|
||||
if let Some(callback) = event_callback {
|
||||
sandbox.set_event_callback(callback);
|
||||
}
|
||||
Ok(Arc::new(sandbox))
|
||||
}
|
||||
#[cfg(feature = "docker")]
|
||||
Self::Docker { config } => {
|
||||
let mut sandbox = DockerSandbox::new(DockerSandboxConfig {
|
||||
image: config.image.clone(),
|
||||
host_working_directory: config.host_working_directory.clone(),
|
||||
container_mount_point: config.container_mount_point.clone(),
|
||||
network_mode: config.network_mode.clone(),
|
||||
extra_mounts: config.extra_mounts.clone(),
|
||||
memory_limit: config.memory_limit,
|
||||
cpu_quota: config.cpu_quota,
|
||||
auto_pull: config.auto_pull,
|
||||
env_vars: config.env_vars.clone(),
|
||||
})
|
||||
.map_err(|e| anyhow!("Failed to create Docker sandbox: {e}"))?;
|
||||
if let Some(callback) = event_callback {
|
||||
sandbox.set_event_callback(callback);
|
||||
}
|
||||
Ok(Arc::new(sandbox))
|
||||
}
|
||||
#[cfg(feature = "daytona")]
|
||||
Self::Daytona {
|
||||
config,
|
||||
github_app,
|
||||
run_id,
|
||||
clone_branch,
|
||||
} => {
|
||||
let mut sandbox = DaytonaSandbox::new(
|
||||
config.clone(),
|
||||
github_app.clone(),
|
||||
run_id.clone(),
|
||||
clone_branch.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
if let Some(callback) = event_callback {
|
||||
sandbox.set_event_callback(callback);
|
||||
}
|
||||
Ok(Arc::new(sandbox))
|
||||
}
|
||||
#[cfg(feature = "exe")]
|
||||
Self::Exe {
|
||||
config,
|
||||
clone_params,
|
||||
run_id,
|
||||
github_app,
|
||||
mgmt_destination,
|
||||
} => {
|
||||
let mgmt_ssh = OpensshRunner::connect_raw(mgmt_destination)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to connect to {mgmt_destination}: {e}"))?;
|
||||
let mut sandbox = ExeSandbox::new(
|
||||
Box::new(mgmt_ssh),
|
||||
config.clone(),
|
||||
clone_params.clone(),
|
||||
run_id.clone(),
|
||||
github_app.clone(),
|
||||
);
|
||||
if let Some(callback) = event_callback {
|
||||
sandbox.set_event_callback(callback);
|
||||
}
|
||||
Ok(Arc::new(sandbox))
|
||||
}
|
||||
#[cfg(feature = "ssh")]
|
||||
Self::Ssh {
|
||||
config,
|
||||
clone_params,
|
||||
run_id,
|
||||
github_app,
|
||||
} => {
|
||||
let mut sandbox = SshSandbox::new(
|
||||
config.clone(),
|
||||
clone_params.clone(),
|
||||
run_id.clone(),
|
||||
github_app.clone(),
|
||||
);
|
||||
if let Some(callback) = event_callback {
|
||||
sandbox.set_event_callback(callback);
|
||||
}
|
||||
Ok(Arc::new(sandbox))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -593,6 +593,10 @@ impl Sandbox for SshSandbox {
|
|||
Ok(Some(self.ssh_command()))
|
||||
}
|
||||
|
||||
fn data_host(&self) -> Option<&str> {
|
||||
Some(&self.config.destination)
|
||||
}
|
||||
|
||||
fn origin_url(&self) -> Option<&str> {
|
||||
self.origin_url.get().map(String::as_str)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
//! Shared types and utilities for SSH-based sandbox implementations (exe, ssh).
|
||||
|
||||
use std::path::Path;
|
||||
use std::time::Instant;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
|
@ -41,6 +42,19 @@ pub struct GitCloneParams {
|
|||
pub branch: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "daytona")]
|
||||
pub fn detect_clone_params(cwd: &Path) -> Option<GitCloneParams> {
|
||||
let (detected_url, branch) = match crate::daytona::detect_repo_info(cwd) {
|
||||
Ok(info) => info,
|
||||
Err(err) => {
|
||||
tracing::warn!("No git repo detected for sandbox clone: {err}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let url = fabro_github::ssh_url_to_https(&detected_url);
|
||||
Some(GitCloneParams { url, branch })
|
||||
}
|
||||
|
||||
/// Wrap a shell command in base64 encoding to avoid escaping issues.
|
||||
pub(crate) fn wrap_bash_command(command: &str) -> String {
|
||||
let encoded = STANDARD.encode(command);
|
||||
|
|
|
|||
|
|
@ -293,6 +293,10 @@ impl Sandbox for WorktreeSandbox {
|
|||
Some(&self.config.worktree_path)
|
||||
}
|
||||
|
||||
fn data_host(&self) -> Option<&str> {
|
||||
self.inner.data_host()
|
||||
}
|
||||
|
||||
async fn setup_git_for_run(&self, run_id: &str) -> Result<Option<crate::GitRunInfo>, String> {
|
||||
self.inner.setup_git_for_run(run_id).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ mod start;
|
|||
mod test_support;
|
||||
mod validate;
|
||||
|
||||
pub use crate::pipeline::{DevcontainerSpec, LlmSpec, SandboxEnvSpec, SandboxSpec};
|
||||
pub use crate::pipeline::{DevcontainerSpec, LlmSpec, SandboxEnvSpec};
|
||||
pub use create::{CreateRunInput, CreatedRun, create};
|
||||
pub use fabro_sandbox::SandboxSpec;
|
||||
pub use fork::{ForkRunInput, fork};
|
||||
pub use resume::resume;
|
||||
pub use rewind::{
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use fabro_config::sandbox::WorktreeMode;
|
|||
use fabro_config::{project as project_config, run as run_config, sandbox as sandbox_config};
|
||||
use fabro_interview::{AutoApproveInterviewer, Interviewer};
|
||||
use fabro_model::{Catalog, FallbackTarget, Provider};
|
||||
use fabro_sandbox::SandboxProvider;
|
||||
use fabro_sandbox::{SandboxProvider, SandboxSpec, detect_clone_params};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::context::Context;
|
||||
|
|
@ -23,8 +23,8 @@ use crate::handler::HandlerRegistry;
|
|||
use crate::outcome::{Outcome, StageStatus};
|
||||
use crate::pipeline::{
|
||||
self, DevcontainerSpec, FinalizeOptions, Finalized, InitOptions, LlmSpec, Persisted,
|
||||
PullRequestOptions, RetroOptions, SandboxEnvSpec, SandboxSpec, build_conclusion,
|
||||
classify_engine_result, persist_terminal_outcome,
|
||||
PullRequestOptions, RetroOptions, SandboxEnvSpec, build_conclusion, classify_engine_result,
|
||||
persist_terminal_outcome,
|
||||
};
|
||||
use crate::records::{Checkpoint, Conclusion, ConclusionExt, RunRecord, RunRecordExt};
|
||||
use crate::run_options::{GitCheckpointOptions, LifecycleOptions, RunOptions};
|
||||
|
|
@ -33,7 +33,7 @@ use fabro_config::run::PullRequestSettings;
|
|||
use fabro_retro::retro::Retro;
|
||||
use fabro_sandbox::daytona::DaytonaConfig;
|
||||
use fabro_sandbox::daytona::detect_repo_info;
|
||||
use fabro_sandbox::ssh::{GitCloneParams as SshGitCloneParams, SshConfig};
|
||||
use fabro_sandbox::ssh::SshConfig;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
struct RunSession {
|
||||
|
|
@ -174,12 +174,11 @@ impl RunSession {
|
|||
.unwrap_or((None, None));
|
||||
|
||||
let sandbox_provider = resolve_sandbox_provider(&settings)?;
|
||||
let sandbox_provider =
|
||||
if settings.dry_run_enabled() && !matches!(sandbox_provider, SandboxProvider::Local) {
|
||||
SandboxProvider::Local
|
||||
} else {
|
||||
sandbox_provider
|
||||
};
|
||||
let sandbox_provider = if settings.dry_run_enabled() && !sandbox_provider.is_local() {
|
||||
SandboxProvider::Local
|
||||
} else {
|
||||
sandbox_provider
|
||||
};
|
||||
let model = settings
|
||||
.llm
|
||||
.as_ref()
|
||||
|
|
@ -225,7 +224,7 @@ impl RunSession {
|
|||
#[cfg(feature = "exedev")]
|
||||
SandboxProvider::Exe => SandboxSpec::Exe {
|
||||
config: resolve_exe_config(&settings).unwrap_or_default(),
|
||||
clone_params: resolve_exe_clone_params(&working_directory),
|
||||
clone_params: detect_clone_params(&working_directory),
|
||||
run_id: Some(record.run_id.clone()),
|
||||
github_app: services.github_app.clone(),
|
||||
mgmt_destination: "exe.dev".to_string(),
|
||||
|
|
@ -242,7 +241,7 @@ impl RunSession {
|
|||
"--sandbox ssh requires [sandbox.ssh] config".to_string(),
|
||||
)
|
||||
})?,
|
||||
clone_params: resolve_ssh_clone_params(&working_directory),
|
||||
clone_params: detect_clone_params(&working_directory),
|
||||
run_id: Some(record.run_id.clone()),
|
||||
github_app: services.github_app.clone(),
|
||||
},
|
||||
|
|
@ -346,37 +345,12 @@ fn resolve_exe_config(settings: &FabroSettings) -> Option<fabro_sandbox::exe::Ex
|
|||
.and_then(|sandbox| sandbox.exe.clone())
|
||||
}
|
||||
|
||||
#[cfg(feature = "exedev")]
|
||||
fn resolve_exe_clone_params(cwd: &Path) -> Option<fabro_sandbox::exe::GitCloneParams> {
|
||||
let (detected_url, branch) = match fabro_sandbox::daytona::detect_repo_info(cwd) {
|
||||
Ok(info) => info,
|
||||
Err(err) => {
|
||||
tracing::warn!("No git repo detected for exe.dev clone: {err}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let url = fabro_github::ssh_url_to_https(&detected_url);
|
||||
Some(fabro_sandbox::exe::GitCloneParams { url, branch })
|
||||
}
|
||||
|
||||
fn resolve_ssh_config(settings: &FabroSettings) -> Option<SshConfig> {
|
||||
settings
|
||||
.sandbox_settings()
|
||||
.and_then(|sandbox| sandbox.ssh.clone())
|
||||
}
|
||||
|
||||
fn resolve_ssh_clone_params(cwd: &Path) -> Option<SshGitCloneParams> {
|
||||
let (detected_url, branch) = match detect_repo_info(cwd) {
|
||||
Ok(info) => info,
|
||||
Err(err) => {
|
||||
tracing::warn!("No git repo detected for SSH clone: {err}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let url = fabro_github::ssh_url_to_https(&detected_url);
|
||||
Some(SshGitCloneParams { url, branch })
|
||||
}
|
||||
|
||||
fn resolve_fallback_chain(
|
||||
provider: Provider,
|
||||
model: &str,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use fabro_config::FabroSettings;
|
|||
use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
|
||||
use fabro_hooks::HookConfig;
|
||||
use fabro_interview::AutoApproveInterviewer;
|
||||
use fabro_sandbox::SandboxSpec;
|
||||
|
||||
use super::*;
|
||||
use crate::context::{self, Context};
|
||||
|
|
@ -20,7 +21,7 @@ use crate::handler::start::StartHandler;
|
|||
use crate::handler::{Handler as HandlerTrait, HandlerRegistry};
|
||||
use crate::outcome::{Outcome, OutcomeExt, StageStatus};
|
||||
use crate::pipeline::initialize;
|
||||
use crate::pipeline::types::{InitOptions, LlmSpec, Persisted, SandboxEnvSpec, SandboxSpec};
|
||||
use crate::pipeline::types::{InitOptions, LlmSpec, Persisted, SandboxEnvSpec};
|
||||
use crate::records::{Checkpoint, CheckpointExt, RunRecord, StartRecordExt};
|
||||
use crate::run_options::{GitCheckpointOptions, LifecycleOptions, RunOptions};
|
||||
use crate::test_support::run_graph;
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@ use std::sync::Arc;
|
|||
use std::time::Instant;
|
||||
|
||||
use fabro_agent::Sandbox;
|
||||
use fabro_config::sandbox::WorktreeMode;
|
||||
use fabro_hooks::{HookContext, HookDecision, HookEvent, HookRunner};
|
||||
use fabro_llm::client::Client;
|
||||
use fabro_sandbox::{
|
||||
DockerSandbox, LocalSandbox, ReadBeforeWriteSandbox, SandboxRecord, SandboxRecordExt,
|
||||
ReadBeforeWriteSandbox, SandboxEventCallback, SandboxRecordExt, WorkdirStrategy,
|
||||
WorktreeConfig, WorktreeSandbox,
|
||||
};
|
||||
use shlex::try_quote;
|
||||
|
|
@ -19,28 +18,13 @@ use crate::event::{EventEmitter, RunNoticeLevel, WorkflowRunEvent};
|
|||
use crate::git::{self, GitSyncStatus, MetadataStore};
|
||||
use crate::handler::llm::{AgentApiBackend, AgentCliBackend, BackendRouter};
|
||||
use crate::handler::{HandlerRegistry, default_registry};
|
||||
use crate::run_options::{GitCheckpointOptions, RunOptions};
|
||||
use fabro_sandbox::daytona::DaytonaSandbox;
|
||||
use fabro_sandbox::docker::DockerSandboxConfig;
|
||||
use fabro_sandbox::ssh::SshSandbox;
|
||||
use crate::run_options::GitCheckpointOptions;
|
||||
use tokio::process::Command as TokioCommand;
|
||||
use tokio::runtime::Handle;
|
||||
use tokio::task::spawn_blocking;
|
||||
use tokio::time::timeout as tokio_timeout;
|
||||
|
||||
use super::types::{InitOptions, Initialized, LlmSpec, Persisted, SandboxEnvSpec, SandboxSpec};
|
||||
|
||||
struct SandboxBuildResult {
|
||||
sandbox: Arc<dyn Sandbox>,
|
||||
worktree_created: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum WorkdirStrategy {
|
||||
LocalDirectory,
|
||||
LocalWorktree,
|
||||
Cloud,
|
||||
}
|
||||
use super::types::{InitOptions, Initialized, LlmSpec, Persisted, SandboxEnvSpec};
|
||||
|
||||
struct WorktreePlan {
|
||||
branch_name: String,
|
||||
|
|
@ -73,64 +57,6 @@ fn emit_run_notice(
|
|||
});
|
||||
}
|
||||
|
||||
fn sandbox_provider_name(spec: &SandboxSpec) -> &'static str {
|
||||
match spec {
|
||||
SandboxSpec::Local { .. } => "local",
|
||||
SandboxSpec::Docker { .. } => "docker",
|
||||
SandboxSpec::Daytona { .. } => "daytona",
|
||||
#[cfg(feature = "exedev")]
|
||||
SandboxSpec::Exe { .. } => "exe",
|
||||
SandboxSpec::Ssh { .. } => "ssh",
|
||||
}
|
||||
}
|
||||
|
||||
fn host_repo_path_for_planning(run_options: &RunOptions, spec: &SandboxSpec) -> Option<PathBuf> {
|
||||
run_options.host_repo_path.clone().or_else(|| match spec {
|
||||
SandboxSpec::Local { working_directory } => Some(working_directory.clone()),
|
||||
SandboxSpec::Docker { config } => Some(PathBuf::from(&config.host_working_directory)),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_workdir_strategy(
|
||||
spec: &SandboxSpec,
|
||||
worktree_mode: WorktreeMode,
|
||||
git_status: GitSyncStatus,
|
||||
checkpoint_present: bool,
|
||||
) -> WorkdirStrategy {
|
||||
if checkpoint_present {
|
||||
return match spec {
|
||||
SandboxSpec::Local { .. } | SandboxSpec::Docker { .. } => {
|
||||
WorkdirStrategy::LocalDirectory
|
||||
}
|
||||
_ => WorkdirStrategy::Cloud,
|
||||
};
|
||||
}
|
||||
|
||||
match spec {
|
||||
SandboxSpec::Local { .. } => match worktree_mode {
|
||||
WorktreeMode::Always => WorkdirStrategy::LocalWorktree,
|
||||
WorktreeMode::Clean => {
|
||||
if git_status.is_clean() {
|
||||
WorkdirStrategy::LocalWorktree
|
||||
} else {
|
||||
WorkdirStrategy::LocalDirectory
|
||||
}
|
||||
}
|
||||
WorktreeMode::Dirty => {
|
||||
if git_status.is_clean() {
|
||||
WorkdirStrategy::LocalDirectory
|
||||
} else {
|
||||
WorkdirStrategy::LocalWorktree
|
||||
}
|
||||
}
|
||||
WorktreeMode::Never => WorkdirStrategy::LocalDirectory,
|
||||
},
|
||||
SandboxSpec::Docker { .. } => WorkdirStrategy::LocalDirectory,
|
||||
_ => WorkdirStrategy::Cloud,
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_worktree_plan(
|
||||
options: &mut InitOptions,
|
||||
) -> Result<Option<WorktreePlan>, FabroError> {
|
||||
|
|
@ -139,16 +65,19 @@ async fn resolve_worktree_plan(
|
|||
return Ok(None);
|
||||
};
|
||||
|
||||
let host_repo_path = host_repo_path_for_planning(&options.run_options, &options.sandbox);
|
||||
let host_repo_path = options
|
||||
.run_options
|
||||
.host_repo_path
|
||||
.clone()
|
||||
.or_else(|| options.sandbox.host_repo_path());
|
||||
let git_status = host_repo_path
|
||||
.as_ref()
|
||||
.map_or(GitSyncStatus::Dirty, |path| {
|
||||
git::sync_status(path, "origin", options.run_options.base_branch.as_deref())
|
||||
});
|
||||
let strategy = resolve_workdir_strategy(
|
||||
&options.sandbox,
|
||||
let strategy = options.sandbox.workdir_strategy(
|
||||
worktree_mode,
|
||||
git_status,
|
||||
git_status.is_clean(),
|
||||
options.checkpoint.is_some(),
|
||||
);
|
||||
|
||||
|
|
@ -256,154 +185,6 @@ async fn resolve_worktree_plan(
|
|||
}
|
||||
}
|
||||
|
||||
fn local_sandbox_with_callback(
|
||||
working_directory: PathBuf,
|
||||
emitter: Arc<EventEmitter>,
|
||||
) -> Arc<dyn Sandbox> {
|
||||
let mut sandbox = LocalSandbox::new(working_directory);
|
||||
sandbox.set_event_callback(Arc::new(move |event| {
|
||||
emitter.emit(&WorkflowRunEvent::Sandbox { event });
|
||||
}));
|
||||
Arc::new(sandbox)
|
||||
}
|
||||
|
||||
async fn build_sandbox(
|
||||
spec: &SandboxSpec,
|
||||
worktree_plan: Option<&WorktreePlan>,
|
||||
emitter: Arc<EventEmitter>,
|
||||
) -> Result<SandboxBuildResult, FabroError> {
|
||||
let mut worktree_created = false;
|
||||
let sandbox: Arc<dyn Sandbox> = match spec {
|
||||
SandboxSpec::Local { working_directory } => {
|
||||
if let Some(plan) = worktree_plan {
|
||||
let inner =
|
||||
local_sandbox_with_callback(working_directory.clone(), Arc::clone(&emitter));
|
||||
let mut worktree = WorktreeSandbox::new(
|
||||
inner,
|
||||
WorktreeConfig {
|
||||
branch_name: plan.branch_name.clone(),
|
||||
base_sha: plan.base_sha.clone(),
|
||||
worktree_path: plan.worktree_path.to_string_lossy().into_owned(),
|
||||
skip_branch_creation: false,
|
||||
},
|
||||
);
|
||||
worktree.set_event_callback(Arc::clone(&emitter).worktree_callback());
|
||||
match worktree.initialize().await {
|
||||
Ok(()) => {
|
||||
worktree_created = true;
|
||||
Arc::new(ReadBeforeWriteSandbox::new(Arc::new(worktree)))
|
||||
}
|
||||
Err(e) => {
|
||||
emit_run_notice(
|
||||
&emitter,
|
||||
RunNoticeLevel::Warn,
|
||||
"worktree_setup_failed",
|
||||
format!("Git worktree setup failed ({e}), running without worktree."),
|
||||
);
|
||||
Arc::new(ReadBeforeWriteSandbox::new(local_sandbox_with_callback(
|
||||
working_directory.clone(),
|
||||
Arc::clone(&emitter),
|
||||
)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Arc::new(ReadBeforeWriteSandbox::new(local_sandbox_with_callback(
|
||||
working_directory.clone(),
|
||||
Arc::clone(&emitter),
|
||||
)))
|
||||
}
|
||||
}
|
||||
SandboxSpec::Docker { config } => {
|
||||
let mut sandbox = DockerSandbox::new(DockerSandboxConfig {
|
||||
image: config.image.clone(),
|
||||
host_working_directory: config.host_working_directory.clone(),
|
||||
container_mount_point: config.container_mount_point.clone(),
|
||||
network_mode: config.network_mode.clone(),
|
||||
extra_mounts: config.extra_mounts.clone(),
|
||||
memory_limit: config.memory_limit,
|
||||
cpu_quota: config.cpu_quota,
|
||||
auto_pull: config.auto_pull,
|
||||
env_vars: config.env_vars.clone(),
|
||||
})
|
||||
.map_err(|e| FabroError::engine(format!("Failed to create Docker sandbox: {e}")))?;
|
||||
let emitter_cb = Arc::clone(&emitter);
|
||||
sandbox.set_event_callback(Arc::new(move |event| {
|
||||
emitter_cb.emit(&WorkflowRunEvent::Sandbox { event });
|
||||
}));
|
||||
Arc::new(ReadBeforeWriteSandbox::new(Arc::new(sandbox)))
|
||||
}
|
||||
SandboxSpec::Daytona {
|
||||
config,
|
||||
github_app,
|
||||
run_id,
|
||||
clone_branch,
|
||||
} => {
|
||||
let mut sandbox = DaytonaSandbox::new(
|
||||
config.clone(),
|
||||
github_app.clone(),
|
||||
run_id.clone(),
|
||||
clone_branch.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(FabroError::engine)?;
|
||||
let emitter_cb = Arc::clone(&emitter);
|
||||
sandbox.set_event_callback(Arc::new(move |event| {
|
||||
emitter_cb.emit(&WorkflowRunEvent::Sandbox { event });
|
||||
}));
|
||||
Arc::new(ReadBeforeWriteSandbox::new(Arc::new(sandbox)))
|
||||
}
|
||||
#[cfg(feature = "exedev")]
|
||||
SandboxSpec::Exe {
|
||||
config,
|
||||
clone_params,
|
||||
run_id,
|
||||
github_app,
|
||||
mgmt_destination,
|
||||
} => {
|
||||
let mgmt_ssh = fabro_sandbox::exe::OpensshRunner::connect_raw(mgmt_destination)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
FabroError::engine(format!("Failed to connect to {mgmt_destination}: {e}"))
|
||||
})?;
|
||||
let mut sandbox = fabro_sandbox::exe::ExeSandbox::new(
|
||||
Box::new(mgmt_ssh),
|
||||
config.clone(),
|
||||
clone_params.clone(),
|
||||
run_id.clone(),
|
||||
github_app.clone(),
|
||||
);
|
||||
let emitter_cb = Arc::clone(&emitter);
|
||||
sandbox.set_event_callback(Arc::new(move |event| {
|
||||
emitter_cb.emit(&WorkflowRunEvent::Sandbox { event });
|
||||
}));
|
||||
Arc::new(ReadBeforeWriteSandbox::new(Arc::new(sandbox)))
|
||||
}
|
||||
SandboxSpec::Ssh {
|
||||
config,
|
||||
clone_params,
|
||||
run_id,
|
||||
github_app,
|
||||
} => {
|
||||
let mut sandbox = SshSandbox::new(
|
||||
config.clone(),
|
||||
clone_params.clone(),
|
||||
run_id.clone(),
|
||||
github_app.clone(),
|
||||
);
|
||||
let emitter_cb = Arc::clone(&emitter);
|
||||
sandbox.set_event_callback(Arc::new(move |event| {
|
||||
emitter_cb.emit(&WorkflowRunEvent::Sandbox { event });
|
||||
}));
|
||||
Arc::new(ReadBeforeWriteSandbox::new(Arc::new(sandbox)))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(SandboxBuildResult {
|
||||
sandbox,
|
||||
worktree_created,
|
||||
})
|
||||
}
|
||||
|
||||
async fn mint_github_token(
|
||||
creds: &fabro_github::GitHubAppCredentials,
|
||||
origin_url: &str,
|
||||
|
|
@ -531,12 +312,9 @@ async fn resolve_devcontainer(options: &mut InitOptions) -> Result<(), FabroErro
|
|||
workspace_folder: config.workspace_folder.clone(),
|
||||
});
|
||||
|
||||
if let SandboxSpec::Daytona {
|
||||
config: daytona, ..
|
||||
} = &mut options.sandbox
|
||||
{
|
||||
daytona.snapshot = Some(devcontainer_to_snapshot_config(&config));
|
||||
}
|
||||
options
|
||||
.sandbox
|
||||
.apply_devcontainer_snapshot(devcontainer_to_snapshot_config(&config));
|
||||
|
||||
let timeout = std::time::Duration::from_millis(300_000);
|
||||
for command in &config.initialize_commands {
|
||||
|
|
@ -602,48 +380,6 @@ async fn resolve_devcontainer(options: &mut InitOptions) -> Result<(), FabroErro
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_sandbox_record(
|
||||
run_dir: &Path,
|
||||
spec: &SandboxSpec,
|
||||
sandbox: &Arc<dyn Sandbox>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let working_directory = sandbox.working_directory().to_string();
|
||||
let identifier = {
|
||||
let info = sandbox.sandbox_info();
|
||||
if info.is_empty() { None } else { Some(info) }
|
||||
};
|
||||
|
||||
let record = match spec {
|
||||
SandboxSpec::Docker { config } => SandboxRecord {
|
||||
provider: sandbox_provider_name(spec).to_string(),
|
||||
working_directory: working_directory.clone(),
|
||||
identifier,
|
||||
host_working_directory: Some(config.host_working_directory.clone()),
|
||||
container_mount_point: Some(working_directory),
|
||||
data_host: None,
|
||||
},
|
||||
SandboxSpec::Ssh { config, .. } => SandboxRecord {
|
||||
provider: sandbox_provider_name(spec).to_string(),
|
||||
working_directory,
|
||||
identifier,
|
||||
host_working_directory: None,
|
||||
container_mount_point: None,
|
||||
data_host: Some(config.destination.clone()),
|
||||
},
|
||||
_ => SandboxRecord {
|
||||
provider: sandbox_provider_name(spec).to_string(),
|
||||
working_directory,
|
||||
identifier,
|
||||
host_working_directory: None,
|
||||
container_mount_point: None,
|
||||
data_host: None,
|
||||
},
|
||||
};
|
||||
|
||||
record.save(&run_dir.join("sandbox.json"))
|
||||
}
|
||||
|
||||
/// INITIALIZE phase: prepare the sandbox, env, and handlers for execution.
|
||||
pub async fn initialize(
|
||||
persisted: Persisted,
|
||||
|
|
@ -670,17 +406,62 @@ pub async fn initialize(
|
|||
});
|
||||
}
|
||||
|
||||
let sandbox_result = build_sandbox(
|
||||
&options.sandbox,
|
||||
worktree_plan.as_ref(),
|
||||
Arc::clone(&options.emitter),
|
||||
)
|
||||
.await?;
|
||||
if worktree_plan.is_some() && !sandbox_result.worktree_created {
|
||||
let sandbox_event_callback: SandboxEventCallback = {
|
||||
let emitter = Arc::clone(&options.emitter);
|
||||
Arc::new(move |event| {
|
||||
emitter.emit(&WorkflowRunEvent::Sandbox { event });
|
||||
})
|
||||
};
|
||||
let mut worktree_created = false;
|
||||
let sandbox: Arc<dyn Sandbox> = if let Some(plan) = worktree_plan.as_ref() {
|
||||
let inner = options
|
||||
.sandbox
|
||||
.build(Some(Arc::clone(&sandbox_event_callback)))
|
||||
.await
|
||||
.map_err(|e| FabroError::engine(e.to_string()))?;
|
||||
let mut worktree = WorktreeSandbox::new(
|
||||
inner,
|
||||
WorktreeConfig {
|
||||
branch_name: plan.branch_name.clone(),
|
||||
base_sha: plan.base_sha.clone(),
|
||||
worktree_path: plan.worktree_path.to_string_lossy().into_owned(),
|
||||
skip_branch_creation: false,
|
||||
},
|
||||
);
|
||||
worktree.set_event_callback(Arc::clone(&options.emitter).worktree_callback());
|
||||
match worktree.initialize().await {
|
||||
Ok(()) => {
|
||||
worktree_created = true;
|
||||
Arc::new(ReadBeforeWriteSandbox::new(Arc::new(worktree)))
|
||||
}
|
||||
Err(e) => {
|
||||
emit_run_notice(
|
||||
&options.emitter,
|
||||
RunNoticeLevel::Warn,
|
||||
"worktree_setup_failed",
|
||||
format!("Git worktree setup failed ({e}), running without worktree."),
|
||||
);
|
||||
Arc::new(ReadBeforeWriteSandbox::new(
|
||||
options
|
||||
.sandbox
|
||||
.build(Some(Arc::clone(&sandbox_event_callback)))
|
||||
.await
|
||||
.map_err(|e| FabroError::engine(e.to_string()))?,
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Arc::new(ReadBeforeWriteSandbox::new(
|
||||
options
|
||||
.sandbox
|
||||
.build(Some(Arc::clone(&sandbox_event_callback)))
|
||||
.await
|
||||
.map_err(|e| FabroError::engine(e.to_string()))?,
|
||||
))
|
||||
};
|
||||
if worktree_plan.is_some() && !worktree_created {
|
||||
options.run_options.git = None;
|
||||
}
|
||||
|
||||
let sandbox = sandbox_result.sandbox;
|
||||
let cleanup_guard = scopeguard::guard(Arc::clone(&sandbox), |sandbox| {
|
||||
if let Ok(handle) = Handle::try_current() {
|
||||
handle.spawn(async move {
|
||||
|
|
@ -714,7 +495,11 @@ pub async fn initialize(
|
|||
options.emitter.emit(&WorkflowRunEvent::SandboxInitialized {
|
||||
working_directory: sandbox.working_directory().to_string(),
|
||||
});
|
||||
if let Err(e) = write_sandbox_record(&run_dir, &options.sandbox, &sandbox) {
|
||||
if let Err(e) = options
|
||||
.sandbox
|
||||
.to_sandbox_record(&*sandbox)
|
||||
.save(&run_dir.join("sandbox.json"))
|
||||
{
|
||||
tracing::warn!(error = %e, "Failed to save sandbox record");
|
||||
}
|
||||
|
||||
|
|
@ -868,6 +653,7 @@ mod tests {
|
|||
use fabro_config::FabroSettings;
|
||||
use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
|
||||
use fabro_interview::AutoApproveInterviewer;
|
||||
use fabro_sandbox::SandboxSpec;
|
||||
|
||||
use super::*;
|
||||
use crate::pipeline::types::InitOptions;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,6 @@ pub use transform::transform;
|
|||
pub use types::{
|
||||
Concluded, DevcontainerSpec, Executed, FinalizeOptions, Finalized, InitOptions, Initialized,
|
||||
LlmSpec, Parsed, Persisted, PullRequestOptions, RetroOptions, Retroed, SandboxEnvSpec,
|
||||
SandboxSpec, TransformOptions, Transformed, Validated,
|
||||
TransformOptions, Transformed, Validated,
|
||||
};
|
||||
pub use validate::validate;
|
||||
|
|
|
|||
|
|
@ -10,11 +10,7 @@ use fabro_interview::Interviewer;
|
|||
use fabro_llm::Provider;
|
||||
use fabro_mcp::config::McpServerConfig;
|
||||
use fabro_model::FallbackTarget;
|
||||
use fabro_sandbox::daytona::DaytonaConfig;
|
||||
use fabro_sandbox::docker::DockerSandboxConfig;
|
||||
#[cfg(feature = "exedev")]
|
||||
use fabro_sandbox::exe::{ExeConfig, GitCloneParams as ExeGitCloneParams};
|
||||
use fabro_sandbox::ssh::{GitCloneParams as SshGitCloneParams, SshConfig};
|
||||
use fabro_sandbox::SandboxSpec;
|
||||
use fabro_validate::Diagnostic;
|
||||
|
||||
use crate::context::Context;
|
||||
|
|
@ -202,36 +198,6 @@ impl Persisted {
|
|||
}
|
||||
}
|
||||
|
||||
/// Options for the INITIALIZE phase.
|
||||
pub enum SandboxSpec {
|
||||
Local {
|
||||
working_directory: PathBuf,
|
||||
},
|
||||
Docker {
|
||||
config: DockerSandboxConfig,
|
||||
},
|
||||
Daytona {
|
||||
config: DaytonaConfig,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
run_id: Option<String>,
|
||||
clone_branch: Option<String>,
|
||||
},
|
||||
#[cfg(feature = "exedev")]
|
||||
Exe {
|
||||
config: ExeConfig,
|
||||
clone_params: Option<ExeGitCloneParams>,
|
||||
run_id: Option<String>,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
mgmt_destination: String,
|
||||
},
|
||||
Ssh {
|
||||
config: SshConfig,
|
||||
clone_params: Option<SshGitCloneParams>,
|
||||
run_id: Option<String>,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LlmSpec {
|
||||
pub model: String,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue