mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-21 00:21:27 +00:00
480 lines
22 KiB
Diff
480 lines
22 KiB
Diff
diff --git a/lib/crates/fabro-cli/src/commands/run.rs b/lib/crates/fabro-cli/src/commands/run.rs
|
|
index d180835b..8a422504 100644
|
|
--- a/lib/crates/fabro-cli/src/commands/run.rs
|
|
+++ b/lib/crates/fabro-cli/src/commands/run.rs
|
|
@@ -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}"))?;
|
|
diff --git a/lib/crates/fabro-daytona/src/lib.rs b/lib/crates/fabro-daytona/src/lib.rs
|
|
index 1f808377..42744a1c 100644
|
|
--- a/lib/crates/fabro-daytona/src/lib.rs
|
|
+++ b/lib/crates/fabro-daytona/src/lib.rs
|
|
@@ -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();
|
|
-
|
|
- // 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 clone_token = password.clone();
|
|
- let clone_result = git_svc
|
|
- .clone(
|
|
- &url,
|
|
- WORKING_DIRECTORY,
|
|
- daytona_sdk::GitCloneOptions {
|
|
- branch,
|
|
- username,
|
|
- password,
|
|
- ..Default::default()
|
|
- },
|
|
- )
|
|
- .await;
|
|
+ // 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),
|
|
+ };
|
|
|
|
- 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"
|
|
- );
|
|
- }
|
|
+ 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;
|
|
+
|
|
+ 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"
|
|
+ );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
- 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();
|
|
diff --git a/lib/crates/fabro-workflows/tests/daytona_integration.rs b/lib/crates/fabro-workflows/tests/daytona_integration.rs
|
|
index 92cbe424..285d9b42 100644
|
|
--- a/lib/crates/fabro-workflows/tests/daytona_integration.rs
|
|
+++ b/lib/crates/fabro-workflows/tests/daytona_integration.rs
|
|
@@ -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();
|