Merge pull request #860 from fabro-sh/pebble-port-routes

Pin pebble main 6996942 and implement its PortRoutes trait
This commit is contained in:
Bryan Helmkamp 2026-09-13 10:35:03 -04:00 • committed by GitHub
commit 6cd4bb417b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 67 additions and 34 deletions

7
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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());

View file

@ -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<Self>) -> Option<Arc<dyn PreviewUrls>> {
pub fn port_routes(self: &Arc<Self>) -> Option<Arc<dyn PortRoutes>> {
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<RunSandbox>);
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<PreviewUrl> {
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<PortRoute, PortRouteError> {
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));
}
}