From 4645eba5169fa7a797075e85e7a1529d9aae4bcf Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:12:44 +0530 Subject: [PATCH 1/2] fix(pi): disable user extensions in sandboxed runs In read_only and allowed mode DevSpace loads a sandbox extension that replaces Pi's read/bash/edit/write/grep tools to enforce the workspace boundary. A user extension that overrides the same tools (for example pi-foldable-tools) made Pi exit on startup, and one that won would have bypassed the sandbox. Launch sandboxed runs with --no-extensions; the explicit --extension sandbox path still loads. full_access has no sandbox and keeps the user's extensions. Co-Authored-By: Claude Opus 5.5 --- src/local-agent-pi.test.ts | 17 +++++++++++++++ src/local-agent-pi.ts | 42 +++++++++++++++++++++++++++----------- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/src/local-agent-pi.test.ts b/src/local-agent-pi.test.ts index 6b9effd6c..6b03b8aeb 100644 --- a/src/local-agent-pi.test.ts +++ b/src/local-agent-pi.test.ts @@ -8,6 +8,7 @@ import { PiLocalAgentDriver, type PiRpcFactory, type PiRpcRuntimeConnection, + piLaunchArgs, } from "./local-agent-pi.js"; import { LocalAgentRuntimePool } from "./local-agent-runtime-pool.js"; import type { LocalAgentRuntimeContext } from "./local-agent-runtime.js"; @@ -396,3 +397,19 @@ test("a missing Pi command is unavailable", async () => { const missingCommand = await new PiLocalAgentDriver(undefined, { PATH: "", PI_COMMAND: "/missing/pi" }).createRuntime(context); assert.equal(missingCommand.isErr() && missingCommand.error.code, "PROVIDER_UNAVAILABLE"); }); + +test("sandboxed Pi runs disable user extensions but keep the DevSpace sandbox", () => { + assert.deepEqual(piLaunchArgs(["cli.js"], "read_only", "/tmp/sandbox.ts"), [ + "cli.js", "--mode", "rpc", "--approve", + "--no-extensions", "--extension", "/tmp/sandbox.ts", + "--tools", "read,grep,find,ls", + ]); + assert.deepEqual(piLaunchArgs([], "allowed", "/tmp/sandbox.ts"), [ + "--mode", "rpc", "--approve", + "--no-extensions", "--extension", "/tmp/sandbox.ts", + "--tools", "read,grep,find,ls,edit,write,bash", + ]); + // full_access has no sandbox, so the user's extensions stay enabled. + assert.deepEqual(piLaunchArgs([], "full_access", undefined), ["--mode", "rpc", "--approve"]); + assert.throws(() => piLaunchArgs([], "allowed", undefined), /sandbox extension/); +}); diff --git a/src/local-agent-pi.ts b/src/local-agent-pi.ts index cf076f51a..a17b81b92 100644 --- a/src/local-agent-pi.ts +++ b/src/local-agent-pi.ts @@ -28,6 +28,7 @@ import type { LocalAgentRunResult, LocalAgentRuntime, LocalAgentRuntimeContext, + LocalAgentWriteMode, } from "./local-agent-runtime.js"; const PI_READ_ONLY_TOOLS = ["read", "grep", "find", "ls"] as const; @@ -327,6 +328,34 @@ export class PiLocalAgentDriver implements LocalAgentDriver { } } +/** + * In read_only and allowed mode DevSpace's sandbox extension replaces Pi's file and shell tools to enforce the + * workspace boundary, so user extensions are disabled: one that also overrides those tools stops Pi from starting, + * and one that won would bypass the sandbox. Explicit `--extension` paths still load. full_access has no sandbox and + * keeps the user's extensions. + */ +export function piLaunchArgs( + launchArgs: readonly string[], + writeMode: LocalAgentWriteMode, + sandboxPath: string | undefined, +): string[] { + if (writeMode === "full_access") return [...launchArgs, "--mode", "rpc", "--approve"]; + + if (!sandboxPath) throw new Error(`Pi ${writeMode} runs require the DevSpace sandbox extension.`); + + return [ + ...launchArgs, + "--mode", + "rpc", + "--approve", + "--no-extensions", + "--extension", + sandboxPath, + "--tools", + (writeMode === "read_only" ? PI_READ_ONLY_TOOLS : PI_ALLOWED_TOOLS).join(","), + ]; +} + async function defaultPiRpcFactory( context: LocalAgentRuntimeContext, env: NodeJS.ProcessEnv, @@ -353,18 +382,7 @@ async function defaultPiRpcFactory( ? undefined : await materializePiSandboxExtension(context.workspaceRoot, writeMode); - const args = [ - ...launch.args, - "--mode", - "rpc", - "--approve", - ...(writeMode === "full_access" ? [] : ["--extension", sandbox!.path]), - ...(writeMode === "read_only" - ? ["--tools", PI_READ_ONLY_TOOLS.join(",")] - : writeMode === "allowed" - ? ["--tools", PI_ALLOWED_TOOLS.join(",")] - : []), - ]; + const args = piLaunchArgs(launch.args, writeMode, sandbox?.path); try { return { From 17c71adc108aa56f4cfbf464d349fdd3d0887a79 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:36:35 +0530 Subject: [PATCH 2/2] test(pi): record --no-extensions in sandboxed replay transcripts Co-Authored-By: Claude Opus 5.5 --- test/agents-replay/transcripts/pi/continue.jsonl | 2 +- test/agents-replay/transcripts/pi/events.jsonl | 2 +- test/agents-replay/transcripts/pi/read-only.jsonl | 2 +- test/agents-replay/transcripts/pi/run-wait.jsonl | 2 +- test/agents-replay/transcripts/pi/stop.jsonl | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/agents-replay/transcripts/pi/continue.jsonl b/test/agents-replay/transcripts/pi/continue.jsonl index 22c466550..0bb8c711b 100644 --- a/test/agents-replay/transcripts/pi/continue.jsonl +++ b/test/agents-replay/transcripts/pi/continue.jsonl @@ -1,4 +1,4 @@ -{"type":"meta","provider":"pi","scenario":"continue","args":["--mode","rpc","--approve","--extension","$TMP/devspace-pi-Ayylsj/devspace-sandbox.ts","--tools","read,grep,find,ls,edit,write,bash"],"version":"1.4.0","recordedAt":"2026-10-26T07:11:15.420Z"} +{"type":"meta","provider":"pi","scenario":"continue","args":["--mode","rpc","--approve","--no-extensions","--extension","$TMP/devspace-pi-Ayylsj/devspace-sandbox.ts","--tools","read,grep,find,ls,edit,write,bash"],"version":"1.4.0","recordedAt":"2026-10-26T07:11:15.420Z"} {"dir":"in","ms":240,"frame":{"type":"new_session","id":"devspace-1"}} {"dir":"out","ms":1265,"frame":{"id":"devspace-1","type":"response","command":"new_session","success":true,"data":{"cancelled":false}}} {"dir":"in","ms":1266,"frame":{"type":"get_state","id":"devspace-2"}} diff --git a/test/agents-replay/transcripts/pi/events.jsonl b/test/agents-replay/transcripts/pi/events.jsonl index e79ba31f5..52cdf4900 100644 --- a/test/agents-replay/transcripts/pi/events.jsonl +++ b/test/agents-replay/transcripts/pi/events.jsonl @@ -1,4 +1,4 @@ -{"type":"meta","provider":"pi","scenario":"events","args":["--mode","rpc","--approve","--extension","$TMP/devspace-pi-5dV6hw/devspace-sandbox.ts","--tools","read,grep,find,ls,edit,write,bash"],"version":"1.4.0","recordedAt":"2026-10-26T07:03:11.992Z"} +{"type":"meta","provider":"pi","scenario":"events","args":["--mode","rpc","--approve","--no-extensions","--extension","$TMP/devspace-pi-5dV6hw/devspace-sandbox.ts","--tools","read,grep,find,ls,edit,write,bash"],"version":"1.4.0","recordedAt":"2026-10-26T07:03:11.992Z"} {"dir":"in","ms":243,"frame":{"type":"new_session","id":"devspace-1"}} {"dir":"out","ms":1330,"frame":{"id":"devspace-1","type":"response","command":"new_session","success":true,"data":{"cancelled":false}}} {"dir":"in","ms":1330,"frame":{"type":"get_state","id":"devspace-2"}} diff --git a/test/agents-replay/transcripts/pi/read-only.jsonl b/test/agents-replay/transcripts/pi/read-only.jsonl index 596d3a2b2..90eba9446 100644 --- a/test/agents-replay/transcripts/pi/read-only.jsonl +++ b/test/agents-replay/transcripts/pi/read-only.jsonl @@ -1,4 +1,4 @@ -{"type":"meta","provider":"pi","scenario":"read-only","args":["--mode","rpc","--approve","--extension","$TMP/devspace-pi-rAyzWp/devspace-sandbox.ts","--tools","read,grep,find,ls"],"version":"1.4.0","recordedAt":"2026-10-26T07:02:31.178Z"} +{"type":"meta","provider":"pi","scenario":"read-only","args":["--mode","rpc","--approve","--no-extensions","--extension","$TMP/devspace-pi-rAyzWp/devspace-sandbox.ts","--tools","read,grep,find,ls"],"version":"1.4.0","recordedAt":"2026-10-26T07:02:31.178Z"} {"dir":"in","ms":245,"frame":{"type":"new_session","id":"devspace-1"}} {"dir":"out","ms":1292,"frame":{"id":"devspace-1","type":"response","command":"new_session","success":true,"data":{"cancelled":false}}} {"dir":"in","ms":1292,"frame":{"type":"get_state","id":"devspace-2"}} diff --git a/test/agents-replay/transcripts/pi/run-wait.jsonl b/test/agents-replay/transcripts/pi/run-wait.jsonl index 2b2bc7dea..be2cf09ee 100644 --- a/test/agents-replay/transcripts/pi/run-wait.jsonl +++ b/test/agents-replay/transcripts/pi/run-wait.jsonl @@ -1,4 +1,4 @@ -{"type":"meta","provider":"pi","scenario":"run-wait","args":["--mode","rpc","--approve","--extension","$TMP/devspace-pi-0rf9GL/devspace-sandbox.ts","--tools","read,grep,find,ls,edit,write,bash"],"version":"1.4.0","recordedAt":"2026-10-26T07:02:02.046Z"} +{"type":"meta","provider":"pi","scenario":"run-wait","args":["--mode","rpc","--approve","--no-extensions","--extension","$TMP/devspace-pi-0rf9GL/devspace-sandbox.ts","--tools","read,grep,find,ls,edit,write,bash"],"version":"1.4.0","recordedAt":"2026-10-26T07:02:02.046Z"} {"dir":"in","ms":281,"frame":{"type":"new_session","id":"devspace-1"}} {"dir":"out","ms":1577,"frame":{"id":"devspace-1","type":"response","command":"new_session","success":true,"data":{"cancelled":false}}} {"dir":"in","ms":1577,"frame":{"type":"get_state","id":"devspace-2"}} diff --git a/test/agents-replay/transcripts/pi/stop.jsonl b/test/agents-replay/transcripts/pi/stop.jsonl index 9754314a4..988937284 100644 --- a/test/agents-replay/transcripts/pi/stop.jsonl +++ b/test/agents-replay/transcripts/pi/stop.jsonl @@ -1,4 +1,4 @@ -{"type":"meta","provider":"pi","scenario":"stop","args":["--mode","rpc","--approve","--extension","$TMP/devspace-pi-MwbB60/devspace-sandbox.ts","--tools","read,grep,find,ls,edit,write,bash"],"version":"1.4.0","recordedAt":"2026-10-26T07:02:48.572Z"} +{"type":"meta","provider":"pi","scenario":"stop","args":["--mode","rpc","--approve","--no-extensions","--extension","$TMP/devspace-pi-MwbB60/devspace-sandbox.ts","--tools","read,grep,find,ls,edit,write,bash"],"version":"1.4.0","recordedAt":"2026-10-26T07:02:48.572Z"} {"dir":"in","ms":290,"frame":{"type":"new_session","id":"devspace-1"}} {"dir":"out","ms":1592,"frame":{"id":"devspace-1","type":"response","command":"new_session","success":true,"data":{"cancelled":false}}} {"dir":"in","ms":1593,"frame":{"type":"get_state","id":"devspace-2"}}