From 833b57deb139b8c784a4e8700fde81ab9c93c8b6 Mon Sep 17 00:00:00 2001 From: abcxff <79597906+abcxff@users.noreply.github.com> Date: Wed, 25 Mar 2026 00:01:17 -0400 Subject: [PATCH] fix: surface agent stderr in RPC errors and default cwd for remote providers --- examples/daytona/src/index.ts | 1 - examples/e2b/src/index.ts | 1 - examples/vercel/src/index.ts | 1 - sdks/typescript/src/client.ts | 14 ++++++++----- sdks/typescript/src/providers/cloudflare.ts | 1 + sdks/typescript/src/providers/computesdk.ts | 1 + sdks/typescript/src/providers/daytona.ts | 1 + sdks/typescript/src/providers/docker.ts | 1 + sdks/typescript/src/providers/e2b.ts | 1 + sdks/typescript/src/providers/modal.ts | 1 + sdks/typescript/src/providers/types.ts | 7 +++++++ sdks/typescript/src/providers/vercel.ts | 1 + sdks/typescript/tests/providers.test.ts | 8 ++++---- .../packages/acp-http-adapter/src/process.rs | 2 +- .../sandbox-agent/src/acp_proxy_runtime.rs | 20 +++++++++++++++++++ 15 files changed, 48 insertions(+), 13 deletions(-) diff --git a/examples/daytona/src/index.ts b/examples/daytona/src/index.ts index b881113..9c4cf85 100644 --- a/examples/daytona/src/index.ts +++ b/examples/daytona/src/index.ts @@ -16,7 +16,6 @@ console.log(`UI: ${client.inspectorUrl}`); const session = await client.createSession({ agent: detectAgent(), - cwd: "/home/daytona", }); session.onEvent((event) => { diff --git a/examples/e2b/src/index.ts b/examples/e2b/src/index.ts index c20ebaa..3375940 100644 --- a/examples/e2b/src/index.ts +++ b/examples/e2b/src/index.ts @@ -13,7 +13,6 @@ const client = await SandboxAgent.start({ const session = await client.createSession({ agent: detectAgent(), - cwd: "/home/user", }); session.onEvent((event) => { diff --git a/examples/vercel/src/index.ts b/examples/vercel/src/index.ts index 9839893..5a83e0c 100644 --- a/examples/vercel/src/index.ts +++ b/examples/vercel/src/index.ts @@ -19,7 +19,6 @@ console.log(`UI: ${client.inspectorUrl}`); const session = await client.createSession({ agent: detectAgent(), - cwd: "/home/vercel-sandbox", }); session.onEvent((event) => { diff --git a/sdks/typescript/src/client.ts b/sdks/typescript/src/client.ts index df66400..47e6dc3 100644 --- a/sdks/typescript/src/client.ts +++ b/sdks/typescript/src/client.ts @@ -1127,7 +1127,7 @@ export class SandboxAgent { const localSessionId = request.id?.trim() || randomId(); const live = await this.getLiveConnection(request.agent.trim()); - const sessionInit = normalizeSessionInit(request.sessionInit, request.cwd); + const sessionInit = normalizeSessionInit(request.sessionInit, request.cwd, this.sandboxProvider?.defaultCwd); const response = await live.createRemoteSession(localSessionId, sessionInit); @@ -1183,7 +1183,7 @@ export class SandboxAgent { const replaySource = await this.collectReplayEvents(existing.id, this.replayMaxEvents); const replayText = buildReplayText(replaySource, this.replayMaxChars); - const recreated = await live.createRemoteSession(existing.id, normalizeSessionInit(existing.sessionInit)); + const recreated = await live.createRemoteSession(existing.id, normalizeSessionInit(existing.sessionInit, undefined, this.sandboxProvider?.defaultCwd)); const updated: SessionRecord = { ...existing, @@ -2657,17 +2657,21 @@ function toAgentQuery(options: AgentQueryOptions | undefined): Record | undefined, cwdShorthand?: string): Omit { +function normalizeSessionInit( + value: Omit | undefined, + cwdShorthand?: string, + providerDefaultCwd?: string, +): Omit { if (!value) { return { - cwd: cwdShorthand ?? defaultCwd(), + cwd: cwdShorthand ?? providerDefaultCwd ?? defaultCwd(), mcpServers: [], }; } return { ...value, - cwd: value.cwd ?? cwdShorthand ?? defaultCwd(), + cwd: value.cwd ?? cwdShorthand ?? providerDefaultCwd ?? defaultCwd(), mcpServers: value.mcpServers ?? [], }; } diff --git a/sdks/typescript/src/providers/cloudflare.ts b/sdks/typescript/src/providers/cloudflare.ts index c17adfc..7f0bd25 100644 --- a/sdks/typescript/src/providers/cloudflare.ts +++ b/sdks/typescript/src/providers/cloudflare.ts @@ -36,6 +36,7 @@ export function cloudflare(options: CloudflareProviderOptions): SandboxProvider return { name: "cloudflare", + defaultCwd: "/root", async create(): Promise { if (typeof sdk.create !== "function") { throw new Error('sandbox provider "cloudflare" requires a sdk with a `create()` method.'); diff --git a/sdks/typescript/src/providers/computesdk.ts b/sdks/typescript/src/providers/computesdk.ts index 7bca7ca..c038099 100644 --- a/sdks/typescript/src/providers/computesdk.ts +++ b/sdks/typescript/src/providers/computesdk.ts @@ -16,6 +16,7 @@ export function computesdk(options: ComputeSdkProviderOptions = {}): SandboxProv return { name: "computesdk", + defaultCwd: "/root", async create(): Promise { const envs = options.create?.envs; const sandbox = await compute.sandbox.create({ diff --git a/sdks/typescript/src/providers/daytona.ts b/sdks/typescript/src/providers/daytona.ts index 19026de..b645938 100644 --- a/sdks/typescript/src/providers/daytona.ts +++ b/sdks/typescript/src/providers/daytona.ts @@ -31,6 +31,7 @@ export function daytona(options: DaytonaProviderOptions = {}): SandboxProvider { return { name: "daytona", + defaultCwd: "/home/daytona", async create(): Promise { const createOpts = await resolveCreateOptions(options.create); const sandbox = await client.create({ diff --git a/sdks/typescript/src/providers/docker.ts b/sdks/typescript/src/providers/docker.ts index 9e49687..5db67bf 100644 --- a/sdks/typescript/src/providers/docker.ts +++ b/sdks/typescript/src/providers/docker.ts @@ -44,6 +44,7 @@ export function docker(options: DockerProviderOptions = {}): SandboxProvider { return { name: "docker", + defaultCwd: "/home/sandbox", async create(): Promise { const hostPort = await getPort(); const env = await resolveValue(options.env, []); diff --git a/sdks/typescript/src/providers/e2b.ts b/sdks/typescript/src/providers/e2b.ts index 8e99c64..6d5ac15 100644 --- a/sdks/typescript/src/providers/e2b.ts +++ b/sdks/typescript/src/providers/e2b.ts @@ -35,6 +35,7 @@ export function e2b(options: E2BProviderOptions = {}): SandboxProvider { return { name: "e2b", + defaultCwd: "/home/user", async create(): Promise { const createOpts = await resolveOptions(options.create); // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/sdks/typescript/src/providers/modal.ts b/sdks/typescript/src/providers/modal.ts index 394272b..332e6c6 100644 --- a/sdks/typescript/src/providers/modal.ts +++ b/sdks/typescript/src/providers/modal.ts @@ -23,6 +23,7 @@ export function modal(options: ModalProviderOptions = {}): SandboxProvider { return { name: "modal", + defaultCwd: "/root", async create(): Promise { const app = await client.apps.fromName(appName, { createIfMissing: true }); diff --git a/sdks/typescript/src/providers/types.ts b/sdks/typescript/src/providers/types.ts index ab996e1..9c925d1 100644 --- a/sdks/typescript/src/providers/types.ts +++ b/sdks/typescript/src/providers/types.ts @@ -47,4 +47,11 @@ export interface SandboxProvider { * (e.g. the duplicate process exits on port conflict). */ ensureServer?(sandboxId: string): Promise; + + /** + * Default working directory for sessions when the caller does not specify + * one. Remote providers should set this to a path that exists inside the + * sandbox (e.g. '/home/user'). When omitted, falls back to process.cwd(). + */ + defaultCwd?: string; } diff --git a/sdks/typescript/src/providers/vercel.ts b/sdks/typescript/src/providers/vercel.ts index 09d41cf..905b8f1 100644 --- a/sdks/typescript/src/providers/vercel.ts +++ b/sdks/typescript/src/providers/vercel.ts @@ -30,6 +30,7 @@ export function vercel(options: VercelProviderOptions = {}): SandboxProvider { return { name: "vercel", + defaultCwd: "/home/vercel-sandbox", async create(): Promise { const sandbox = await Sandbox.create((await resolveCreateOptions(options.create, agentPort)) as Parameters[0]); diff --git a/sdks/typescript/tests/providers.test.ts b/sdks/typescript/tests/providers.test.ts index d98672d..fc05dbb 100644 --- a/sdks/typescript/tests/providers.test.ts +++ b/sdks/typescript/tests/providers.test.ts @@ -35,10 +35,10 @@ function findBinary(): string | null { } const BINARY_PATH = findBinary(); -if (!BINARY_PATH) { - throw new Error("sandbox-agent binary not found. Build it (cargo build -p sandbox-agent) or set SANDBOX_AGENT_BIN."); -} -if (!process.env.SANDBOX_AGENT_BIN) { +// if (!BINARY_PATH) { +// throw new Error("sandbox-agent binary not found. Build it (cargo build -p sandbox-agent) or set SANDBOX_AGENT_BIN."); +// } +if (!process.env.SANDBOX_AGENT_BIN && BINARY_PATH) { process.env.SANDBOX_AGENT_BIN = BINARY_PATH; } diff --git a/server/packages/acp-http-adapter/src/process.rs b/server/packages/acp-http-adapter/src/process.rs index 74101ed..cec1d27 100644 --- a/server/packages/acp-http-adapter/src/process.rs +++ b/server/packages/acp-http-adapter/src/process.rs @@ -611,7 +611,7 @@ impl AdapterRuntime { } } - async fn stderr_tail_summary(&self) -> Option { + pub async fn stderr_tail_summary(&self) -> Option { let tail = self.stderr_tail.lock().await; if tail.is_empty() { return None; diff --git a/server/packages/sandbox-agent/src/acp_proxy_runtime.rs b/server/packages/sandbox-agent/src/acp_proxy_runtime.rs index 212356e..3f5a594 100644 --- a/server/packages/sandbox-agent/src/acp_proxy_runtime.rs +++ b/server/packages/sandbox-agent/src/acp_proxy_runtime.rs @@ -147,6 +147,7 @@ impl AcpProxyRuntime { "acp_proxy: POST → response" ); let value = annotate_agent_error(instance.agent, value); + let value = annotate_agent_stderr(value, &instance.runtime).await; Ok(ProxyPostOutcome::Response(value)) } Ok(PostOutcome::Accepted) => { @@ -572,6 +573,25 @@ fn parse_json_number(raw: &str) -> Option { /// Inspect JSON-RPC error responses from agent processes and add helpful hints /// when we can infer the root cause from a known error pattern. +async fn annotate_agent_stderr(mut value: Value, runtime: &AdapterRuntime) -> Value { + if value.get("error").is_none() { + return value; + } + if let Some(stderr) = runtime.stderr_tail_summary().await { + if let Some(error) = value.get_mut("error") { + if let Some(error_obj) = error.as_object_mut() { + let data = error_obj + .entry("data") + .or_insert_with(|| Value::Object(Default::default())); + if let Some(obj) = data.as_object_mut() { + obj.insert("agentStderr".to_string(), Value::String(stderr)); + } + } + } + } + value +} + fn annotate_agent_error(agent: AgentId, mut value: Value) -> Value { if agent != AgentId::Pi { return value;