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 6eb79b7..67b74dc 100644 --- a/examples/e2b/src/index.ts +++ b/examples/e2b/src/index.ts @@ -14,7 +14,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 8cbef53..3ebb0da 100644 --- a/sdks/typescript/src/providers/computesdk.ts +++ b/sdks/typescript/src/providers/computesdk.ts @@ -21,6 +21,7 @@ export function computesdk(options: ComputeSdkProviderOptions = {}): SandboxProv return { name: "computesdk", + defaultCwd: "/root", async create(): Promise { const createOpts = await resolveCreateOptions(options.create); const sandbox = await compute.sandbox.create({ diff --git a/sdks/typescript/src/providers/daytona.ts b/sdks/typescript/src/providers/daytona.ts index f614faf..18777c6 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 54d2e28..3f64d0d 100644 --- a/sdks/typescript/src/providers/e2b.ts +++ b/sdks/typescript/src/providers/e2b.ts @@ -42,6 +42,7 @@ export function e2b(options: E2BProviderOptions = {}): SandboxProvider { return { name: "e2b", + defaultCwd: "/home/user", async create(): Promise { const createOpts = await resolveOptions(options.create); const rawTemplate = typeof createOpts.template === "string" ? createOpts.template : undefined; diff --git a/sdks/typescript/src/providers/modal.ts b/sdks/typescript/src/providers/modal.ts index 4d5b39f..fad98c2 100644 --- a/sdks/typescript/src/providers/modal.ts +++ b/sdks/typescript/src/providers/modal.ts @@ -29,6 +29,7 @@ export function modal(options: ModalProviderOptions = {}): SandboxProvider { return { name: "modal", + defaultCwd: "/root", async create(): Promise { const createOpts = await resolveCreateOptions(options.create); const appName = createOpts.appName ?? DEFAULT_APP_NAME; 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;