Close 3 exe.dev gaps: base64 command wrapping, --ssh flag, encapsulated git clone

- Replace fragile shell escaping in exec_command with base64 encoding
  (matching Daytona's wrap_bash_command pattern)
- Add ssh_command() to ExeSandbox and wire --ssh flag support in run.rs
- Move git cloning into ExeSandbox::initialize() via GitCloneParams,
  removing clone_repo_into_exe() from run.rs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-09 08:51:44 -04:00
parent 0e73071928
commit 381d90d8b1
3 changed files with 399 additions and 179 deletions

View file

@ -9,6 +9,7 @@ use arc_agent::sandbox::{
SandboxEventCallback,
};
use async_trait::async_trait;
use base64::Engine;
use serde::{Deserialize, Serialize};
use tokio_util::sync::CancellationToken;
@ -54,6 +55,17 @@ pub trait SshRunner: Send + Sync {
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct ExeConfig {}
/// Parameters for cloning a git repo into the sandbox during initialization.
#[derive(Clone, Debug)]
pub struct GitCloneParams {
/// HTTPS URL with embedded token (or without for public repos).
pub clone_url: String,
/// Clean URL for events/logging (no embedded credentials).
pub display_url: String,
/// Branch to clone. If None, uses the remote's default.
pub branch: Option<String>,
}
/// Sandbox that runs all operations inside an exe.dev VM via SSH.
///
/// Uses two SSH connections:
@ -70,11 +82,13 @@ 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,
clone_params: Option<GitCloneParams>,
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>) -> Self {
pub fn new(mgmt_ssh: Box<dyn SshRunner>, clone_params: Option<GitCloneParams>) -> Self {
Self {
mgmt_ssh,
data_ssh: tokio::sync::OnceCell::new(),
@ -90,9 +104,16 @@ impl ExeSandbox {
.map(|r| Box::new(r) as Box<dyn SshRunner>)
})
}),
clone_params,
origin_url: tokio::sync::OnceCell::new(),
}
}
/// The display URL of the cloned origin remote, if a clone was performed.
pub fn origin_url(&self) -> Option<&str> {
self.origin_url.get().map(String::as_str)
}
pub fn set_event_callback(&mut self, cb: SandboxEventCallback) {
self.event_callback = Some(cb);
}
@ -112,6 +133,124 @@ impl ExeSandbox {
.ok_or_else(|| "Exe sandbox not initialized — call initialize() first".to_string())
}
/// Return the SSH command to connect to this VM's data host.
pub fn ssh_command(&self) -> Result<String, String> {
let host = self.data_host.get().ok_or("Exe sandbox not initialized")?;
Ok(format!("ssh {host}"))
}
/// Wrap a shell command in base64 encoding to avoid escaping issues.
fn wrap_bash_command(command: &str) -> String {
let encoded = base64::engine::general_purpose::STANDARD.encode(command);
format!("echo '{encoded}' | base64 -d | sh")
}
/// Clone a git repo into the sandbox working directory.
async fn clone_repo(&self, params: &GitCloneParams) -> Result<(), String> {
let ssh = self.data_ssh()?;
self.emit(SandboxEvent::GitCloneStarted {
url: params.display_url.clone(),
branch: params.branch.clone(),
});
let clone_start = Instant::now();
let branch_flag = params
.branch
.as_deref()
.map(|b| format!(" --branch {b}"))
.unwrap_or_default();
let clone_script = format!(
"git clone{branch_flag} {} {WORKING_DIRECTORY}",
params.clone_url
);
let clone_cmd = Self::wrap_bash_command(&clone_script);
let clone_timeout = std::time::Duration::from_secs(300);
let clone_output = ssh
.run_command_with_timeout(&clone_cmd, clone_timeout)
.await
.map_err(|e| {
let err = format!("git clone failed: {e}");
self.emit(SandboxEvent::GitCloneFailed {
url: params.display_url.clone(),
error: err.clone(),
});
err
})?;
if clone_output.exit_code != 0 {
let stderr = String::from_utf8_lossy(&clone_output.stderr);
// Fall back to init + fetch + checkout if directory is not empty
if stderr.contains("not an empty directory")
|| stderr.contains("already exists and is not an empty")
{
let branch = params.branch.as_deref().unwrap_or("main");
let fallback_script = format!(
"cd {WORKING_DIRECTORY} && git init && git remote add origin {} && git fetch origin && git checkout {branch}",
params.clone_url,
);
let fallback_cmd = Self::wrap_bash_command(&fallback_script);
let fallback_output = ssh
.run_command_with_timeout(&fallback_cmd, clone_timeout)
.await
.map_err(|e| {
let err = format!("git fallback clone failed: {e}");
self.emit(SandboxEvent::GitCloneFailed {
url: params.display_url.clone(),
error: err.clone(),
});
err
})?;
if fallback_output.exit_code != 0 {
let fallback_stderr = String::from_utf8_lossy(&fallback_output.stderr);
let err = format!(
"git fallback clone failed (exit {}): {fallback_stderr}",
fallback_output.exit_code,
);
self.emit(SandboxEvent::GitCloneFailed {
url: params.display_url.clone(),
error: err.clone(),
});
return Err(err);
}
} else {
let err = format!(
"git clone failed (exit {}): {stderr}",
clone_output.exit_code,
);
self.emit(SandboxEvent::GitCloneFailed {
url: params.display_url.clone(),
error: err.clone(),
});
return Err(err);
}
}
// Set remote URL with auth credentials
let set_url_script = format!(
"cd {WORKING_DIRECTORY} && git remote set-url origin {}",
params.clone_url,
);
let set_url_cmd = Self::wrap_bash_command(&set_url_script);
let _ = ssh
.run_command_with_timeout(&set_url_cmd, std::time::Duration::from_secs(10))
.await;
// Store the display URL
let _ = self.origin_url.set(params.display_url.clone());
let duration_ms = u64::try_from(clone_start.elapsed().as_millis()).unwrap_or(u64::MAX);
self.emit(SandboxEvent::GitCloneCompleted {
url: params.display_url.clone(),
duration_ms,
});
Ok(())
}
/// Resolve a path: relative paths are prepended with the working directory.
fn resolve_path(&self, path: &str) -> String {
if Path::new(path).is_absolute() {
@ -201,6 +340,11 @@ impl Sandbox for ExeSandbox {
.set(runner)
.map_err(|_| "Exe sandbox data SSH already set".to_string())?;
// Clone git repo if clone params were provided
if let Some(ref params) = self.clone_params {
self.clone_repo(params).await?;
}
let init_duration = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX);
self.emit(SandboxEvent::Ready {
provider: PROVIDER.into(),
@ -251,26 +395,22 @@ impl Sandbox for ExeSandbox {
let ssh = self.data_ssh()?;
let start = Instant::now();
// Build the shell command with optional cd and env vars
let mut parts = Vec::new();
// Build inner script as plain text, then base64-wrap for safe transport
let mut script = String::new();
if let Some(vars) = env_vars {
for (key, value) in vars {
parts.push(format!("{}='{}'", key, value.replace('\'', "'\\''")));
script.push_str(&format!("export {key}={value}\n"));
}
}
if let Some(dir) = working_dir {
let resolved = self.resolve_path(dir);
parts.push(format!("cd '{}'", resolved.replace('\'', "'\\''")));
parts.push("&&".to_string());
} else {
parts.push(format!("cd '{WORKING_DIRECTORY}'"));
parts.push("&&".to_string());
}
let dir = match working_dir {
Some(dir) => self.resolve_path(dir),
None => WORKING_DIRECTORY.to_string(),
};
script.push_str(&format!("cd {dir} && {command}"));
parts.push(command.to_string());
let full_cmd = parts.join(" ");
let full_cmd = Self::wrap_bash_command(&script);
let timeout = std::time::Duration::from_millis(timeout_ms);
let token = cancel_token.unwrap_or_default();
@ -696,10 +836,22 @@ mod tests {
}
}
/// Extract and decode the inner command from a base64-wrapped SSH command.
/// The format is: echo '<base64>' | base64 -d | sh
fn decode_bash_payload(wrapped: &str) -> String {
let start = wrapped.find("echo '").expect("missing echo prefix") + 6;
let end = wrapped[start..].find('\'').expect("missing closing quote") + start;
let encoded = &wrapped[start..end];
let bytes = base64::engine::general_purpose::STANDARD
.decode(encoded)
.expect("invalid base64");
String::from_utf8(bytes).expect("invalid utf8")
}
/// 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));
let sandbox = ExeSandbox::new(Box::new(mgmt), 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));
@ -732,6 +884,21 @@ mod tests {
assert_eq!(sandbox.os_version(), "Linux (exe.dev)");
}
// ---- ssh_command ----
#[test]
fn ssh_command_returns_host_after_init() {
let sandbox = sandbox_with_mock_data(MockSshRunner::new());
assert_eq!(sandbox.ssh_command().unwrap(), "ssh test-vm.exe.xyz");
}
#[test]
fn ssh_command_errors_before_init() {
let mgmt = MockSshRunner::new();
let sandbox = ExeSandbox::new(Box::new(mgmt), None);
assert!(sandbox.ssh_command().is_err());
}
// ---- Step 2: exec_command ----
#[tokio::test]
@ -763,10 +930,10 @@ mod tests {
.unwrap();
let recorded = commands.lock().unwrap();
let inner = decode_bash_payload(&recorded[0].command);
assert!(
recorded[0].command.contains("cd '/tmp/work'"),
"expected cd to working dir, got: {}",
recorded[0].command,
inner.contains("cd /tmp/work"),
"expected cd to working dir, got: {inner}",
);
}
@ -786,10 +953,10 @@ mod tests {
.unwrap();
let recorded = commands.lock().unwrap();
let inner = decode_bash_payload(&recorded[0].command);
assert!(
recorded[0].command.contains("FOO='bar'"),
"expected env var, got: {}",
recorded[0].command,
inner.contains("export FOO=bar"),
"expected env var export, got: {inner}",
);
}
@ -1104,7 +1271,7 @@ mod tests {
let data_for_init = MockSshRunner::new();
let mut sandbox = ExeSandbox::new(Box::new(mgmt));
let mut sandbox = ExeSandbox::new(Box::new(mgmt), 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))));
@ -1136,7 +1303,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));
let mut sandbox = ExeSandbox::new(Box::new(mgmt), None);
sandbox.set_event_callback(Arc::new(move |event| {
events_cb.lock().unwrap().push(format!("{event:?}"));
}));
@ -1175,7 +1342,7 @@ mod tests {
// Response for `rm <vm_name>`
mgmt.queue_response("", "", 0);
let sandbox = ExeSandbox::new(Box::new(mgmt));
let sandbox = ExeSandbox::new(Box::new(mgmt), None);
let _ = sandbox.vm_name.set("doomed-vm".to_string());
sandbox.cleanup().await.unwrap();
@ -1187,8 +1354,150 @@ mod tests {
#[tokio::test]
async fn cleanup_before_initialize_is_noop() {
let mgmt = MockSshRunner::new();
let sandbox = ExeSandbox::new(Box::new(mgmt));
let sandbox = ExeSandbox::new(Box::new(mgmt), None);
// Should not error — no VM to destroy
sandbox.cleanup().await.unwrap();
}
// ---- clone_repo ----
#[tokio::test]
async fn initialize_with_clone_params_clones_repo() {
let mgmt = MockSshRunner::new();
mgmt.queue_response(
r#"{"vm_name": "clone-vm", "ssh_dest": "clone-vm.exe.xyz"}"#,
"",
0,
);
let data = MockSshRunner::new();
let data_commands = data.commands.clone();
// Response for git clone
data.queue_response("", "", 0);
// Response for git remote set-url
data.queue_response("", "", 0);
let data_box: Arc<Mutex<Option<Box<dyn SshRunner>>>> =
Arc::new(Mutex::new(Some(Box::new(data))));
let clone_params = GitCloneParams {
clone_url: "https://x-access-token:tok@github.com/org/repo.git".to_string(),
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));
sandbox.data_ssh_factory = Box::new(move |_host: &str| {
let data_box = Arc::clone(&data_box);
Box::pin(async move {
data_box
.lock()
.unwrap()
.take()
.ok_or_else(|| "mock data SSH already taken".to_string())
})
});
sandbox.initialize().await.unwrap();
let recorded = data_commands.lock().unwrap();
let clone_inner = decode_bash_payload(&recorded[0].command);
assert!(
clone_inner.contains("git clone"),
"expected git clone, got: {clone_inner}",
);
assert!(
clone_inner.contains("--branch main"),
"expected branch flag, got: {clone_inner}",
);
assert_eq!(
sandbox.origin_url(),
Some("https://github.com/org/repo.git"),
);
}
#[tokio::test]
async fn initialize_without_clone_params_skips_clone() {
let mgmt = MockSshRunner::new();
mgmt.queue_response(
r#"{"vm_name": "no-clone-vm", "ssh_dest": "no-clone-vm.exe.xyz"}"#,
"",
0,
);
let data = MockSshRunner::new();
let data_commands = data.commands.clone();
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);
sandbox.data_ssh_factory = Box::new(move |_host: &str| {
let data_box = Arc::clone(&data_box);
Box::pin(async move {
data_box
.lock()
.unwrap()
.take()
.ok_or_else(|| "mock data SSH already taken".to_string())
})
});
sandbox.initialize().await.unwrap();
let recorded = data_commands.lock().unwrap();
assert!(
recorded.is_empty(),
"expected no data SSH commands without clone params, got: {recorded:?}",
);
assert!(sandbox.origin_url().is_none());
}
#[tokio::test]
async fn initialize_clone_failure_emits_event_and_errors() {
let mgmt = MockSshRunner::new();
mgmt.queue_response(
r#"{"vm_name": "fail-vm", "ssh_dest": "fail-vm.exe.xyz"}"#,
"",
0,
);
let data = MockSshRunner::new();
// git clone fails
data.queue_response("", "auth failed", 128);
let data_box: Arc<Mutex<Option<Box<dyn SshRunner>>>> =
Arc::new(Mutex::new(Some(Box::new(data))));
let events: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let events_cb = Arc::clone(&events);
let clone_params = GitCloneParams {
clone_url: "https://github.com/org/repo.git".to_string(),
display_url: "https://github.com/org/repo.git".to_string(),
branch: None,
};
let mut sandbox = ExeSandbox::new(Box::new(mgmt), Some(clone_params));
sandbox.set_event_callback(Arc::new(move |event| {
events_cb.lock().unwrap().push(format!("{event:?}"));
}));
sandbox.data_ssh_factory = Box::new(move |_host: &str| {
let data_box = Arc::clone(&data_box);
Box::pin(async move {
data_box
.lock()
.unwrap()
.take()
.ok_or_else(|| "mock data SSH already taken".to_string())
})
});
let result = sandbox.initialize().await;
assert!(result.is_err());
let captured = events.lock().unwrap();
assert!(
captured.iter().any(|e| e.contains("GitCloneFailed")),
"expected GitCloneFailed event, got: {captured:?}",
);
}
}

View file

@ -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));
let sandbox = ExeSandbox::new(Box::new(mgmt_ssh), None);
// Initialize (creates VM)
sandbox.initialize().await.unwrap();

View file

@ -484,6 +484,7 @@ pub async fn run_command(
let emitter = Arc::new(emitter);
let mut daytona_sandbox_ref: Option<Arc<crate::daytona_sandbox::DaytonaSandbox>> = None;
let mut exe_sandbox_ref: Option<Arc<arc_exe::ExeSandbox>> = None;
let sandbox: Arc<dyn Sandbox> = match sandbox_provider {
SandboxProvider::Docker => {
let config = DockerSandboxConfig {
@ -518,15 +519,58 @@ pub async fn run_command(
daytona_arc
}
SandboxProvider::Exe => {
// Resolve git clone params before creating the sandbox
let clone_params = match crate::daytona_sandbox::detect_repo_info(&original_cwd) {
Ok((detected_url, branch)) => {
let display_url = crate::github_app::ssh_url_to_https(&detected_url);
let token = match &github_app {
Some(creds) => {
let (owner, repo) = crate::github_app::parse_github_owner_repo(
&display_url,
)
.map_err(|e| {
anyhow::anyhow!("Failed to parse GitHub URL for clone: {e}")
})?;
let (_username, password) =
crate::github_app::resolve_clone_credentials(creds, &owner, &repo)
.await
.map_err(|e| {
anyhow::anyhow!(
"Failed to get GitHub App credentials for clone: {e}"
)
})?;
password
}
None => None,
};
let clone_url = match &token {
Some(t) => display_url.replacen(
"https://",
&format!("https://x-access-token:{t}@"),
1,
),
None => display_url.clone(),
};
Some(arc_exe::GitCloneParams {
clone_url,
display_url,
branch,
})
}
Err(_) => None,
};
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));
let mut env = arc_exe::ExeSandbox::new(Box::new(mgmt_ssh), clone_params);
let emitter_cb = Arc::clone(&emitter);
env.set_event_callback(Arc::new(move |event| {
emitter_cb.emit(&crate::event::WorkflowRunEvent::Sandbox { event });
}));
Arc::new(env) as Arc<dyn Sandbox>
let exe_arc = Arc::new(env);
exe_sandbox_ref = Some(Arc::clone(&exe_arc));
exe_arc
}
SandboxProvider::Local => {
let mut env = LocalSandbox::new(cwd);
@ -544,18 +588,14 @@ pub async fn run_command(
.await
.map_err(|e| anyhow::anyhow!("Failed to initialize sandbox: {e}"))?;
// Clone repo into exe.dev VM after initialization
let exe_origin_url = if sandbox_provider == SandboxProvider::Exe {
clone_repo_into_exe(&*sandbox, &emitter, &original_cwd, &github_app).await?
} else {
None
};
// Wrap exe.dev sandbox with GitCredentialSandbox for push credential refresh
let sandbox: Arc<dyn Sandbox> = if sandbox_provider == SandboxProvider::Exe {
let origin_url = exe_sandbox_ref
.as_ref()
.and_then(|e| e.origin_url().map(String::from));
Arc::new(crate::git_credential_sandbox::GitCredentialSandbox::new(
sandbox,
exe_origin_url,
origin_url,
github_app.clone(),
))
} else {
@ -612,9 +652,21 @@ pub async fn run_command(
);
}
}
} else if let Some(ref exe) = exe_sandbox_ref {
match exe.ssh_command() {
Ok(ssh_command) => {
emitter.emit(&crate::event::WorkflowRunEvent::SshAccessReady { ssh_command });
}
Err(e) => {
eprintln!(
"{} Failed to get exe.dev SSH command: {e}",
styles.yellow.apply_to("Warning:"),
);
}
}
} else {
eprintln!(
"{} --ssh only works with --sandbox daytona, skipping.",
"{} --ssh only works with --sandbox daytona or exe, skipping.",
styles.yellow.apply_to("Warning:"),
);
}
@ -1022,147 +1074,6 @@ fn setup_worktree(
Ok((worktree_path.clone(), worktree_path, branch_name, base_sha))
}
/// Clone the local git repo into an exe.dev VM.
/// Returns the HTTPS origin URL on success, or None if no git repo was detected.
async fn clone_repo_into_exe(
sandbox: &dyn arc_agent::Sandbox,
emitter: &std::sync::Arc<crate::event::EventEmitter>,
cwd: &std::path::Path,
github_app: &Option<crate::github_app::GitHubAppCredentials>,
) -> anyhow::Result<Option<String>> {
use arc_agent::sandbox::SandboxEvent;
let (detected_url, branch) = match crate::daytona_sandbox::detect_repo_info(cwd) {
Ok(info) => info,
Err(_) => return Ok(None), // no git repo — continue without cloning
};
let url = crate::github_app::ssh_url_to_https(&detected_url);
emitter.emit(&crate::event::WorkflowRunEvent::Sandbox {
event: SandboxEvent::GitCloneStarted {
url: url.clone(),
branch: branch.clone(),
},
});
let clone_start = std::time::Instant::now();
// Resolve clone credentials via GitHub App or fall back to no auth
let token = match github_app {
Some(creds) => {
let (owner, repo) = crate::github_app::parse_github_owner_repo(&url).map_err(|e| {
emitter.emit(&crate::event::WorkflowRunEvent::Sandbox {
event: SandboxEvent::GitCloneFailed {
url: url.clone(),
error: e.clone(),
},
});
anyhow::anyhow!("Failed to parse GitHub URL for clone: {e}")
})?;
let (_username, password) =
crate::github_app::resolve_clone_credentials(creds, &owner, &repo)
.await
.map_err(|e| {
emitter.emit(&crate::event::WorkflowRunEvent::Sandbox {
event: SandboxEvent::GitCloneFailed {
url: url.clone(),
error: e.clone(),
},
});
anyhow::anyhow!("Failed to get GitHub App credentials for clone: {e}")
})?;
password
}
None => None,
};
let auth_url = match &token {
Some(t) => url.replacen("https://", &format!("https://x-access-token:{t}@"), 1),
None => url.clone(),
};
let working_dir = sandbox.working_directory();
let branch_flag = branch
.as_deref()
.map(|b| format!(" --branch '{}'", b.replace('\'', "'\\''")))
.unwrap_or_default();
// Try cloning directly into the working directory
let clone_cmd = format!(
"git clone{branch_flag} '{}' '{working_dir}'",
auth_url.replace('\'', "'\\''"),
);
let clone_result = sandbox
.exec_command(&clone_cmd, 300_000, Some("/tmp"), None, None)
.await
.map_err(|e| anyhow::anyhow!("git clone failed: {e}"))?;
if clone_result.exit_code != 0 {
// Fall back to init + fetch + checkout if directory is not empty
if clone_result.stderr.contains("not an empty directory")
|| clone_result
.stderr
.contains("already exists and is not an empty")
{
let init_cmds = [
"git init",
&format!(
"git remote add origin '{}'",
auth_url.replace('\'', "'\\''")
),
"git fetch origin",
&format!("git checkout {}", branch.as_deref().unwrap_or("main")),
];
for cmd in &init_cmds {
let r = sandbox
.exec_command(cmd, 300_000, None, None, None)
.await
.map_err(|e| anyhow::anyhow!("git fallback command failed: {e}"))?;
if r.exit_code != 0 {
let err = format!("git fallback failed on '{cmd}': {}", r.stderr);
emitter.emit(&crate::event::WorkflowRunEvent::Sandbox {
event: SandboxEvent::GitCloneFailed {
url: url.clone(),
error: err.clone(),
},
});
anyhow::bail!("{err}");
}
}
} else {
let err = format!(
"git clone failed (exit {}): {}",
clone_result.exit_code, clone_result.stderr
);
emitter.emit(&crate::event::WorkflowRunEvent::Sandbox {
event: SandboxEvent::GitCloneFailed {
url: url.clone(),
error: err.clone(),
},
});
anyhow::bail!("{err}");
}
}
// Set clean push credentials on the remote
let set_url_cmd = format!(
"git remote set-url origin '{}'",
auth_url.replace('\'', "'\\''"),
);
let _ = sandbox
.exec_command(&set_url_cmd, 10_000, None, None, None)
.await;
let duration_ms = u64::try_from(clone_start.elapsed().as_millis()).unwrap_or(u64::MAX);
emitter.emit(&crate::event::WorkflowRunEvent::Sandbox {
event: SandboxEvent::GitCloneCompleted {
url: url.clone(),
duration_ms,
},
});
Ok(Some(url))
}
/// Set up git inside a remote sandbox (Daytona or exe.dev) for checkpoint commits.
/// Returns (base_sha, branch_name, base_branch) on success.
async fn setup_remote_git(
@ -1552,7 +1463,7 @@ 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));
let env = arc_exe::ExeSandbox::new(Box::new(mgmt_ssh), None);
Ok(Arc::new(env) as Arc<dyn Sandbox>)
}
Err(e) => Err(format!("exe.dev SSH connection failed: {e}")),