mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-13 23:14:17 +00:00
Build the local sandbox through the one provider path
SandboxSpec had a Local variant beside the provider spec, and a local sandbox was created by hand over a bare Host provider: no workspace, no provider connection, its own reconnect, and its own push rule for the designated directory. The local kind is now one more SandboxSpec: SandboxSpec::local names the directory on a HostDirectory spec with a skip clone, and provider_sandbox builds it like a plugin kind, creating the directory when missing since the Host provider requires it to exist. Every RunSandbox carries a workspace; a handle wrapped as is gets the workspace of its own working directory. The push rule is one rule for every checkout: a checkout fabro cloned pushes with the credentials it was cloned with, and any other checkout pushes when it has an origin, with whatever credentials it carries. A local run therefore pushes the same way before and after a resume; before, a reconnected local sandbox carried an attached workspace that never pushed while a fresh one did. Reconnect uses the recorded id for every kind. The recompute of a local id from its directory, kept for records written before directories had ids, is gone, and test fixtures that wrote made-up local ids derive them through test_support::local_sandbox_id instead. A local run's record now carries its workspace layout like every provider-chosen directory, and the sandbox.initializing event precedes the driver's create events for local as for every other kind. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
981253f990
commit
350abac014
16 changed files with 406 additions and 412 deletions
|
|
@ -118,6 +118,7 @@ chrono = { workspace = true }
|
|||
assert_cmd = "2"
|
||||
fabro-acp = { path = "../../components/fabro-acp", features = ["test-support"] }
|
||||
fabro-build-support = { path = "../../foundation/build-support" }
|
||||
fabro-sandbox = { path = "../../components/fabro-sandbox", features = ["test-support"] }
|
||||
fabro-server = { path = "../fabro-server", features = ["test-support"] }
|
||||
fabro-workflow = { path = "../../components/fabro-workflow", features = ["test-support"] }
|
||||
fabro-types = { path = "../../foundation/fabro-types", features = ["clap", "test-support"] }
|
||||
|
|
|
|||
|
|
@ -1084,6 +1084,19 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"run_id": "[ULID]",
|
||||
"ts": "[TIMESTAMP]"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"kind": "worker",
|
||||
"run_id": "[ULID]"
|
||||
},
|
||||
"event": "sandbox.initializing",
|
||||
"id": "[EVENT_ID]",
|
||||
"properties": {
|
||||
"provider": "local"
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"ts": "[TIMESTAMP]"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"kind": "worker",
|
||||
|
|
@ -1169,19 +1182,6 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"run_id": "[ULID]",
|
||||
"ts": "[TIMESTAMP]"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"kind": "worker",
|
||||
"run_id": "[ULID]"
|
||||
},
|
||||
"event": "sandbox.initializing",
|
||||
"id": "[EVENT_ID]",
|
||||
"properties": {
|
||||
"provider": "local"
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"ts": "[TIMESTAMP]"
|
||||
},
|
||||
{
|
||||
"actor": {
|
||||
"kind": "worker",
|
||||
|
|
@ -1207,7 +1207,9 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"id": "host-dir-[HEX]",
|
||||
"provider": "local",
|
||||
"repo_cloned": false,
|
||||
"working_directory": "[TEMP_DIR]"
|
||||
"repos_root": "[TEMP_DIR]/.repos",
|
||||
"working_directory": "[TEMP_DIR]",
|
||||
"workspace_root": "[TEMP_DIR]"
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"ts": "[TIMESTAMP]"
|
||||
|
|
|
|||
|
|
@ -1107,7 +1107,7 @@ async fn append_seeded_simple_completion_events(
|
|||
serde_json::json!({
|
||||
"working_directory": context.temp_dir.display().to_string(),
|
||||
"provider": "local",
|
||||
"id": format!("local:{}", run.run_id),
|
||||
"id": fabro_sandbox::test_support::local_sandbox_id(&context.temp_dir).await,
|
||||
"repo_cloned": false,
|
||||
"clone_origin_url": null,
|
||||
"clone_branch": null,
|
||||
|
|
@ -1276,7 +1276,7 @@ async fn append_seeded_git_completion_events(
|
|||
serde_json::json!({
|
||||
"working_directory": context.temp_dir.display().to_string(),
|
||||
"provider": "local",
|
||||
"id": format!("local:{}", run.run_id),
|
||||
"id": fabro_sandbox::test_support::local_sandbox_id(&context.temp_dir).await,
|
||||
"repo_cloned": false,
|
||||
"clone_origin_url": null,
|
||||
"clone_branch": null,
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ use fabro_llm::FabroClient;
|
|||
use fabro_llm::lithos_catalog::Catalog;
|
||||
use fabro_llm::probe::{self, ModelTestStatus};
|
||||
use fabro_sandbox::{
|
||||
CloneRequest, ProviderAccess, ProviderSandboxSpec, RunSandbox, SandboxSpec,
|
||||
sandbox_spec_for_environment,
|
||||
CloneRequest, ProviderAccess, RunSandbox, SandboxSpec, sandbox_spec_for_environment,
|
||||
};
|
||||
use fabro_static::EnvVars;
|
||||
use fabro_types::settings::ModelRef;
|
||||
|
|
@ -929,7 +928,7 @@ fn preflight_sandbox_spec(
|
|||
err,
|
||||
)
|
||||
})?;
|
||||
return Ok(SandboxSpec::Local { working_directory });
|
||||
return Ok(SandboxSpec::local(working_directory, access.clone()));
|
||||
}
|
||||
// No vault is available on this path, so a `{{ secrets.* }}` value keeps
|
||||
// its source form. Preflight never clones.
|
||||
|
|
@ -942,14 +941,14 @@ fn preflight_sandbox_spec(
|
|||
branch: clone_branch,
|
||||
..CloneRequest::none()
|
||||
};
|
||||
Ok(SandboxSpec::Provider(Box::new(ProviderSandboxSpec {
|
||||
Ok(SandboxSpec {
|
||||
kind: sandbox_provider.clone(),
|
||||
access: access.clone(),
|
||||
spec,
|
||||
clone,
|
||||
github_app,
|
||||
run_id: None,
|
||||
})))
|
||||
})
|
||||
}
|
||||
|
||||
async fn run_sandbox_check(
|
||||
|
|
@ -2220,18 +2219,14 @@ provider = "local"
|
|||
&ProviderAccess::default(),
|
||||
);
|
||||
|
||||
match spec {
|
||||
Ok(SandboxSpec::Provider(spec)) => {
|
||||
assert_eq!(spec.kind, SandboxProviderKind::DOCKER);
|
||||
assert!(spec.clone.skip);
|
||||
assert_eq!(
|
||||
spec.clone.origin_url.as_deref(),
|
||||
Some("https://github.com/acme/widgets")
|
||||
);
|
||||
assert_eq!(spec.clone.branch.as_deref(), Some("main"));
|
||||
}
|
||||
_ => panic!("expected Docker preflight sandbox spec"),
|
||||
}
|
||||
let spec = spec.expect("Docker preflight sandbox spec");
|
||||
assert_eq!(spec.kind, SandboxProviderKind::DOCKER);
|
||||
assert!(spec.clone.skip);
|
||||
assert_eq!(
|
||||
spec.clone.origin_url.as_deref(),
|
||||
Some("https://github.com/acme/widgets")
|
||||
);
|
||||
assert_eq!(spec.clone.branch.as_deref(), Some("main"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1011,6 +1011,7 @@ mod tests {
|
|||
mod retrieve_sandbox_tests {
|
||||
use axum::body::{Body, to_bytes};
|
||||
use axum::http::{Request, StatusCode};
|
||||
use fabro_sandbox::test_support::local_sandbox_id;
|
||||
use fabro_types::{Graph, RunId, WorkflowSettings, test_support};
|
||||
use serde_json::{Value, json};
|
||||
use tower::ServiceExt;
|
||||
|
|
@ -1064,15 +1065,24 @@ mod retrieve_sandbox_tests {
|
|||
run_id: &RunId,
|
||||
provider: &str,
|
||||
) {
|
||||
append_sandbox_initialized_in(run_store, run_id, provider, "/workspace").await;
|
||||
append_sandbox_initialized_in(
|
||||
run_store,
|
||||
run_id,
|
||||
provider,
|
||||
&format!("{provider}:sandbox-id"),
|
||||
"/workspace",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// A local sandbox reconnects by attaching to its working directory, so
|
||||
/// a test that reaches one records a directory that exists.
|
||||
/// A local sandbox reconnects by the id the Host provider derives from
|
||||
/// its working directory, so a test that reaches one records an
|
||||
/// existing directory under the id fabro would have written for it.
|
||||
async fn append_sandbox_initialized_in(
|
||||
run_store: &fabro_store::RunDatabase,
|
||||
run_id: &RunId,
|
||||
provider: &str,
|
||||
id: &str,
|
||||
working_directory: &str,
|
||||
) {
|
||||
let payload = fabro_store::EventPayload::new(
|
||||
|
|
@ -1083,7 +1093,7 @@ mod retrieve_sandbox_tests {
|
|||
"event": "sandbox.initialized",
|
||||
"properties": {
|
||||
"provider": provider,
|
||||
"id": format!("{provider}:sandbox-id"),
|
||||
"id": id,
|
||||
"working_directory": working_directory,
|
||||
},
|
||||
}),
|
||||
|
|
@ -1218,11 +1228,10 @@ mod retrieve_sandbox_tests {
|
|||
.await
|
||||
.expect("test run should be creatable");
|
||||
append_run_created(&run_store, &run_id).await;
|
||||
// A record written before local sandboxes had directory-derived
|
||||
// ids: the id is recomputed from the directory on reconnect.
|
||||
let workspace = tempfile::tempdir().expect("scratch directory");
|
||||
let working_directory = workspace.path().to_str().expect("utf-8").to_owned();
|
||||
append_sandbox_initialized_in(&run_store, &run_id, "local", &working_directory).await;
|
||||
let id = local_sandbox_id(workspace.path()).await;
|
||||
append_sandbox_initialized_in(&run_store, &run_id, "local", &id, &working_directory).await;
|
||||
|
||||
let response = app
|
||||
.oneshot(req_get(&format!("/api/v1/runs/{run_id}/sandbox")))
|
||||
|
|
@ -1231,7 +1240,7 @@ mod retrieve_sandbox_tests {
|
|||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = body_json(response).await;
|
||||
assert_eq!(body["sandbox"]["provider"], "local");
|
||||
assert_eq!(body["sandbox"]["runtime"]["id"], "local:sandbox-id");
|
||||
assert_eq!(body["sandbox"]["runtime"]["id"], id);
|
||||
assert_eq!(
|
||||
body["sandbox"]["runtime"]["working_directory"],
|
||||
working_directory
|
||||
|
|
@ -1265,6 +1274,7 @@ mod retrieve_sandbox_tests {
|
|||
&run_store,
|
||||
&run_id,
|
||||
"local",
|
||||
&local_sandbox_id(workspace.path()).await,
|
||||
workspace.path().to_str().expect("utf-8"),
|
||||
)
|
||||
.await;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
//! through the [`EventContext`] a sandbox is created or attached with.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
|
|
@ -23,11 +23,9 @@ use fabro_util::workspace_glob::WorkspaceGlob;
|
|||
use sandbox_driver::{
|
||||
DirEntry, EventContext, ExecControls, ExecResult, ExecSpec, ExecStreamingResult, FileKind,
|
||||
GitRetryPolicy, GrepMatch, GrepOptions, PtyOptions, PtySession, PtySize,
|
||||
Sandbox as DriverHandle, SandboxProvider as DriverProvider, SandboxSource,
|
||||
SandboxSpec as DriverSpec, SandboxState, Search as _, StdioProcess, WaitOptions, WalkOptions,
|
||||
Sandbox as DriverHandle, SandboxProvider as DriverProvider, SandboxSpec as DriverSpec,
|
||||
SandboxState, Search as _, StdioProcess, WaitOptions, WalkOptions,
|
||||
};
|
||||
use sandbox_driver_host::HostProvider;
|
||||
use tokio::fs;
|
||||
use tokio::sync::OnceCell;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
|
|
@ -35,42 +33,9 @@ use crate::clone::{self, GitHubClone};
|
|||
use crate::clone_source::{self, CloneDecision, EmptyWorkspaceReason};
|
||||
use crate::credentials::{self, RepoCredentials};
|
||||
use crate::environment::CloneRequest;
|
||||
use crate::{GitRunInfo, GitSetupIntent};
|
||||
|
||||
/// A sandbox on the worker host at `working_directory`, the fabro `local`
|
||||
/// kind, served by the driver's in-process Host provider.
|
||||
///
|
||||
/// The directory is designated: the sandbox uses it in place and never
|
||||
/// removes it. It is created when missing so a run can point at a fresh
|
||||
/// scratch path. The registry lives in a per-process temporary root, so a
|
||||
/// later process rebuilds the handle by calling this again with the
|
||||
/// persisted working directory rather than by id.
|
||||
pub async fn local_sandbox(working_directory: impl Into<PathBuf>) -> crate::Result<RunSandbox> {
|
||||
local_sandbox_with_events(working_directory, None).await
|
||||
}
|
||||
|
||||
/// [`local_sandbox`] whose driver lifecycle events reach `events`.
|
||||
pub async fn local_sandbox_with_events(
|
||||
working_directory: impl Into<PathBuf>,
|
||||
events: Option<EventContext>,
|
||||
) -> crate::Result<RunSandbox> {
|
||||
let working_directory: PathBuf = working_directory.into();
|
||||
fs::create_dir_all(&working_directory)
|
||||
.await
|
||||
.map_err(|error| crate::Error::context("Failed to create working directory", error))?;
|
||||
let provider = HostProvider::new();
|
||||
let spec = DriverSpec::new(SandboxSource::HostDirectory)
|
||||
.working_directory(working_directory.display().to_string());
|
||||
let handle = provider
|
||||
.create(&spec, events)
|
||||
.await
|
||||
.map_err(|error| crate::Error::context("Failed to create local sandbox", error))?;
|
||||
let sandbox = RunSandbox::new(SandboxProviderKind::LOCAL, handle);
|
||||
sandbox.learn_platform().await?;
|
||||
Ok(sandbox)
|
||||
}
|
||||
use crate::exec::SandboxExec;
|
||||
use crate::sandbox::{self, PushError, PushReport, SandboxFile, SandboxWorkspaceLayout};
|
||||
use crate::{GitRunInfo, GitSetupIntent};
|
||||
|
||||
/// Where a clone-based provider puts its files: the run works under
|
||||
/// `workspace_root`, and repositories check out under `repos_root`.
|
||||
|
|
@ -111,8 +76,10 @@ enum WorkspacePlan {
|
|||
Attached,
|
||||
}
|
||||
|
||||
/// Fabro's clone-based workspace on an isolated sandbox: the layout, the
|
||||
/// clone it performs, and the GitHub credentials its checkout carries.
|
||||
/// The run's workspace on a sandbox: the layout, the clone fabro performs
|
||||
/// into it (if any), and the GitHub credentials its checkout carries. A
|
||||
/// workspace fabro did not clone into is still a checkout the run may push
|
||||
/// from, with whatever credentials the checkout carries itself.
|
||||
pub(crate) struct RepoWorkspace {
|
||||
layout: OnceLock<WorkspaceLayout>,
|
||||
plan: WorkspacePlan,
|
||||
|
|
@ -202,6 +169,21 @@ impl RepoWorkspace {
|
|||
workspace
|
||||
}
|
||||
|
||||
/// The workspace an existing handle already works in, whatever it
|
||||
/// holds: nothing fabro cloned, laid out from the handle's own working
|
||||
/// directory.
|
||||
pub(crate) fn existing() -> Self {
|
||||
Self {
|
||||
layout: LayoutSource::ProviderWorkingDirectory.into_cell(),
|
||||
plan: WorkspacePlan::Attached,
|
||||
credentials: RepoCredentials::none(),
|
||||
repo_cloned: OnceLock::new(),
|
||||
origin_url: OnceLock::new(),
|
||||
execution_directory: OnceLock::new(),
|
||||
checkout_path: OnceLock::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Settle a provider-dependent layout from the sandbox's working
|
||||
/// directory. A fixed layout is left alone.
|
||||
fn resolve_layout(&self, provider_working_directory: &str) -> &WorkspaceLayout {
|
||||
|
|
@ -285,7 +267,7 @@ pub struct RunSandbox {
|
|||
/// pending one.
|
||||
handle: OnceCell<Arc<dyn DriverHandle>>,
|
||||
pending: Option<PendingCreate>,
|
||||
workspace: Option<RepoWorkspace>,
|
||||
workspace: RepoWorkspace,
|
||||
/// Where the driver reports the lifecycle of a sandbox this creates.
|
||||
/// Set before `initialize` on a pending sandbox; an existing handle
|
||||
/// already carries the context it was created or attached with.
|
||||
|
|
@ -298,12 +280,11 @@ pub struct RunSandbox {
|
|||
}
|
||||
|
||||
impl RunSandbox {
|
||||
/// Wraps an existing driver handle as a sandbox of `kind`.
|
||||
/// Wraps an existing driver handle as a sandbox of `kind`, working in
|
||||
/// whatever the handle's working directory holds.
|
||||
#[must_use]
|
||||
pub fn new(kind: SandboxProviderKind, handle: Arc<dyn DriverHandle>) -> Self {
|
||||
let sandbox = Self::empty(kind);
|
||||
let _ = sandbox.handle.set(handle);
|
||||
sandbox
|
||||
Self::attached(kind, handle, RepoWorkspace::existing())
|
||||
}
|
||||
|
||||
/// A sandbox over an existing handle whose platform is already known,
|
||||
|
|
@ -328,9 +309,8 @@ impl RunSandbox {
|
|||
spec: DriverSpec,
|
||||
workspace: RepoWorkspace,
|
||||
) -> Self {
|
||||
let mut sandbox = Self::empty(kind);
|
||||
let mut sandbox = Self::empty(kind, workspace);
|
||||
sandbox.pending = Some(PendingCreate { provider, spec });
|
||||
sandbox.workspace = Some(workspace);
|
||||
sandbox
|
||||
}
|
||||
|
||||
|
|
@ -347,17 +327,17 @@ impl RunSandbox {
|
|||
workspace: RepoWorkspace,
|
||||
) -> Self {
|
||||
workspace.resolve_layout(handle.working_directory());
|
||||
let mut sandbox = Self::new(kind, handle);
|
||||
sandbox.workspace = Some(workspace);
|
||||
let sandbox = Self::empty(kind, workspace);
|
||||
let _ = sandbox.handle.set(handle);
|
||||
sandbox
|
||||
}
|
||||
|
||||
fn empty(kind: SandboxProviderKind) -> Self {
|
||||
fn empty(kind: SandboxProviderKind, workspace: RepoWorkspace) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
handle: OnceCell::new(),
|
||||
pending: None,
|
||||
workspace: None,
|
||||
workspace,
|
||||
events: None,
|
||||
platform: OnceLock::new(),
|
||||
snapshot: OnceLock::new(),
|
||||
|
|
@ -393,10 +373,8 @@ impl RunSandbox {
|
|||
/// run's directory. Absent until a pending sandbox is initialized.
|
||||
pub fn exec(&self) -> crate::Result<SandboxExec<'_>> {
|
||||
let mut exec = SandboxExec::new(self.handle()?.exec());
|
||||
if let Some(workspace) = &self.workspace {
|
||||
if let Some(dir) = workspace.execution_directory.get() {
|
||||
exec = exec.with_working_dir(dir.clone());
|
||||
}
|
||||
if let Some(dir) = self.workspace.execution_directory.get() {
|
||||
exec = exec.with_working_dir(dir.clone());
|
||||
}
|
||||
Ok(exec)
|
||||
}
|
||||
|
|
@ -405,11 +383,7 @@ impl RunSandbox {
|
|||
/// resolves relative paths against the sandbox's own working directory,
|
||||
/// which sits above a cloned repository's link.
|
||||
fn resolve(&self, path: &str) -> String {
|
||||
match self
|
||||
.workspace
|
||||
.as_ref()
|
||||
.and_then(|workspace| workspace.execution_directory.get())
|
||||
{
|
||||
match self.workspace.execution_directory.get() {
|
||||
Some(working_directory) => sandbox::resolve_path(path, working_directory),
|
||||
None => path.to_string(),
|
||||
}
|
||||
|
|
@ -488,9 +462,7 @@ impl RunSandbox {
|
|||
/// Prepare the workspace after the sandbox runs for the first time:
|
||||
/// an empty root, or fabro's clone.
|
||||
async fn prepare_workspace(&self) -> crate::Result<()> {
|
||||
let Some(workspace) = &self.workspace else {
|
||||
return Ok(());
|
||||
};
|
||||
let workspace = &self.workspace;
|
||||
let layout = workspace
|
||||
.resolve_layout(self.handle()?.working_directory())
|
||||
.clone();
|
||||
|
|
@ -615,11 +587,7 @@ impl RunSandbox {
|
|||
// A cloned repository is reached through a workspace link. The
|
||||
// driver refuses a symlinked traversal root, so walk the real
|
||||
// checkout; results are reported under the link.
|
||||
if let Some(checkout) = self
|
||||
.workspace
|
||||
.as_ref()
|
||||
.and_then(|workspace| workspace.checkout_path.get())
|
||||
{
|
||||
if let Some(checkout) = self.workspace.checkout_path.get() {
|
||||
return sandbox::join_sandbox_path(checkout, relative_start);
|
||||
}
|
||||
if relative_start.is_empty() {
|
||||
|
|
@ -909,16 +877,10 @@ impl RunSandbox {
|
|||
/// The directory the run works in: the cloned repository's link for a
|
||||
/// clone-based workspace, the provider's working directory otherwise.
|
||||
pub fn working_directory(&self) -> &str {
|
||||
if let Some(directory) = self
|
||||
.workspace
|
||||
.as_ref()
|
||||
.and_then(RepoWorkspace::working_directory)
|
||||
{
|
||||
return directory;
|
||||
}
|
||||
self.handle
|
||||
.get()
|
||||
.map_or("", |handle| handle.working_directory())
|
||||
self.workspace
|
||||
.working_directory()
|
||||
.or_else(|| self.handle.get().map(|handle| handle.working_directory()))
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
pub fn runtime_directory(&self) -> Option<&str> {
|
||||
|
|
@ -955,7 +917,7 @@ impl RunSandbox {
|
|||
}
|
||||
|
||||
pub fn workspace_layout(&self) -> Option<SandboxWorkspaceLayout> {
|
||||
self.workspace.as_ref().and_then(RepoWorkspace::record)
|
||||
self.workspace.record()
|
||||
}
|
||||
|
||||
pub async fn setup_git(&self, intent: &GitSetupIntent) -> crate::Result<Option<GitRunInfo>> {
|
||||
|
|
@ -965,44 +927,42 @@ impl RunSandbox {
|
|||
sandbox::setup_git(self, intent).await.map(Some)
|
||||
}
|
||||
|
||||
/// Push `refspec` from the run's checkout. A checkout fabro cloned
|
||||
/// pushes with the credentials it was cloned with. Any other checkout
|
||||
/// pushes only when it has an origin, with whatever credentials it
|
||||
/// carries itself; a workspace without one has nothing to push.
|
||||
pub async fn git_push_ref(
|
||||
&self,
|
||||
refspec: &str,
|
||||
policy: &GitRetryPolicy,
|
||||
) -> Result<PushReport, PushError> {
|
||||
let Some(workspace) = &self.workspace else {
|
||||
// A designated directory: push only when the checkout has an
|
||||
// origin, with whatever credentials its URL already carries.
|
||||
let has_origin = match self
|
||||
.exec_command("git remote get-url origin", 10_000, None, None, None)
|
||||
.await
|
||||
{
|
||||
Ok(result) if result.success() => true,
|
||||
Ok(_) => false,
|
||||
Err(err) => {
|
||||
return Err(PushError {
|
||||
report: PushReport::default(),
|
||||
error: crate::Error::context("git remote get-url origin", err),
|
||||
});
|
||||
}
|
||||
};
|
||||
if !has_origin {
|
||||
return Ok(PushReport::default());
|
||||
let workspace = &self.workspace;
|
||||
if workspace.repo_cloned() {
|
||||
return sandbox::git_push(self, Some(&workspace.credentials), refspec, policy).await;
|
||||
}
|
||||
let has_origin = match self
|
||||
.exec_command("git remote get-url origin", 10_000, None, None, None)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result.success(),
|
||||
Err(err) => {
|
||||
return Err(PushError {
|
||||
report: PushReport::default(),
|
||||
error: crate::Error::context("git remote get-url origin", err),
|
||||
});
|
||||
}
|
||||
return sandbox::git_push(self, None, refspec, policy).await;
|
||||
};
|
||||
if !workspace.repo_cloned() {
|
||||
if !has_origin {
|
||||
return Ok(PushReport::default());
|
||||
}
|
||||
sandbox::git_push(self, Some(&workspace.credentials), refspec, policy).await
|
||||
sandbox::git_push(self, None, refspec, policy).await
|
||||
}
|
||||
|
||||
pub fn origin_url(&self) -> Option<&str> {
|
||||
let workspace = self.workspace.as_ref()?;
|
||||
if !workspace.repo_cloned() {
|
||||
if !self.workspace.repo_cloned() {
|
||||
return None;
|
||||
}
|
||||
workspace.origin_url.get().map(String::as_str)
|
||||
self.workspace.origin_url.get().map(String::as_str)
|
||||
}
|
||||
|
||||
/// Renew the credentials the agent's own git commands read for the
|
||||
|
|
@ -1012,9 +972,7 @@ impl RunSandbox {
|
|||
/// checkout to install them in.
|
||||
#[tracing::instrument(name = "git_op", skip_all, fields(op = "refresh-credentials"))]
|
||||
pub async fn refresh_ambient_credentials(&self) -> crate::Result<Option<TokenSnapshot>> {
|
||||
let Some(workspace) = &self.workspace else {
|
||||
return Ok(None);
|
||||
};
|
||||
let workspace = &self.workspace;
|
||||
let Some(checkout) = workspace.checkout_path.get() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
|
@ -1026,9 +984,7 @@ impl RunSandbox {
|
|||
}
|
||||
|
||||
pub fn push_token_source(&self) -> Option<Arc<InstallationTokenSource>> {
|
||||
self.workspace
|
||||
.as_ref()
|
||||
.and_then(|workspace| workspace.credentials.source().cloned())
|
||||
self.workspace.credentials.source().cloned()
|
||||
}
|
||||
|
||||
/// The local command that opens a shell in the sandbox, from the
|
||||
|
|
@ -1065,9 +1021,7 @@ impl RunSandbox {
|
|||
|
||||
impl RunSandbox {
|
||||
fn repo_cloned(&self) -> bool {
|
||||
self.workspace
|
||||
.as_ref()
|
||||
.is_some_and(RepoWorkspace::repo_cloned)
|
||||
self.workspace.repo_cloned()
|
||||
}
|
||||
|
||||
/// Delete the sandbox on the provider. A pending sandbox that was never
|
||||
|
|
@ -1095,7 +1049,10 @@ mod tests {
|
|||
use tokio::fs;
|
||||
|
||||
use super::*;
|
||||
use crate::driver::ProviderAccess;
|
||||
use crate::exec::ExecResultExt;
|
||||
use crate::provider_sandbox::local_sandbox;
|
||||
use crate::sandbox_spec::SandboxSpec as RunSandboxSpec;
|
||||
|
||||
struct Fixture {
|
||||
dir: tempfile::TempDir,
|
||||
|
|
@ -1327,14 +1284,13 @@ mod tests {
|
|||
async fn lifecycle_reaches_the_driver_events_and_learns_the_platform() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let recorded = Arc::new(Recorded(Mutex::new(Vec::new())));
|
||||
let sandbox = local_sandbox_with_events(
|
||||
dir.path(),
|
||||
Some(EventContext::new(
|
||||
let sandbox = RunSandboxSpec::local(dir.path(), ProviderAccess::default())
|
||||
.build(Some(EventContext::new(
|
||||
Arc::clone(&recorded) as Arc<dyn sandbox_driver::EventObserver>
|
||||
)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
)))
|
||||
.await
|
||||
.unwrap();
|
||||
sandbox.initialize().await.unwrap();
|
||||
let expected = if cfg!(target_os = "macos") {
|
||||
"darwin"
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ pub mod test_support;
|
|||
pub use details::sandbox_details;
|
||||
pub use docker::check_docker_daemon;
|
||||
pub use driver::{DaytonaCredentials, ProviderAccess};
|
||||
pub use driver_sandbox::{RunSandbox, local_sandbox};
|
||||
pub use driver_sandbox::RunSandbox;
|
||||
pub use environment::{CloneRequest, sandbox_spec_for_environment};
|
||||
pub use error::{Error, Result, default_redacted_output_tail, display_for_log};
|
||||
pub use exec::{
|
||||
|
|
@ -49,7 +49,7 @@ pub use git_policy::{
|
|||
retry_git_messages, transient_git_failure,
|
||||
};
|
||||
pub use provider::{SandboxInventory, SandboxLookupError};
|
||||
pub use provider_sandbox::{attach_provider_sandbox, provider_sandbox};
|
||||
pub use provider_sandbox::{attach_provider_sandbox, local_sandbox, provider_sandbox};
|
||||
pub use reconnect::{open_terminal_for_run, reconnect_for_run};
|
||||
pub use sandbox::{
|
||||
DEFAULT_EXEC_OUTPUT_TAIL_BYTES, GitRunInfo, GitSetupIntent, PushAttempt, PushError, PushReport,
|
||||
|
|
@ -65,4 +65,4 @@ pub use sandbox_driver::{
|
|||
OutputStream, PtySession, PtySize, Resources, SandboxSource, SandboxSpec as DriverSpec,
|
||||
StderrTail, StdioProcess, StdioProcessHandle, Termination, TransportError, WalkOptions,
|
||||
};
|
||||
pub use sandbox_spec::{ProviderSandboxSpec, SandboxSpec};
|
||||
pub use sandbox_spec::SandboxSpec;
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@
|
|||
//! construction function, and a bundled provider adds only what its
|
||||
//! backend needs on top: Docker its fixed working directory and default
|
||||
//! image, Daytona its fixed working directory, default snapshot, and
|
||||
//! lifecycle timers. A plugin gets the spec as is, trimmed to what it can
|
||||
//! honor, laid out inside the working directory the provider chooses.
|
||||
//! lifecycle timers, the Host the designated directory it works in,
|
||||
//! created when missing. A plugin gets the spec as is, trimmed to what it
|
||||
//! can honor, laid out inside the working directory the provider chooses.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fabro_github::GitHubCredentials;
|
||||
|
|
@ -17,10 +19,12 @@ use sandbox_driver::{
|
|||
EventContext, OwnedProvider, SandboxId, SandboxProvider, SandboxSource,
|
||||
SandboxSpec as DriverSpec,
|
||||
};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::driver::{ProviderAccess, connect_provider};
|
||||
use crate::driver_sandbox::{LayoutSource, RepoWorkspace, RunSandbox};
|
||||
use crate::environment::{self, CloneRequest};
|
||||
use crate::sandbox_spec::SandboxSpec;
|
||||
use crate::{daytona, docker, managed_labels};
|
||||
|
||||
/// A sandbox for a run on `kind`. The sandbox is created by `initialize`;
|
||||
|
|
@ -51,12 +55,10 @@ pub async fn provider_sandbox(
|
|||
daytona::overlay(spec, run_id.as_ref()),
|
||||
workspace,
|
||||
),
|
||||
Some(BundledProvider::Local) => {
|
||||
return Err(crate::Error::message(
|
||||
"local sandboxes are built from a working directory, not a provider spec",
|
||||
));
|
||||
}
|
||||
None => {
|
||||
Some(BundledProvider::Local) | None => {
|
||||
if kind.is_local() {
|
||||
designate_directory(&spec).await?;
|
||||
}
|
||||
let capabilities = provider.capabilities();
|
||||
spec.network = environment::supported_network(spec.network, capabilities);
|
||||
spec.timers = environment::supported_timers(spec.timers, capabilities);
|
||||
|
|
@ -65,6 +67,33 @@ pub async fn provider_sandbox(
|
|||
})
|
||||
}
|
||||
|
||||
/// The Host provider works in a designated directory in place and needs it
|
||||
/// to exist. A run may point at a fresh scratch path, so the directory is
|
||||
/// created before the provider sees the spec.
|
||||
async fn designate_directory(spec: &DriverSpec) -> crate::Result<()> {
|
||||
let Some(directory) = &spec.working_directory else {
|
||||
return Ok(());
|
||||
};
|
||||
fs::create_dir_all(directory).await.map_err(|error| {
|
||||
crate::Error::context(
|
||||
format!("Failed to create working directory {directory}"),
|
||||
error,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// A sandbox on this host at `working_directory`, ready to use: the `local`
|
||||
/// kind, built through the provider path with default settings and
|
||||
/// initialized. For the agent CLI and tests; a run builds its sandbox from
|
||||
/// its [`SandboxSpec`] and initializes it itself.
|
||||
pub async fn local_sandbox(working_directory: impl Into<PathBuf>) -> crate::Result<RunSandbox> {
|
||||
let spec = SandboxSpec::local(working_directory, ProviderAccess::default());
|
||||
let sandbox =
|
||||
provider_sandbox(spec.kind, &spec.access, spec.spec, &spec.clone, None, None).await?;
|
||||
sandbox.initialize().await?;
|
||||
Ok(sandbox)
|
||||
}
|
||||
|
||||
/// Reattach to a run's sandbox on `kind` by its persisted id. The driver
|
||||
/// reports the sandbox's lifecycle from here on through `events`.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use fabro_types::{BundledProvider, RunId, RunSandboxInstance};
|
||||
use fabro_types::{RunId, RunSandboxInstance};
|
||||
use sandbox_driver::{EventContext, PtySession, PtySize};
|
||||
use sandbox_driver_host::HostProvider;
|
||||
|
||||
use crate::driver::ProviderAccess;
|
||||
use crate::driver_sandbox::RunSandbox;
|
||||
|
|
@ -22,11 +19,10 @@ pub async fn reconnect_for_run(
|
|||
events: Option<EventContext>,
|
||||
) -> Result<RunSandbox> {
|
||||
let runtime = &record.runtime;
|
||||
let sandbox_id = sandbox_id(record).await;
|
||||
provider_sandbox::attach_provider_sandbox(
|
||||
record.provider.clone(),
|
||||
access,
|
||||
&sandbox_id,
|
||||
&runtime.id,
|
||||
// A record without the flag was written for a sandbox fabro never
|
||||
// cloned into.
|
||||
runtime.repo_cloned.unwrap_or(false),
|
||||
|
|
@ -39,20 +35,6 @@ pub async fn reconnect_for_run(
|
|||
.with_context(|| format!("Failed to reconnect {} sandbox", record.provider))
|
||||
}
|
||||
|
||||
/// The id the record's sandbox attaches by. A local sandbox is its working
|
||||
/// directory, and the Host provider derives the directory's id from its
|
||||
/// path, so the record's id is recomputed from the directory: a record
|
||||
/// written before directories had ids attaches the same way.
|
||||
async fn sandbox_id(record: &RunSandboxInstance) -> String {
|
||||
let runtime = &record.runtime;
|
||||
if record.provider.bundled() == Some(BundledProvider::Local) {
|
||||
if let Some(id) = HostProvider::directory_id(Path::new(&runtime.working_directory)).await {
|
||||
return id.to_string();
|
||||
}
|
||||
}
|
||||
runtime.id.clone()
|
||||
}
|
||||
|
||||
/// Opens an interactive shell in a run's sandbox over the driver's Pty
|
||||
/// facet, reconnecting from the run record first. The session is the
|
||||
/// driver's own; it is closed by the caller.
|
||||
|
|
|
|||
|
|
@ -242,8 +242,8 @@ pub struct PushError {
|
|||
/// Pushes a refspec to origin through the driver's git facet, retried by
|
||||
/// the driver under `policy` with one token for the whole operation.
|
||||
/// `credentials` is the checkout's managed credentials; `None` pushes with
|
||||
/// whatever the checkout already has (the local sandbox, or a workspace
|
||||
/// without a GitHub App).
|
||||
/// whatever the checkout already has (a checkout fabro did not clone, or a
|
||||
/// clone made without a GitHub App).
|
||||
#[tracing::instrument(name = "git_op", skip_all, fields(op = "push"))]
|
||||
pub(crate) async fn git_push(
|
||||
sandbox: &RunSandbox,
|
||||
|
|
|
|||
|
|
@ -4,28 +4,18 @@ use std::sync::Arc;
|
|||
use anyhow::Context as _;
|
||||
use fabro_github::GitHubCredentials;
|
||||
use fabro_types::{RunId, RunSandboxInstance, RunSandboxRuntime, SandboxProviderKind};
|
||||
use sandbox_driver::{EventContext, SandboxSpec as DriverSpec};
|
||||
use sandbox_driver::{EventContext, SandboxSource, SandboxSpec as DriverSpec};
|
||||
|
||||
use crate::driver::ProviderAccess;
|
||||
use crate::driver_sandbox::{LayoutSource, RunSandbox, local_sandbox_with_events};
|
||||
use crate::driver_sandbox::{LayoutSource, RunSandbox};
|
||||
use crate::environment::CloneRequest;
|
||||
use crate::{clone_source, provider_sandbox};
|
||||
|
||||
/// Options for sandbox initialization and construction.
|
||||
/// A run's sandbox on any provider fabro can name: a bundled kind in
|
||||
/// process or a sandbox-driver plugin. What the environment asked for, and
|
||||
/// how the repository is cloned into it.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SandboxSpec {
|
||||
Local {
|
||||
working_directory: PathBuf,
|
||||
},
|
||||
/// A sandbox on any provider fabro can name: a bundled kind in process
|
||||
/// or a sandbox-driver plugin.
|
||||
Provider(Box<ProviderSandboxSpec>),
|
||||
}
|
||||
|
||||
/// A run's sandbox on a provider: what the environment asked for and how
|
||||
/// the repository is cloned into it.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ProviderSandboxSpec {
|
||||
pub struct SandboxSpec {
|
||||
pub kind: SandboxProviderKind,
|
||||
/// The provider settings and vault credentials the kind needs.
|
||||
pub access: ProviderAccess,
|
||||
|
|
@ -38,144 +28,114 @@ pub struct ProviderSandboxSpec {
|
|||
}
|
||||
|
||||
impl SandboxSpec {
|
||||
pub fn provider(&self) -> SandboxProviderKind {
|
||||
match self {
|
||||
Self::Local { .. } => SandboxProviderKind::LOCAL,
|
||||
Self::Provider(spec) => spec.kind.clone(),
|
||||
/// A sandbox on this host at `working_directory`, the fabro `local`
|
||||
/// kind. The directory is designated: the sandbox uses it in place,
|
||||
/// never removes it, and clones nothing into it. The Host provider has
|
||||
/// no image, labels, or lifecycle timers, so the spec names only the
|
||||
/// directory.
|
||||
#[must_use]
|
||||
pub fn local(working_directory: impl Into<PathBuf>, access: ProviderAccess) -> Self {
|
||||
Self {
|
||||
kind: SandboxProviderKind::LOCAL,
|
||||
access,
|
||||
spec: DriverSpec::new(SandboxSource::HostDirectory)
|
||||
.working_directory(working_directory.into().display().to_string()),
|
||||
clone: CloneRequest::none(),
|
||||
github_app: None,
|
||||
run_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn provider(&self) -> SandboxProviderKind {
|
||||
self.kind.clone()
|
||||
}
|
||||
|
||||
pub fn provider_name(&self) -> String {
|
||||
self.provider().to_string()
|
||||
self.kind.to_string()
|
||||
}
|
||||
|
||||
/// The directory the spec designates on the provider, when it names one.
|
||||
#[must_use]
|
||||
pub fn working_directory(&self) -> Option<&str> {
|
||||
self.spec.working_directory.as_deref()
|
||||
}
|
||||
|
||||
/// The image the run record names for this sandbox: the environment's,
|
||||
/// or the provider's default when the environment names none. A local
|
||||
/// sandbox has no image.
|
||||
/// or the provider's default when the environment names none.
|
||||
pub fn image(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::Local { .. } => None,
|
||||
Self::Provider(spec) => provider_sandbox::recorded_image(&spec.kind, &spec.spec),
|
||||
}
|
||||
provider_sandbox::recorded_image(&self.kind, &self.spec)
|
||||
}
|
||||
|
||||
/// Build initialized sandbox metadata for persistence.
|
||||
pub fn to_run_sandbox_instance(&self, sandbox: &RunSandbox) -> RunSandboxInstance {
|
||||
let working_directory = sandbox.working_directory().to_string();
|
||||
let id = sandbox.sandbox_info();
|
||||
|
||||
match self {
|
||||
Self::Provider(spec) => {
|
||||
let ProviderSandboxSpec {
|
||||
kind, spec, clone, ..
|
||||
} = spec.as_ref();
|
||||
let clone_origin_url = &clone.origin_url;
|
||||
let repo_cloned =
|
||||
clone_source::repo_cloned_for_record(clone.skip, clone_origin_url.as_deref());
|
||||
// A fixed layout is known before the sandbox exists; a
|
||||
// provider-chosen one only from the sandbox.
|
||||
let layout = match provider_sandbox::layout_source(kind) {
|
||||
LayoutSource::Fixed(fixed) => {
|
||||
let repo = runtime_layout_metadata(
|
||||
repo_cloned,
|
||||
clone_origin_url.as_deref(),
|
||||
&fixed.workspace_root,
|
||||
&fixed.repos_root,
|
||||
);
|
||||
Some(crate::SandboxWorkspaceLayout {
|
||||
workspace_root: fixed.workspace_root,
|
||||
repos_root: fixed.repos_root,
|
||||
primary_repo_path: repo
|
||||
.as_ref()
|
||||
.map(|layout| layout.primary_repo_path.clone()),
|
||||
primary_repo_link: repo
|
||||
.as_ref()
|
||||
.map(|layout| layout.primary_repo_link.clone()),
|
||||
})
|
||||
}
|
||||
LayoutSource::ProviderWorkingDirectory => sandbox.workspace_layout(),
|
||||
};
|
||||
RunSandboxInstance {
|
||||
provider: kind.clone(),
|
||||
image: provider_sandbox::recorded_image(kind, spec),
|
||||
snapshot: sandbox.snapshot_info(),
|
||||
runtime: RunSandboxRuntime {
|
||||
id,
|
||||
working_directory,
|
||||
repo_cloned,
|
||||
clone_origin_url: clone_source::clean_clone_origin_for_record(
|
||||
clone_origin_url.as_deref(),
|
||||
),
|
||||
clone_branch: clone.branch.clone(),
|
||||
workspace_root: layout.as_ref().map(|layout| layout.workspace_root.clone()),
|
||||
repos_root: layout.as_ref().map(|layout| layout.repos_root.clone()),
|
||||
primary_repo_path: layout
|
||||
.as_ref()
|
||||
.and_then(|layout| layout.primary_repo_path.clone()),
|
||||
primary_repo_link: layout
|
||||
.as_ref()
|
||||
.and_then(|layout| layout.primary_repo_link.clone()),
|
||||
},
|
||||
}
|
||||
let clone_origin_url = &self.clone.origin_url;
|
||||
let repo_cloned =
|
||||
clone_source::repo_cloned_for_record(self.clone.skip, clone_origin_url.as_deref());
|
||||
// A fixed layout is known before the sandbox exists; a
|
||||
// provider-chosen one only from the sandbox.
|
||||
let layout = match provider_sandbox::layout_source(&self.kind) {
|
||||
LayoutSource::Fixed(fixed) => {
|
||||
let repo = runtime_layout_metadata(
|
||||
repo_cloned,
|
||||
clone_origin_url.as_deref(),
|
||||
&fixed.workspace_root,
|
||||
&fixed.repos_root,
|
||||
);
|
||||
Some(crate::SandboxWorkspaceLayout {
|
||||
workspace_root: fixed.workspace_root,
|
||||
repos_root: fixed.repos_root,
|
||||
primary_repo_path: repo.as_ref().map(|layout| layout.primary_repo_path.clone()),
|
||||
primary_repo_link: repo.as_ref().map(|layout| layout.primary_repo_link.clone()),
|
||||
})
|
||||
}
|
||||
Self::Local { .. } => RunSandboxInstance {
|
||||
provider: self.provider(),
|
||||
image: None,
|
||||
snapshot: None,
|
||||
runtime: RunSandboxRuntime {
|
||||
id,
|
||||
working_directory,
|
||||
repo_cloned: Some(false),
|
||||
clone_origin_url: None,
|
||||
clone_branch: None,
|
||||
workspace_root: None,
|
||||
repos_root: None,
|
||||
primary_repo_path: None,
|
||||
primary_repo_link: None,
|
||||
},
|
||||
LayoutSource::ProviderWorkingDirectory => sandbox.workspace_layout(),
|
||||
};
|
||||
RunSandboxInstance {
|
||||
provider: self.kind.clone(),
|
||||
image: self.image(),
|
||||
snapshot: sandbox.snapshot_info(),
|
||||
runtime: RunSandboxRuntime {
|
||||
id,
|
||||
working_directory,
|
||||
repo_cloned,
|
||||
clone_origin_url: clone_source::clean_clone_origin_for_record(
|
||||
clone_origin_url.as_deref(),
|
||||
),
|
||||
clone_branch: self.clone.branch.clone(),
|
||||
workspace_root: layout.as_ref().map(|layout| layout.workspace_root.clone()),
|
||||
repos_root: layout.as_ref().map(|layout| layout.repos_root.clone()),
|
||||
primary_repo_path: layout
|
||||
.as_ref()
|
||||
.and_then(|layout| layout.primary_repo_path.clone()),
|
||||
primary_repo_link: layout
|
||||
.as_ref()
|
||||
.and_then(|layout| layout.primary_repo_link.clone()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the sandbox. The driver reports its lifecycle through
|
||||
/// `events`: the local sandbox's from creation here, a provider
|
||||
/// sandbox's from `initialize` on.
|
||||
/// Builds the sandbox; `initialize` creates it on the provider. The
|
||||
/// driver reports its lifecycle through `events` from then on.
|
||||
pub async fn build(
|
||||
&self,
|
||||
events: Option<EventContext>,
|
||||
) -> Result<Arc<RunSandbox>, anyhow::Error> {
|
||||
match self {
|
||||
Self::Local { working_directory } => {
|
||||
let sandbox = local_sandbox_with_events(working_directory.clone(), events)
|
||||
.await
|
||||
.context("Failed to create local sandbox")?;
|
||||
Ok(Arc::new(sandbox))
|
||||
}
|
||||
Self::Provider(spec) => {
|
||||
let ProviderSandboxSpec {
|
||||
kind,
|
||||
access,
|
||||
spec,
|
||||
clone,
|
||||
github_app,
|
||||
run_id,
|
||||
} = spec.as_ref();
|
||||
let mut sandbox = provider_sandbox::provider_sandbox(
|
||||
kind.clone(),
|
||||
access,
|
||||
spec.clone(),
|
||||
clone,
|
||||
github_app.as_ref(),
|
||||
*run_id,
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("Failed to create {kind} sandbox"))?;
|
||||
if let Some(events) = events {
|
||||
sandbox.set_events(events);
|
||||
}
|
||||
Ok(Arc::new(sandbox))
|
||||
}
|
||||
let mut sandbox = provider_sandbox::provider_sandbox(
|
||||
self.kind.clone(),
|
||||
&self.access,
|
||||
self.spec.clone(),
|
||||
&self.clone,
|
||||
self.github_app.as_ref(),
|
||||
self.run_id,
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("Failed to create {} sandbox", self.kind))?;
|
||||
if let Some(events) = events {
|
||||
sandbox.set_events(events);
|
||||
}
|
||||
Ok(Arc::new(sandbox))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -193,13 +153,12 @@ fn runtime_layout_metadata(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use sandbox_driver::SandboxSource;
|
||||
use sandbox_driver_testing::ScriptedSandbox;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn provider_spec(clone: CloneRequest) -> ProviderSandboxSpec {
|
||||
ProviderSandboxSpec {
|
||||
fn docker_spec(clone: CloneRequest) -> SandboxSpec {
|
||||
SandboxSpec {
|
||||
kind: SandboxProviderKind::DOCKER,
|
||||
access: ProviderAccess::default(),
|
||||
spec: DriverSpec::new(SandboxSource::HostDirectory),
|
||||
|
|
@ -209,9 +168,9 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn sandbox_at(working_dir: &str) -> RunSandbox {
|
||||
fn sandbox_at(kind: SandboxProviderKind, working_dir: &str) -> RunSandbox {
|
||||
RunSandbox::new(
|
||||
SandboxProviderKind::DOCKER,
|
||||
kind,
|
||||
Arc::new(ScriptedSandbox::with_id_and_working_dir(
|
||||
"scripted-1",
|
||||
working_dir,
|
||||
|
|
@ -221,12 +180,12 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn docker_run_sandbox_persists_layout_metadata_for_cloned_repo() {
|
||||
let spec = SandboxSpec::Provider(Box::new(provider_spec(CloneRequest {
|
||||
let spec = docker_spec(CloneRequest {
|
||||
origin_url: Some("git@github.com:brynary/rack-test.git".to_string()),
|
||||
branch: Some("main".to_string()),
|
||||
..CloneRequest::default()
|
||||
})));
|
||||
let sandbox = sandbox_at("/workspace/rack-test");
|
||||
});
|
||||
let sandbox = sandbox_at(SandboxProviderKind::DOCKER, "/workspace/rack-test");
|
||||
|
||||
let record = spec.to_run_sandbox_instance(&sandbox);
|
||||
let runtime = record.runtime;
|
||||
|
|
@ -253,12 +212,12 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn invalid_exact_checkout_spec_fails_before_provider_connection() {
|
||||
let spec = SandboxSpec::Provider(Box::new(provider_spec(CloneRequest {
|
||||
let spec = docker_spec(CloneRequest {
|
||||
origin_url: Some("https://github.com/acme/widgets".to_string()),
|
||||
branch: Some("main".to_string()),
|
||||
commit_sha: Some("not-a-sha".to_string()),
|
||||
..CloneRequest::default()
|
||||
})));
|
||||
});
|
||||
|
||||
let error = spec
|
||||
.build(None)
|
||||
|
|
@ -276,11 +235,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn docker_run_sandbox_omits_primary_repo_metadata_for_empty_workspace() {
|
||||
let spec = SandboxSpec::Provider(Box::new(provider_spec(CloneRequest {
|
||||
let spec = docker_spec(CloneRequest {
|
||||
origin_url: Some("https://gitlab.com/acme/widgets".to_string()),
|
||||
..CloneRequest::none()
|
||||
})));
|
||||
let sandbox = sandbox_at("/workspace");
|
||||
});
|
||||
let sandbox = sandbox_at(SandboxProviderKind::DOCKER, "/workspace");
|
||||
|
||||
let record = spec.to_run_sandbox_instance(&sandbox);
|
||||
let runtime = record.runtime;
|
||||
|
|
@ -292,4 +251,34 @@ mod tests {
|
|||
assert!(runtime.primary_repo_path.is_none());
|
||||
assert!(runtime.primary_repo_link.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_spec_designates_the_directory_and_clones_nothing() {
|
||||
let spec = SandboxSpec::local("/home/dev/project", ProviderAccess::default());
|
||||
|
||||
assert_eq!(spec.kind, SandboxProviderKind::LOCAL);
|
||||
assert_eq!(spec.working_directory(), Some("/home/dev/project"));
|
||||
assert!(spec.clone.skip);
|
||||
assert_eq!(spec.clone.origin_url, None);
|
||||
assert_eq!(spec.image(), None);
|
||||
assert!(matches!(spec.spec.source, SandboxSource::HostDirectory));
|
||||
|
||||
let sandbox = sandbox_at(SandboxProviderKind::LOCAL, "/home/dev/project");
|
||||
let record = spec.to_run_sandbox_instance(&sandbox);
|
||||
|
||||
assert_eq!(record.provider, SandboxProviderKind::LOCAL);
|
||||
assert_eq!(record.image, None);
|
||||
assert_eq!(record.snapshot, None);
|
||||
assert_eq!(record.runtime.id, "scripted-1");
|
||||
assert_eq!(record.runtime.working_directory, "/home/dev/project");
|
||||
assert_eq!(record.runtime.repo_cloned, Some(false));
|
||||
assert_eq!(record.runtime.clone_origin_url, None);
|
||||
assert_eq!(record.runtime.clone_branch, None);
|
||||
assert_eq!(
|
||||
record.runtime.workspace_root.as_deref(),
|
||||
Some("/home/dev/project")
|
||||
);
|
||||
assert!(record.runtime.primary_repo_path.is_none());
|
||||
assert!(record.runtime.primary_repo_link.is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
//! fabro's exec policy, down to the scripted driver.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::time::Duration;
|
||||
|
||||
|
|
@ -17,6 +18,7 @@ use fabro_types::SandboxProviderKind;
|
|||
use sandbox_driver::{
|
||||
ExecResult, GrepMatch, PlatformInfo, SandboxState, StderrTail, Termination, WalkedFile,
|
||||
};
|
||||
use sandbox_driver_host::HostProvider;
|
||||
pub use sandbox_driver_testing::{
|
||||
ScriptedExec, ScriptedProvider, ScriptedSandbox, ScriptedStdioProcess,
|
||||
};
|
||||
|
|
@ -27,6 +29,22 @@ use crate::driver_sandbox::RunSandbox;
|
|||
use crate::managed_labels::{MANAGED_LABEL, MANAGED_LABEL_VALUE};
|
||||
use crate::sandbox::SandboxFile;
|
||||
|
||||
/// The id a run record carries for a local sandbox at `working_directory`,
|
||||
/// as the Host provider derives it from the canonical path. A record a test
|
||||
/// writes by hand reconnects the way one fabro wrote would. The directory
|
||||
/// must exist.
|
||||
pub async fn local_sandbox_id(working_directory: &Path) -> String {
|
||||
HostProvider::directory_id(working_directory)
|
||||
.await
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"no local sandbox id for {}: the directory must exist",
|
||||
working_directory.display()
|
||||
)
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// A driver [`ExecResult`] with the given streams, for scripting a mock
|
||||
/// sandbox's answers.
|
||||
#[must_use]
|
||||
|
|
|
|||
|
|
@ -10,8 +10,7 @@ use fabro_llm::credentials::readiness;
|
|||
use fabro_llm::lithos_catalog::Catalog;
|
||||
use fabro_mcp::config::McpServerSettings;
|
||||
use fabro_sandbox::{
|
||||
CloneRequest, DaytonaCredentials, ProviderAccess, ProviderSandboxSpec, SandboxSpec,
|
||||
sandbox_spec_for_environment,
|
||||
CloneRequest, DaytonaCredentials, ProviderAccess, SandboxSpec, sandbox_spec_for_environment,
|
||||
};
|
||||
use fabro_static::EnvVars;
|
||||
#[cfg(test)]
|
||||
|
|
@ -506,10 +505,17 @@ impl RunSession {
|
|||
)));
|
||||
}
|
||||
}
|
||||
let daytona = vault_guard
|
||||
.get(EnvVars::DAYTONA_API_KEY)
|
||||
.map(|api_key| DaytonaCredentials::from_api_key(api_key.to_string(), process_env_var));
|
||||
let access = ProviderAccess {
|
||||
providers: services.sandbox_providers.clone(),
|
||||
daytona,
|
||||
};
|
||||
let sandbox = match sandbox_provider.bundled() {
|
||||
Some(BundledProvider::Local) if dry_run_clone_target => SandboxSpec::Local {
|
||||
working_directory: dry_run_workspace_for_target(persisted).await?,
|
||||
},
|
||||
Some(BundledProvider::Local) if dry_run_clone_target => {
|
||||
SandboxSpec::local(dry_run_workspace_for_target(persisted).await?, access)
|
||||
}
|
||||
Some(BundledProvider::Local) => match record.target.as_ref() {
|
||||
Some(target @ (RunTarget::Git(_) | RunTarget::None {})) => {
|
||||
return Err(Error::engine(format!(
|
||||
|
|
@ -517,9 +523,10 @@ impl RunSession {
|
|||
target.kind_name()
|
||||
)));
|
||||
}
|
||||
Some(RunTarget::Folder { path }) => SandboxSpec::Local {
|
||||
working_directory: folder_working_directory_from_record(record, path).await?,
|
||||
},
|
||||
Some(RunTarget::Folder { path }) => SandboxSpec::local(
|
||||
folder_working_directory_from_record(record, path).await?,
|
||||
access,
|
||||
),
|
||||
None => {
|
||||
let working_directory = resolved
|
||||
.environment
|
||||
|
|
@ -530,17 +537,10 @@ impl RunSession {
|
|||
err,
|
||||
)
|
||||
})?;
|
||||
SandboxSpec::Local { working_directory }
|
||||
SandboxSpec::local(working_directory, access)
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
let daytona = vault_guard.get(EnvVars::DAYTONA_API_KEY).map(|api_key| {
|
||||
DaytonaCredentials::from_api_key(api_key.to_string(), process_env_var)
|
||||
});
|
||||
let access = ProviderAccess {
|
||||
providers: services.sandbox_providers.clone(),
|
||||
daytona,
|
||||
};
|
||||
let spec = resolve_sandbox_spec(resolved, secret_lookup)?;
|
||||
let mut clone = CloneRequest::from_settings(&resolved.clone);
|
||||
clone.skip |= clone_source.skip_clone;
|
||||
|
|
@ -548,14 +548,14 @@ impl RunSession {
|
|||
clone.branch = clone_source.branch;
|
||||
clone.tag = clone_source.tag;
|
||||
clone.commit_sha = clone_source.commit_sha;
|
||||
SandboxSpec::Provider(Box::new(ProviderSandboxSpec {
|
||||
SandboxSpec {
|
||||
kind: sandbox_provider.clone(),
|
||||
access,
|
||||
spec,
|
||||
clone,
|
||||
github_app: services.github_app.clone(),
|
||||
run_id: Some(record.run_id),
|
||||
}))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1875,10 +1875,7 @@ mod tests {
|
|||
assert_eq!(runtime.clone_branch, None);
|
||||
assert_eq!(runtime.primary_repo_path, None);
|
||||
assert_eq!(runtime.primary_repo_link, None);
|
||||
let SandboxSpec::Provider(spec) = sandbox else {
|
||||
panic!("none target should retain the selected Docker provider");
|
||||
};
|
||||
let ProviderSandboxSpec { kind, clone, .. } = *spec;
|
||||
let SandboxSpec { kind, clone, .. } = sandbox;
|
||||
assert_eq!(kind, SandboxProviderKind::DOCKER);
|
||||
assert!(clone.skip);
|
||||
assert_eq!(clone.origin_url, None);
|
||||
|
|
@ -1937,15 +1934,12 @@ mod tests {
|
|||
assert_eq!(runtime.clone_branch, None);
|
||||
assert_eq!(runtime.primary_repo_path, None);
|
||||
assert_eq!(runtime.primary_repo_link, None);
|
||||
let SandboxSpec::Provider(spec) = sandbox else {
|
||||
panic!("none target should retain the selected Daytona provider");
|
||||
};
|
||||
let ProviderSandboxSpec {
|
||||
let SandboxSpec {
|
||||
kind,
|
||||
access,
|
||||
clone,
|
||||
..
|
||||
} = *spec;
|
||||
} = sandbox;
|
||||
assert_eq!(kind, SandboxProviderKind::DAYTONA);
|
||||
assert!(access.daytona.is_some(), "the vault key reaches the spec");
|
||||
assert!(clone.skip);
|
||||
|
|
@ -2024,12 +2018,20 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let SandboxSpec::Local { working_directory } = session.sandbox else {
|
||||
panic!("clone target dry-run should execute in a Local scratch sandbox");
|
||||
};
|
||||
assert_eq!(
|
||||
working_directory,
|
||||
run_dir.join("dry-run-workspace").canonicalize().unwrap()
|
||||
session.sandbox.kind,
|
||||
SandboxProviderKind::LOCAL,
|
||||
"clone target dry-run should execute in a Local scratch sandbox"
|
||||
);
|
||||
assert_eq!(
|
||||
session.sandbox.working_directory().map(Path::new),
|
||||
Some(
|
||||
run_dir
|
||||
.join("dry-run-workspace")
|
||||
.canonicalize()
|
||||
.unwrap()
|
||||
.as_path()
|
||||
)
|
||||
);
|
||||
assert_eq!(session.sandbox_env.origin_url, None);
|
||||
assert_eq!(session.pr_origin_url, None);
|
||||
|
|
@ -2138,11 +2140,14 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let SandboxSpec::Local { working_directory } = session.sandbox else {
|
||||
panic!("folder target should retain the selected Local provider");
|
||||
};
|
||||
assert_eq!(working_directory, canonical_folder);
|
||||
assert_ne!(working_directory, environment_cwd);
|
||||
assert_eq!(
|
||||
session.sandbox.kind,
|
||||
SandboxProviderKind::LOCAL,
|
||||
"folder target should retain the selected Local provider"
|
||||
);
|
||||
let working_directory = session.sandbox.working_directory().map(Path::new);
|
||||
assert_eq!(working_directory, Some(canonical_folder.as_path()));
|
||||
assert_ne!(working_directory, Some(environment_cwd.as_path()));
|
||||
assert_eq!(session.sandbox_env.origin_url.as_deref(), Some(origin_url));
|
||||
assert_eq!(session.pr_origin_url.as_deref(), Some(origin_url));
|
||||
}
|
||||
|
|
@ -2207,10 +2212,15 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let SandboxSpec::Local { working_directory } = session.sandbox else {
|
||||
panic!("legacy Local run should retain the selected Local provider");
|
||||
};
|
||||
assert_eq!(working_directory, environment_cwd);
|
||||
assert_eq!(
|
||||
session.sandbox.kind,
|
||||
SandboxProviderKind::LOCAL,
|
||||
"legacy Local run should retain the selected Local provider"
|
||||
);
|
||||
assert_eq!(
|
||||
session.sandbox.working_directory().map(Path::new),
|
||||
Some(environment_cwd.as_path())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ use fabro_auth::test_support as auth_test_support;
|
|||
use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
|
||||
use fabro_hooks::HookSettings;
|
||||
use fabro_interview::AutoApproveInterviewer;
|
||||
use fabro_sandbox::SandboxSpec;
|
||||
use fabro_sandbox::test_support::MockSandbox;
|
||||
use fabro_sandbox::test_support::{MockSandbox, local_sandbox_id};
|
||||
use fabro_sandbox::{ProviderAccess, SandboxSpec};
|
||||
use fabro_store::Database;
|
||||
use fabro_types::settings::run::RunModelControls;
|
||||
use fabro_types::{
|
||||
|
|
@ -262,9 +262,10 @@ async fn execute_test_run_with_options(
|
|||
run_store: run_store.into(),
|
||||
dry_run: false,
|
||||
emitter: emitter.clone(),
|
||||
sandbox: SandboxSpec::Local {
|
||||
working_directory: std::env::current_dir().unwrap(),
|
||||
},
|
||||
sandbox: SandboxSpec::local(
|
||||
std::env::current_dir().unwrap(),
|
||||
ProviderAccess::default(),
|
||||
),
|
||||
llm: LlmSpec {
|
||||
model: "test-model".to_string(),
|
||||
provider_id: lithos_llm::catalog::builtin::anthropic(),
|
||||
|
|
@ -324,9 +325,10 @@ async fn execute_runs_start_to_exit_and_returns_final_context() {
|
|||
run_store: run_store.into(),
|
||||
dry_run: false,
|
||||
emitter: test_emitter_arc("run-test"),
|
||||
sandbox: SandboxSpec::Local {
|
||||
working_directory: std::env::current_dir().unwrap(),
|
||||
},
|
||||
sandbox: SandboxSpec::local(
|
||||
std::env::current_dir().unwrap(),
|
||||
ProviderAccess::default(),
|
||||
),
|
||||
llm: LlmSpec {
|
||||
model: "test-model".to_string(),
|
||||
provider_id: lithos_llm::catalog::builtin::anthropic(),
|
||||
|
|
@ -411,10 +413,11 @@ async fn resumed_in_flight_node_starts_a_new_stage_execution() {
|
|||
let run_store = test_run_store(&run_id).await;
|
||||
seed_created_and_starting(&run_store, &run_options, &graph).await;
|
||||
// Resume reconnects to the previously recorded sandbox.
|
||||
let working_directory = std::env::current_dir().unwrap();
|
||||
append_event(&run_store, &run_id, &Event::SandboxInitialized {
|
||||
working_directory: std::env::current_dir().unwrap().display().to_string(),
|
||||
working_directory: working_directory.display().to_string(),
|
||||
provider: fabro_types::SandboxProviderKind::LOCAL,
|
||||
id: "local".to_string(),
|
||||
id: local_sandbox_id(&working_directory).await,
|
||||
image: None,
|
||||
snapshot: None,
|
||||
repo_cloned: None,
|
||||
|
|
@ -467,9 +470,10 @@ async fn resumed_in_flight_node_starts_a_new_stage_execution() {
|
|||
run_store: run_store.into(),
|
||||
dry_run: false,
|
||||
emitter: emitter.clone(),
|
||||
sandbox: SandboxSpec::Local {
|
||||
working_directory: std::env::current_dir().unwrap(),
|
||||
},
|
||||
sandbox: SandboxSpec::local(
|
||||
std::env::current_dir().unwrap(),
|
||||
ProviderAccess::default(),
|
||||
),
|
||||
llm: LlmSpec {
|
||||
model: "test-model".to_string(),
|
||||
provider_id: lithos_llm::catalog::builtin::anthropic(),
|
||||
|
|
@ -583,9 +587,7 @@ async fn run_with_lifecycle(
|
|||
run_store: run_store.into(),
|
||||
dry_run: false,
|
||||
emitter: emitter.clone(),
|
||||
sandbox: SandboxSpec::Local {
|
||||
working_directory: PathBuf::from(sandbox.working_directory()),
|
||||
},
|
||||
sandbox: SandboxSpec::local(sandbox.working_directory(), ProviderAccess::default()),
|
||||
llm: LlmSpec {
|
||||
model: "test-model".to_string(),
|
||||
provider_id: lithos_llm::catalog::builtin::anthropic(),
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ use fabro_hooks::{HookContext, HookDecision, HookEvent, HookExecutionContext, Ho
|
|||
use fabro_llm::credentials::{CredentialProvider, readiness};
|
||||
use fabro_llm::lithos_catalog::Catalog;
|
||||
use fabro_sandbox::{
|
||||
DaytonaCredentials, ExecResultExt, GitSetupIntent, ProviderAccess, SandboxSpec,
|
||||
reconnect_for_run,
|
||||
DaytonaCredentials, ExecResultExt, GitSetupIntent, ProviderAccess, reconnect_for_run,
|
||||
};
|
||||
use fabro_static::EnvVars;
|
||||
use fabro_types::RunSandboxKind;
|
||||
|
|
@ -368,7 +367,7 @@ pub async fn initialize(
|
|||
.as_ref()
|
||||
.and_then(|git| git.sha.clone());
|
||||
if !is_resume
|
||||
&& !matches!(options.sandbox, SandboxSpec::Local { .. })
|
||||
&& !options.sandbox.kind.is_local()
|
||||
&& matches!(
|
||||
options
|
||||
.run_options
|
||||
|
|
@ -936,7 +935,7 @@ mod tests {
|
|||
run_store,
|
||||
dry_run: false,
|
||||
emitter: Arc::clone(&emitter),
|
||||
sandbox: SandboxSpec::Local { working_directory },
|
||||
sandbox: SandboxSpec::local(working_directory, ProviderAccess::default()),
|
||||
llm: LlmSpec {
|
||||
model: "test-model".to_string(),
|
||||
provider_id: lithos_llm::catalog::builtin::anthropic(),
|
||||
|
|
@ -1387,9 +1386,7 @@ mod tests {
|
|||
run_store: run_store.into(),
|
||||
dry_run: false,
|
||||
emitter: emitter.clone(),
|
||||
sandbox: SandboxSpec::Local {
|
||||
working_directory: temp.path().to_path_buf(),
|
||||
},
|
||||
sandbox: SandboxSpec::local(temp.path(), ProviderAccess::default()),
|
||||
llm: LlmSpec {
|
||||
model: "fake-acp".to_string(),
|
||||
provider_id: lithos_llm::catalog::builtin::openai(),
|
||||
|
|
@ -1492,9 +1489,10 @@ mod tests {
|
|||
run_store: run_store.into(),
|
||||
dry_run: false,
|
||||
emitter: emitter.clone(),
|
||||
sandbox: SandboxSpec::Local {
|
||||
working_directory: std::env::current_dir().unwrap(),
|
||||
},
|
||||
sandbox: SandboxSpec::local(
|
||||
std::env::current_dir().unwrap(),
|
||||
ProviderAccess::default(),
|
||||
),
|
||||
llm: LlmSpec {
|
||||
model: "test-model".to_string(),
|
||||
provider_id: lithos_llm::catalog::builtin::anthropic(),
|
||||
|
|
@ -1636,9 +1634,10 @@ mod tests {
|
|||
},
|
||||
dry_run: false,
|
||||
emitter: emitter.clone(),
|
||||
sandbox: SandboxSpec::Local {
|
||||
working_directory: std::env::current_dir().unwrap(),
|
||||
},
|
||||
sandbox: SandboxSpec::local(
|
||||
std::env::current_dir().unwrap(),
|
||||
ProviderAccess::default(),
|
||||
),
|
||||
llm: LlmSpec {
|
||||
model: "test-model".to_string(),
|
||||
provider_id: lithos_llm::catalog::builtin::anthropic(),
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
)]
|
||||
|
||||
use fabro_sandbox::reconnect::reconnect_for_run;
|
||||
use fabro_sandbox::test_support::local_sandbox_id;
|
||||
use fabro_sandbox::{CloneRequest, ProviderAccess, provider_sandbox};
|
||||
use fabro_types::{RunSandboxInstance, RunSandboxRuntime, SandboxProviderKind};
|
||||
use sandbox_driver::{SandboxSource, SandboxSpec};
|
||||
|
|
@ -25,13 +26,13 @@ const DOCKER_CP_IMAGE: &str = "buildpack-deps:noble";
|
|||
// Local sandbox
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn local_record(working_directory: &std::path::Path) -> RunSandboxInstance {
|
||||
async fn local_record(working_directory: &std::path::Path) -> RunSandboxInstance {
|
||||
RunSandboxInstance {
|
||||
provider: SandboxProviderKind::LOCAL,
|
||||
image: None,
|
||||
snapshot: None,
|
||||
runtime: RunSandboxRuntime {
|
||||
id: "local:test".to_string(),
|
||||
id: local_sandbox_id(working_directory).await,
|
||||
working_directory: working_directory.to_string_lossy().to_string(),
|
||||
repo_cloned: None,
|
||||
clone_origin_url: None,
|
||||
|
|
@ -49,7 +50,7 @@ async fn local_cp_upload_download_round_trip() {
|
|||
let sandbox_dir = tempfile::tempdir().unwrap();
|
||||
let scratch = tempfile::tempdir().unwrap();
|
||||
|
||||
let record = local_record(sandbox_dir.path());
|
||||
let record = local_record(sandbox_dir.path()).await;
|
||||
let sandbox = reconnect_for_run(&record, &ProviderAccess::default(), None, None)
|
||||
.await
|
||||
.expect("reconnect local");
|
||||
|
|
@ -82,7 +83,7 @@ async fn local_cp_binary_round_trip() {
|
|||
let sandbox_dir = tempfile::tempdir().unwrap();
|
||||
let scratch = tempfile::tempdir().unwrap();
|
||||
|
||||
let record = local_record(sandbox_dir.path());
|
||||
let record = local_record(sandbox_dir.path()).await;
|
||||
let sandbox = reconnect_for_run(&record, &ProviderAccess::default(), None, None)
|
||||
.await
|
||||
.expect("reconnect local");
|
||||
|
|
@ -111,7 +112,7 @@ async fn local_cp_creates_parent_dirs() {
|
|||
let sandbox_dir = tempfile::tempdir().unwrap();
|
||||
let scratch = tempfile::tempdir().unwrap();
|
||||
|
||||
let record = local_record(sandbox_dir.path());
|
||||
let record = local_record(sandbox_dir.path()).await;
|
||||
let sandbox = reconnect_for_run(&record, &ProviderAccess::default(), None, None)
|
||||
.await
|
||||
.expect("reconnect local");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue