fabro(01KM3AY9Z9FWFBDH954H4W16SG): implement (success)

Fabro-Run: 01KM3AY9Z9FWFBDH954H4W16SG
Fabro-Completed: 5
Fabro-Checkpoint: ced487d505

⚒️ Generated with [Fabro](https://fabro.sh)
This commit is contained in:
Fabro 2026-03-19 15:27:52 +00:00
parent c81dfe79c9
commit 007488d01d
3 changed files with 186 additions and 179 deletions

View file

@ -1066,11 +1066,17 @@ pub async fn run_command(
}
SandboxProvider::Daytona => {
let config = daytona_config.clone().unwrap_or_default();
let clone_params = origin_url
.as_ref()
.map(|url| fabro_daytona::GitCloneParams {
url: fabro_github::ssh_url_to_https(url),
branch: detected_base_branch.clone(),
});
let mut env = fabro_daytona::DaytonaSandbox::new(
config,
github_app.clone(),
Some(run_id.clone()),
detected_base_branch.clone(),
clone_params,
)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;

View file

@ -17,6 +17,17 @@ pub use fabro_config::sandbox::{
DaytonaConfig, DaytonaNetwork, DaytonaSnapshotConfig, DockerfileSource,
};
/// Parameters for cloning a git repository into the sandbox.
///
/// When provided to `DaytonaSandbox::new`, `initialize()` will clone the repo.
/// When `None`, the sandbox gets an empty working directory (no clone).
pub struct GitCloneParams {
/// HTTPS URL of the repository to clone.
pub url: String,
/// Optional branch to check out after cloning.
pub branch: Option<String>,
}
/// Sandbox that runs all operations inside a Daytona cloud sandbox.
pub struct DaytonaSandbox {
config: DaytonaConfig,
@ -28,10 +39,9 @@ pub struct DaytonaSandbox {
/// HTTPS origin URL stored after clone so we can refresh push credentials later.
origin_url: tokio::sync::OnceCell<String>,
run_id: Option<String>,
/// Explicit branch to clone. When set, overrides the branch detected by
/// `detect_repo_info` — avoids cloning a local-only worktree branch
/// (e.g. `fabro/run/...`) that was never pushed to origin.
clone_branch: Option<String>,
/// Clone parameters. When `Some`, `initialize()` clones the repo; when
/// `None`, the sandbox gets an empty working directory.
clone_params: Option<GitCloneParams>,
}
impl DaytonaSandbox {
@ -40,7 +50,7 @@ impl DaytonaSandbox {
config: DaytonaConfig,
github_app: Option<GitHubAppCredentials>,
run_id: Option<String>,
clone_branch: Option<String>,
clone_params: Option<GitCloneParams>,
) -> Result<Self, String> {
let client = daytona_sdk::Client::new()
.await
@ -54,7 +64,7 @@ impl DaytonaSandbox {
event_callback: None,
origin_url: tokio::sync::OnceCell::new(),
run_id,
clone_branch,
clone_params,
})
}
@ -81,7 +91,7 @@ impl DaytonaSandbox {
event_callback: None,
origin_url: tokio::sync::OnceCell::new(),
run_id: None,
clone_branch: None,
clone_params: None,
})
}
@ -289,8 +299,6 @@ impl DaytonaSandbox {
}
}
use fabro_github::ssh_url_to_https;
/// Detect the git remote URL and current branch from a local repository.
///
/// Uses `git2` to discover the repo at `path`, reads the `origin` remote URL
@ -389,8 +397,6 @@ impl Sandbox for DaytonaSandbox {
});
let init_start = Instant::now();
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let params = if let Some(ref snap_cfg) = self.config.snapshot {
self.emit(SandboxEvent::SnapshotEnsuring {
name: snap_cfg.name.clone(),
@ -444,178 +450,171 @@ impl Sandbox for DaytonaSandbox {
err
})?;
// Clone the repo into the sandbox
match detect_repo_info(&cwd) {
Ok((detected_url, detected_branch)) => {
// Use explicit clone_branch if provided (avoids cloning a local-only
// worktree branch like fabro/run/... that hasn't been pushed).
let branch = self.clone_branch.clone().or(detected_branch);
// Daytona clones over HTTPS with token auth, so rewrite SSH URLs.
let url = ssh_url_to_https(&detected_url);
self.emit(SandboxEvent::GitCloneStarted {
url: url.clone(),
branch: branch.clone(),
});
let clone_start = Instant::now();
// Clone the repo into the sandbox (if clone_params were provided)
if let Some(ref clone_params) = self.clone_params {
let url = clone_params.url.clone();
let branch = clone_params.branch.clone();
self.emit(SandboxEvent::GitCloneStarted {
url: url.clone(),
branch: branch.clone(),
});
let clone_start = Instant::now();
// Resolve clone credentials via GitHub App or fall back to no auth
let (username, password) = match &self.github_app {
Some(creds) => {
let (owner, repo) =
fabro_github::parse_github_owner_repo(&url).map_err(|e| {
let err = format!("Failed to parse GitHub URL for clone: {e}");
self.emit(SandboxEvent::GitCloneFailed {
url: url.clone(),
error: err.clone(),
});
err
})?;
fabro_github::resolve_clone_credentials(creds, &owner, &repo)
.await
.map_err(|e| {
let err =
format!("Failed to get GitHub App credentials for clone: {e}");
self.emit(SandboxEvent::GitCloneFailed {
url: url.clone(),
error: err.clone(),
});
let duration_ms = u64::try_from(init_start.elapsed().as_millis())
.unwrap_or(u64::MAX);
self.emit(SandboxEvent::InitializeFailed {
provider: "daytona".into(),
error: err.clone(),
duration_ms,
});
err
})?
}
None => (None, None),
};
// Resolve clone credentials via GitHub App or fall back to no auth
let (username, password) = match &self.github_app {
Some(creds) => {
let (owner, repo) =
fabro_github::parse_github_owner_repo(&url).map_err(|e| {
let err = format!("Failed to parse GitHub URL for clone: {e}");
self.emit(SandboxEvent::GitCloneFailed {
url: url.clone(),
error: err.clone(),
});
err
})?;
fabro_github::resolve_clone_credentials(creds, &owner, &repo)
.await
.map_err(|e| {
let err =
format!("Failed to get GitHub App credentials for clone: {e}");
self.emit(SandboxEvent::GitCloneFailed {
url: url.clone(),
error: err.clone(),
});
let duration_ms =
u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX);
self.emit(SandboxEvent::InitializeFailed {
provider: "daytona".into(),
error: err.clone(),
duration_ms,
});
err
})?
}
None => (None, None),
};
let git_svc = sandbox
.git()
.await
.map_err(|e| format!("Failed to get Daytona git service: {e}"));
let git_svc = match git_svc {
Ok(g) => g,
Err(e) => {
self.emit(SandboxEvent::GitCloneFailed {
url: url.clone(),
error: e.clone(),
});
let duration_ms =
u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX);
self.emit(SandboxEvent::InitializeFailed {
provider: "daytona".into(),
error: e.clone(),
duration_ms,
});
return Err(e);
}
};
let git_svc = sandbox
.git()
.await
.map_err(|e| format!("Failed to get Daytona git service: {e}"));
let git_svc = match git_svc {
Ok(g) => g,
Err(e) => {
self.emit(SandboxEvent::GitCloneFailed {
url: url.clone(),
error: e.clone(),
});
let duration_ms =
u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX);
self.emit(SandboxEvent::InitializeFailed {
provider: "daytona".into(),
error: e.clone(),
duration_ms,
});
return Err(e);
}
};
let clone_token = password.clone();
let clone_result = git_svc
.clone(
&url,
WORKING_DIRECTORY,
daytona_sdk::GitCloneOptions {
branch,
username,
password,
..Default::default()
},
)
.await;
let clone_token = password.clone();
let clone_result = git_svc
.clone(
&url,
WORKING_DIRECTORY,
daytona_sdk::GitCloneOptions {
branch,
username,
password,
..Default::default()
},
)
.await;
match clone_result {
Ok(()) => {
let clone_duration =
u64::try_from(clone_start.elapsed().as_millis()).unwrap_or(u64::MAX);
self.emit(SandboxEvent::GitCloneCompleted {
url: url.clone(),
duration_ms: clone_duration,
});
match clone_result {
Ok(()) => {
let clone_duration =
u64::try_from(clone_start.elapsed().as_millis()).unwrap_or(u64::MAX);
self.emit(SandboxEvent::GitCloneCompleted {
url: url.clone(),
duration_ms: clone_duration,
});
// Store origin URL and set push credentials for later pushes
if let Some(token) = clone_token {
let _ = self.origin_url.set(url);
let process_svc = sandbox.process().await.ok();
if let Some(ps) = process_svc {
let origin = self.origin_url.get().expect("just set");
let auth_url = origin.replacen(
"https://",
&format!("https://x-access-token:{token}@"),
1,
);
let cmd = format!(
"git -c maintenance.auto=0 remote set-url origin {}",
shell_quote(&auth_url),
);
let opts = daytona_sdk::ExecuteCommandOptions {
cwd: Some(WORKING_DIRECTORY.to_string()),
..Default::default()
};
let wrapped = wrap_bash_command(&cmd);
if let Ok(r) = ps.execute_command(&wrapped, opts).await {
if r.exit_code != 0 {
tracing::warn!(
exit_code = r.exit_code,
"Failed to set push credentials on origin"
);
}
// Store origin URL and set push credentials for later pushes
if let Some(token) = clone_token {
let _ = self.origin_url.set(url);
let process_svc = sandbox.process().await.ok();
if let Some(ps) = process_svc {
let origin = self.origin_url.get().expect("just set");
let auth_url = origin.replacen(
"https://",
&format!("https://x-access-token:{token}@"),
1,
);
let cmd = format!(
"git -c maintenance.auto=0 remote set-url origin {}",
shell_quote(&auth_url),
);
let opts = daytona_sdk::ExecuteCommandOptions {
cwd: Some(WORKING_DIRECTORY.to_string()),
..Default::default()
};
let wrapped = wrap_bash_command(&cmd);
if let Ok(r) = ps.execute_command(&wrapped, opts).await {
if r.exit_code != 0 {
tracing::warn!(
exit_code = r.exit_code,
"Failed to set push credentials on origin"
);
}
}
}
}
Err(e) if self.github_app.is_none() => {
let err = format!(
"Git clone failed: {e}. If this is a private repository, \
configure a GitHub App with `fabro install` and install it \
for your organization."
);
self.emit(SandboxEvent::GitCloneFailed {
url,
error: err.clone(),
});
let duration_ms =
u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX);
self.emit(SandboxEvent::InitializeFailed {
provider: "daytona".into(),
error: err.clone(),
duration_ms,
});
return Err(err);
}
Err(e) => {
let err = format!("Failed to clone repo into Daytona sandbox: {e}");
self.emit(SandboxEvent::GitCloneFailed {
url,
error: err.clone(),
});
let duration_ms =
u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX);
self.emit(SandboxEvent::InitializeFailed {
provider: "daytona".into(),
error: err.clone(),
duration_ms,
});
return Err(err);
}
}
Err(e) if self.github_app.is_none() => {
let err = format!(
"Git clone failed: {e}. If this is a private repository, \
configure a GitHub App with `fabro install` and install it \
for your organization."
);
self.emit(SandboxEvent::GitCloneFailed {
url,
error: err.clone(),
});
let duration_ms =
u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX);
self.emit(SandboxEvent::InitializeFailed {
provider: "daytona".into(),
error: err.clone(),
duration_ms,
});
return Err(err);
}
Err(e) => {
let err = format!("Failed to clone repo into Daytona sandbox: {e}");
self.emit(SandboxEvent::GitCloneFailed {
url,
error: err.clone(),
});
let duration_ms =
u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX);
self.emit(SandboxEvent::InitializeFailed {
provider: "daytona".into(),
error: err.clone(),
duration_ms,
});
return Err(err);
}
}
Err(e) => {
tracing::warn!(error = %e, "Could not detect git repo for Daytona clone");
// Create working directory even without a repo
let fs_svc = sandbox
.fs()
.await
.map_err(|e| format!("Failed to get Daytona fs service: {e}"))?;
fs_svc
.create_folder(WORKING_DIRECTORY, None)
.await
.map_err(|e| format!("Failed to create working directory: {e}"))?;
}
} else {
// No clone params — create working directory without a repo
let fs_svc = sandbox
.fs()
.await
.map_err(|e| format!("Failed to get Daytona fs service: {e}"))?;
fs_svc
.create_folder(WORKING_DIRECTORY, None)
.await
.map_err(|e| format!("Failed to create working directory: {e}"))?;
}
let sandbox_name = sandbox.name.clone();

View file

@ -34,7 +34,14 @@ async fn create_env_with_github_app(
if let Some(home) = dirs::home_dir() {
dotenvy::from_path(home.join(".fabro/.env")).ok();
}
DaytonaSandbox::new(DaytonaConfig::default(), github_app, None, None)
let cwd = std::env::current_dir().unwrap();
let clone_params = fabro_daytona::detect_repo_info(&cwd)
.ok()
.map(|(url, branch)| fabro_daytona::GitCloneParams {
url: fabro_github::ssh_url_to_https(&url),
branch,
});
DaytonaSandbox::new(DaytonaConfig::default(), github_app, None, clone_params)
.await
.expect("Failed to create Daytona client — is DAYTONA_API_KEY set?")
}
@ -1852,9 +1859,6 @@ async fn daytona_cp_upload_download_round_trip() {
async fn daytona_computer_use_browser_screenshot() {
use base64::Engine;
// Run from a temp dir so detect_repo_info() finds no git repo and skips cloning.
let tmp = tempfile::tempdir().unwrap();
std::env::set_current_dir(tmp.path()).unwrap();
dotenvy::dotenv().ok();
if let Some(home) = dirs::home_dir() {
dotenvy::from_path(home.join(".fabro/.env")).ok();
@ -2013,8 +2017,6 @@ async fn daytona_playwright_mcp_sandbox_transport() {
use fabro_agent::Sandbox;
// Create sandbox from daytona-medium (has Node.js + Chromium)
let tmp = tempfile::tempdir().unwrap();
std::env::set_current_dir(tmp.path()).unwrap();
dotenvy::dotenv().ok();
if let Some(home) = dirs::home_dir() {
dotenvy::from_path(home.join(".fabro/.env")).ok();