diff --git a/Cargo.lock b/Cargo.lock index 11932050e..dfc9f6234 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5867,7 +5867,7 @@ dependencies = [ [[package]] name = "pebble-agent" version = "0.1.0" -source = "git+https://github.com/lithoscomputer/pebble?rev=430740f1114f859d6d173683f25b5007cf2023ca#430740f1114f859d6d173683f25b5007cf2023ca" +source = "git+https://github.com/lithoscomputer/pebble?rev=69969420c9017ca15ac6c175c820a0cb8090866a#69969420c9017ca15ac6c175c820a0cb8090866a" dependencies = [ "async-trait", "futures-util", @@ -5884,7 +5884,7 @@ dependencies = [ [[package]] name = "pebble-cli-core" version = "0.1.0" -source = "git+https://github.com/lithoscomputer/pebble?rev=430740f1114f859d6d173683f25b5007cf2023ca#430740f1114f859d6d173683f25b5007cf2023ca" +source = "git+https://github.com/lithoscomputer/pebble?rev=69969420c9017ca15ac6c175c820a0cb8090866a#69969420c9017ca15ac6c175c820a0cb8090866a" dependencies = [ "anyhow", "async-trait", @@ -5913,7 +5913,7 @@ dependencies = [ [[package]] name = "pebble-coding-agent" version = "0.1.0" -source = "git+https://github.com/lithoscomputer/pebble?rev=430740f1114f859d6d173683f25b5007cf2023ca#430740f1114f859d6d173683f25b5007cf2023ca" +source = "git+https://github.com/lithoscomputer/pebble?rev=69969420c9017ca15ac6c175c820a0cb8090866a#69969420c9017ca15ac6c175c820a0cb8090866a" dependencies = [ "async-trait", "futures-util", @@ -5922,7 +5922,6 @@ dependencies = [ "reqwest 0.13.4", "rmcp", "rustix", - "sandbox-driver", "serde", "serde_json", "sha2 0.10.9", diff --git a/Cargo.toml b/Cargo.toml index e65b3f529..03289e8f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,14 +118,13 @@ sandbox-driver-daytona-config = { git = "https://github.com/lithoscomputer/sandb sandbox-driver-testing = { git = "https://github.com/lithoscomputer/sandbox-driver", rev = "a92c0db6b6a122ca9b6df75de6615544f53c0d47" } # pebble: the coding agent loop fabro runs its agent stages, Ask Fabro # sessions, hook evaluators, and `fabro exec` on. Pinned by rev to pebble -# `main`. The `PreviewUrls` trait objects fabro hands pebble's MCP servers -# only cross when fabro and pebble's `mcp` feature name the same -# sandbox-driver revision, so move the two pins together. Pebble pins the -# same lithos-llm rev as fabro, and its lockfile policy is that every shared -# crate resolves to the version lithos-llm locks. -pebble-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "430740f1114f859d6d173683f25b5007cf2023ca" } -pebble-coding-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "430740f1114f859d6d173683f25b5007cf2023ca", features = ["mcp", "search-providers"] } -pebble-cli-core = { git = "https://github.com/lithoscomputer/pebble", rev = "430740f1114f859d6d173683f25b5007cf2023ca" } +# `main`. Pebble owns the `PortRoutes` trait fabro implements over its run +# sandbox, so the pebble and sandbox-driver pins move independently. Pebble +# pins the same lithos-llm rev as fabro, and its lockfile policy is that +# every shared crate resolves to the version lithos-llm locks. +pebble-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "69969420c9017ca15ac6c175c820a0cb8090866a" } +pebble-coding-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "69969420c9017ca15ac6c175c820a0cb8090866a", features = ["mcp", "search-providers"] } +pebble-cli-core = { git = "https://github.com/lithoscomputer/pebble", rev = "69969420c9017ca15ac6c175c820a0cb8090866a" } sentry = { version = "0.35", default-features = false, features = ["backtrace", "contexts", "ureq", "rustls"] } fork = "0.2" exec = "0.3" diff --git a/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs b/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs index 0717e6a73..94c6ce88c 100644 --- a/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs +++ b/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs @@ -780,6 +780,8 @@ mod tests { summary_token_estimate: 500, tracked_file_count: 3, reason: CompactionReason::Threshold, + usage: TokenUsage::default(), + cost_usd_micros: None, }), ); assert!(ui.stage.active_stages["s1"].compaction_bar.is_none()); diff --git a/lib/components/fabro-sandbox/src/driver_sandbox.rs b/lib/components/fabro-sandbox/src/driver_sandbox.rs index 1bab7144d..5e774f791 100644 --- a/lib/components/fabro-sandbox/src/driver_sandbox.rs +++ b/lib/components/fabro-sandbox/src/driver_sandbox.rs @@ -20,11 +20,12 @@ use fabro_github::GitHubCredentials; use fabro_github::token_source::TokenSnapshot; use fabro_types::SandboxProviderKind; use fabro_util::workspace_glob::WorkspaceGlob; +use pebble_coding_agent::mcp::{PortRoute, PortRouteError, PortRoutes}; use sandbox_driver::{ - Capability, DirEntry, EventContext, ExecControls, ExecResult, ExecSpec, ExecStreamingResult, - FileKind, GitRetryPolicy, GrepMatch, GrepOptions, PreviewUrl, PreviewUrls, PtyOptions, - PtySession, PtySize, Sandbox as DriverHandle, SandboxProvider as DriverProvider, - SandboxSpec as DriverSpec, SandboxState, Search as _, StdioProcess, WaitOptions, WalkOptions, + DirEntry, EventContext, ExecControls, ExecResult, ExecSpec, ExecStreamingResult, FileKind, + GitRetryPolicy, GrepMatch, GrepOptions, PreviewUrls, PtyOptions, PtySession, PtySize, + Sandbox as DriverHandle, SandboxProvider as DriverProvider, SandboxSpec as DriverSpec, + SandboxState, Search as _, StdioProcess, WaitOptions, WalkOptions, }; use tokio::sync::OnceCell; use tokio_util::sync::CancellationToken; @@ -998,11 +999,12 @@ impl RunSandbox { } /// The route from fabro to a port inside the sandbox, as pebble's MCP - /// support takes it: the driver's preview URLs, when the provider has - /// them. `None` for a provider without forwarding, which is where pebble - /// reaches the port on the loopback address instead. + /// support takes it: pebble's [`PortRoutes`] over the driver's preview + /// URLs, when the provider has them. `None` for a provider without + /// forwarding, which is where pebble reaches the port on the loopback + /// address instead. #[must_use] - pub fn port_routes(self: &Arc) -> Option> { + pub fn port_routes(self: &Arc) -> Option> { self.handle().ok()?.preview_urls()?; Some(Arc::new(SandboxPortRoutes(Arc::clone(self)))) } @@ -1025,34 +1027,48 @@ impl RunSandbox { } } -/// [`PreviewUrls`] over a run sandbox's driver handle, for pebble. +/// Pebble's [`PortRoutes`] over a run sandbox's driver handle: the driver's +/// preview-URL facet answers with the URL and headers that reach a port. struct SandboxPortRoutes(Arc); impl SandboxPortRoutes { /// The driver's facet, present whenever [`RunSandbox::port_routes`] handed - /// this out: the handle is set once and never cleared. - fn facet(&self) -> Option<&dyn PreviewUrls> { + /// this out: the handle is set once and never cleared. A missing facet is + /// the environment routing to none of its ports. + fn facet(&self) -> Result<&dyn PreviewUrls, PortRouteError> { self.0 .handle() .ok() .and_then(|handle| handle.preview_urls()) + .ok_or(PortRouteError::Unsupported) } } #[async_trait::async_trait] -impl PreviewUrls for SandboxPortRoutes { - async fn preview_url(&self, port: u16) -> sandbox_driver::Result { - match self.facet() { - Some(facet) => facet.preview_url(port).await, - None => Err(sandbox_driver::Error::unsupported(Capability::PreviewUrls)), - } +impl PortRoutes for SandboxPortRoutes { + async fn route(&self, port: u16) -> Result { + let preview = self.facet()?.preview_url(port).await.map_err(|error| { + PortRouteError::failed_with_source( + format!("Failed to open a route to sandbox port {port}"), + error, + ) + })?; + Ok(PortRoute { + url: preview.url, + headers: preview.headers, + }) } - async fn release_preview_url(&self, port: u16) -> sandbox_driver::Result<()> { - match self.facet() { - Some(facet) => facet.release_preview_url(port).await, - None => Err(sandbox_driver::Error::unsupported(Capability::PreviewUrls)), - } + async fn release(&self, port: u16) -> Result<(), PortRouteError> { + self.facet()? + .release_preview_url(port) + .await + .map_err(|error| { + PortRouteError::failed_with_source( + format!("Failed to release the route to sandbox port {port}"), + error, + ) + }) } } @@ -1411,4 +1427,21 @@ mod tests { assert_eq!(url, "http://127.0.0.1:8080"); assert!(headers.is_empty()); } + + #[tokio::test] + async fn port_routes_answer_pebble_with_the_access_facets_preview_url() { + let Fixture { + dir, + _provider: provider, + sandbox, + } = fixture().await; + let sandbox = Arc::new(sandbox); + let routes = sandbox + .port_routes() + .expect("the host provider routes to its ports"); + let route = routes.route(8080).await.unwrap(); + assert_eq!(route, PortRoute::new("http://127.0.0.1:8080")); + routes.release(8080).await.unwrap(); + drop((dir, provider)); + } }