mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
Close 3 exe.dev gaps: ExeConfig.image, run ID tracking, preflight config
Add image field to ExeConfig so users can specify a Docker image for exe.dev VMs via TOML config. Pass run_id to ExeSandbox for VM-to-run correlation in sandbox_info(). Wire resolved exe config into the preflight path for consistency with the main execution path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
740f95414a
commit
54de2aa651
4 changed files with 75 additions and 17 deletions
|
|
@ -53,7 +53,9 @@ pub trait SshRunner: Send + Sync {
|
|||
|
||||
/// Configuration for an exe.dev sandbox (TOML target for `[sandbox.exe]`).
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
|
||||
pub struct ExeConfig {}
|
||||
pub struct ExeConfig {
|
||||
pub image: Option<String>,
|
||||
}
|
||||
|
||||
/// Parameters for cloning a git repo into the sandbox during initialization.
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
@ -82,13 +84,20 @@ pub struct ExeSandbox {
|
|||
/// In production, this connects to the VM host via OpensshRunner.
|
||||
/// In tests, this is replaced with a closure that returns a MockSshRunner.
|
||||
data_ssh_factory: DataSshFactory,
|
||||
config: ExeConfig,
|
||||
clone_params: Option<GitCloneParams>,
|
||||
run_id: Option<String>,
|
||||
origin_url: tokio::sync::OnceCell<String>,
|
||||
}
|
||||
|
||||
impl ExeSandbox {
|
||||
/// Creates a new `ExeSandbox` with a management-plane SSH runner.
|
||||
pub fn new(mgmt_ssh: Box<dyn SshRunner>, clone_params: Option<GitCloneParams>) -> Self {
|
||||
pub fn new(
|
||||
mgmt_ssh: Box<dyn SshRunner>,
|
||||
config: ExeConfig,
|
||||
clone_params: Option<GitCloneParams>,
|
||||
run_id: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
mgmt_ssh,
|
||||
data_ssh: tokio::sync::OnceCell::new(),
|
||||
|
|
@ -104,7 +113,9 @@ impl ExeSandbox {
|
|||
.map(|r| Box::new(r) as Box<dyn SshRunner>)
|
||||
})
|
||||
}),
|
||||
config,
|
||||
clone_params,
|
||||
run_id,
|
||||
origin_url: tokio::sync::OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
|
@ -270,7 +281,11 @@ impl Sandbox for ExeSandbox {
|
|||
let init_start = Instant::now();
|
||||
|
||||
// Create a new VM via the management plane
|
||||
let output = self.mgmt_ssh.run_command("new --json").await.map_err(|e| {
|
||||
let mut cmd = "new --json".to_string();
|
||||
if let Some(ref image) = self.config.image {
|
||||
cmd.push_str(&format!(" --image {image}"));
|
||||
}
|
||||
let output = self.mgmt_ssh.run_command(&cmd).await.map_err(|e| {
|
||||
let err = format!("Failed to create exe.dev VM: {e}");
|
||||
let duration_ms = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
self.emit(SandboxEvent::InitializeFailed {
|
||||
|
|
@ -696,7 +711,11 @@ impl Sandbox for ExeSandbox {
|
|||
}
|
||||
|
||||
fn sandbox_info(&self) -> String {
|
||||
self.vm_name.get().cloned().unwrap_or_default()
|
||||
match (self.vm_name.get(), &self.run_id) {
|
||||
(Some(name), Some(id)) => format!("{name} (run {id})"),
|
||||
(Some(name), None) => name.clone(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -851,7 +870,7 @@ mod tests {
|
|||
/// Helper: create an ExeSandbox with mock data SSH already initialized (skipping lifecycle).
|
||||
fn sandbox_with_mock_data(data_ssh: impl SshRunner + 'static) -> ExeSandbox {
|
||||
let mgmt = MockSshRunner::new();
|
||||
let sandbox = ExeSandbox::new(Box::new(mgmt), None);
|
||||
let sandbox = ExeSandbox::new(Box::new(mgmt), ExeConfig::default(), None, None);
|
||||
let _ = sandbox.vm_name.set("test-vm".to_string());
|
||||
let _ = sandbox.data_host.set("test-vm.exe.xyz".to_string());
|
||||
let _ = sandbox.data_ssh.set(Box::new(data_ssh));
|
||||
|
|
@ -895,7 +914,7 @@ mod tests {
|
|||
#[test]
|
||||
fn ssh_command_errors_before_init() {
|
||||
let mgmt = MockSshRunner::new();
|
||||
let sandbox = ExeSandbox::new(Box::new(mgmt), None);
|
||||
let sandbox = ExeSandbox::new(Box::new(mgmt), ExeConfig::default(), None, None);
|
||||
assert!(sandbox.ssh_command().is_err());
|
||||
}
|
||||
|
||||
|
|
@ -1271,7 +1290,7 @@ mod tests {
|
|||
|
||||
let data_for_init = MockSshRunner::new();
|
||||
|
||||
let mut sandbox = ExeSandbox::new(Box::new(mgmt), None);
|
||||
let mut sandbox = ExeSandbox::new(Box::new(mgmt), ExeConfig::default(), None, None);
|
||||
// Override factory to return our mock data SSH
|
||||
let data_box: Arc<Mutex<Option<Box<dyn SshRunner>>>> =
|
||||
Arc::new(Mutex::new(Some(Box::new(data_for_init))));
|
||||
|
|
@ -1303,7 +1322,7 @@ mod tests {
|
|||
let events: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
let events_cb = Arc::clone(&events);
|
||||
|
||||
let mut sandbox = ExeSandbox::new(Box::new(mgmt), None);
|
||||
let mut sandbox = ExeSandbox::new(Box::new(mgmt), ExeConfig::default(), None, None);
|
||||
sandbox.set_event_callback(Arc::new(move |event| {
|
||||
events_cb.lock().unwrap().push(format!("{event:?}"));
|
||||
}));
|
||||
|
|
@ -1342,7 +1361,7 @@ mod tests {
|
|||
// Response for `rm <vm_name>`
|
||||
mgmt.queue_response("", "", 0);
|
||||
|
||||
let sandbox = ExeSandbox::new(Box::new(mgmt), None);
|
||||
let sandbox = ExeSandbox::new(Box::new(mgmt), ExeConfig::default(), None, None);
|
||||
let _ = sandbox.vm_name.set("doomed-vm".to_string());
|
||||
|
||||
sandbox.cleanup().await.unwrap();
|
||||
|
|
@ -1354,7 +1373,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn cleanup_before_initialize_is_noop() {
|
||||
let mgmt = MockSshRunner::new();
|
||||
let sandbox = ExeSandbox::new(Box::new(mgmt), None);
|
||||
let sandbox = ExeSandbox::new(Box::new(mgmt), ExeConfig::default(), None, None);
|
||||
// Should not error — no VM to destroy
|
||||
sandbox.cleanup().await.unwrap();
|
||||
}
|
||||
|
|
@ -1385,7 +1404,12 @@ mod tests {
|
|||
display_url: "https://github.com/org/repo.git".to_string(),
|
||||
branch: Some("main".to_string()),
|
||||
};
|
||||
let mut sandbox = ExeSandbox::new(Box::new(mgmt), Some(clone_params));
|
||||
let mut sandbox = ExeSandbox::new(
|
||||
Box::new(mgmt),
|
||||
ExeConfig::default(),
|
||||
Some(clone_params),
|
||||
None,
|
||||
);
|
||||
sandbox.data_ssh_factory = Box::new(move |_host: &str| {
|
||||
let data_box = Arc::clone(&data_box);
|
||||
Box::pin(async move {
|
||||
|
|
@ -1430,7 +1454,7 @@ mod tests {
|
|||
let data_box: Arc<Mutex<Option<Box<dyn SshRunner>>>> =
|
||||
Arc::new(Mutex::new(Some(Box::new(data))));
|
||||
|
||||
let mut sandbox = ExeSandbox::new(Box::new(mgmt), None);
|
||||
let mut sandbox = ExeSandbox::new(Box::new(mgmt), ExeConfig::default(), None, None);
|
||||
sandbox.data_ssh_factory = Box::new(move |_host: &str| {
|
||||
let data_box = Arc::clone(&data_box);
|
||||
Box::pin(async move {
|
||||
|
|
@ -1476,7 +1500,12 @@ mod tests {
|
|||
display_url: "https://github.com/org/repo.git".to_string(),
|
||||
branch: None,
|
||||
};
|
||||
let mut sandbox = ExeSandbox::new(Box::new(mgmt), Some(clone_params));
|
||||
let mut sandbox = ExeSandbox::new(
|
||||
Box::new(mgmt),
|
||||
ExeConfig::default(),
|
||||
Some(clone_params),
|
||||
None,
|
||||
);
|
||||
sandbox.set_event_callback(Arc::new(move |event| {
|
||||
events_cb.lock().unwrap().push(format!("{event:?}"));
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use arc_agent::sandbox::Sandbox;
|
||||
use arc_exe::{ExeSandbox, OpensshRunner};
|
||||
use arc_exe::{ExeConfig, ExeSandbox, OpensshRunner};
|
||||
|
||||
/// Full lifecycle test against a real exe.dev account.
|
||||
/// Requires SSH agent with exe.dev credentials.
|
||||
|
|
@ -12,7 +12,7 @@ async fn exe_sandbox_full_lifecycle() {
|
|||
.await
|
||||
.expect("SSH to exe.dev failed — is your SSH agent running?");
|
||||
|
||||
let sandbox = ExeSandbox::new(Box::new(mgmt_ssh), None);
|
||||
let sandbox = ExeSandbox::new(Box::new(mgmt_ssh), ExeConfig::default(), None, None);
|
||||
|
||||
// Initialize (creates VM)
|
||||
sandbox.initialize().await.unwrap();
|
||||
|
|
|
|||
|
|
@ -175,6 +175,17 @@ fn resolve_daytona_config(
|
|||
})
|
||||
}
|
||||
|
||||
/// Resolve exe.dev config: TOML config > run defaults.
|
||||
fn resolve_exe_config(
|
||||
run_cfg: Option<&WorkflowRunConfig>,
|
||||
run_defaults: &RunDefaults,
|
||||
) -> Option<arc_exe::ExeConfig> {
|
||||
run_cfg
|
||||
.and_then(|c| c.sandbox.as_ref())
|
||||
.and_then(|e| e.exe.clone())
|
||||
.or_else(|| run_defaults.sandbox.as_ref().and_then(|s| s.exe.clone()))
|
||||
}
|
||||
|
||||
/// Resolve the fallback chain from config.
|
||||
///
|
||||
/// `apply_defaults` must be called on `run_cfg` before this — it merges
|
||||
|
|
@ -480,6 +491,7 @@ pub async fn run_command(
|
|||
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
let daytona_config = resolve_daytona_config(run_cfg.as_ref(), &run_defaults);
|
||||
let exe_config = resolve_exe_config(run_cfg.as_ref(), &run_defaults);
|
||||
|
||||
// Wrap emitter in Arc now so we can share it with exec env callbacks
|
||||
let emitter = Arc::new(emitter);
|
||||
|
|
@ -564,7 +576,13 @@ pub async fn run_command(
|
|||
let mgmt_ssh = arc_exe::OpensshRunner::connect_raw("exe.dev")
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to connect to exe.dev: {e}"))?;
|
||||
let mut env = arc_exe::ExeSandbox::new(Box::new(mgmt_ssh), clone_params);
|
||||
let config = exe_config.unwrap_or_default();
|
||||
let mut env = arc_exe::ExeSandbox::new(
|
||||
Box::new(mgmt_ssh),
|
||||
config,
|
||||
clone_params,
|
||||
Some(run_id.clone()),
|
||||
);
|
||||
let emitter_cb = Arc::clone(&emitter);
|
||||
env.set_event_callback(Arc::new(move |event| {
|
||||
emitter_cb.emit(&crate::event::WorkflowRunEvent::Sandbox { event });
|
||||
|
|
@ -1438,6 +1456,7 @@ async fn run_preflight(
|
|||
// 2. Sandbox boot check
|
||||
let original_cwd = std::env::current_dir()?;
|
||||
let daytona_config = resolve_daytona_config(run_cfg.as_ref(), run_defaults);
|
||||
let exe_config = resolve_exe_config(run_cfg.as_ref(), run_defaults);
|
||||
|
||||
let sandbox_result: Result<Arc<dyn Sandbox>, String> = match sandbox_provider {
|
||||
SandboxProvider::Docker => {
|
||||
|
|
@ -1464,7 +1483,8 @@ async fn run_preflight(
|
|||
},
|
||||
SandboxProvider::Exe => match arc_exe::OpensshRunner::connect_raw("exe.dev").await {
|
||||
Ok(mgmt_ssh) => {
|
||||
let env = arc_exe::ExeSandbox::new(Box::new(mgmt_ssh), None);
|
||||
let config = exe_config.unwrap_or_default();
|
||||
let env = arc_exe::ExeSandbox::new(Box::new(mgmt_ssh), config, None, None);
|
||||
Ok(Arc::new(env) as Arc<dyn Sandbox>)
|
||||
}
|
||||
Err(e) => Err(format!("exe.dev SSH connection failed: {e}")),
|
||||
|
|
|
|||
|
|
@ -152,6 +152,15 @@ impl WorkflowRunConfig {
|
|||
(None, Some(_)) => task.daytona = default.daytona.clone(),
|
||||
_ => {}
|
||||
}
|
||||
match (&mut task.exe, &default.exe) {
|
||||
(Some(task_e), Some(default_e)) => {
|
||||
if task_e.image.is_none() {
|
||||
task_e.image = default_e.image.clone();
|
||||
}
|
||||
}
|
||||
(None, Some(_)) => task.exe = default.exe.clone(),
|
||||
_ => {}
|
||||
}
|
||||
if let Some(ref default_env) = default.env {
|
||||
let mut merged = default_env.clone();
|
||||
if let Some(ref task_env) = task.env {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue