From 499d01681c4b0a8272c2fa3389cd8eda10d96a2c Mon Sep 17 00:00:00 2001 From: dinq Date: Mon, 5 Oct 2026 10:45:36 -0400 Subject: [PATCH] fix: ship the codemode sandbox in pie-pi-process --- packages/server/build-pi-process.ts | 42 ++++--- .../server/test/harness/pi/rpc-bundle.test.ts | 106 ++++++++++++++++++ ...arendil-works__pi-coding-agent@1.0.2.patch | 21 ++++ pnpm-lock.yaml | 18 +-- 4 files changed, 160 insertions(+), 27 deletions(-) diff --git a/packages/server/build-pi-process.ts b/packages/server/build-pi-process.ts index 55f5226cb..19b8a82dd 100644 --- a/packages/server/build-pi-process.ts +++ b/packages/server/build-pi-process.ts @@ -46,24 +46,30 @@ NodeRuntime.runMain( yield* Effect.die(new Error(`Building pi-process failed: ${piProcessExit}`)); } - const workerExit = yield* spawner.exitCode( - ChildProcess.make( - process.execPath, - [ - "build", - path.join(piDir, "dist/utils/image-resize-worker.js"), - "--target", - "bun", - "--define", - "PI_BUNDLED_NODE=true", - "--outfile", - path.join(outDir, "image-resize-worker.js"), - ], - { stdout: "inherit", stderr: "inherit" }, - ), - ); - if (workerExit !== 0) { - yield* Effect.die(new Error(`Building image-resize-worker failed: ${workerExit}`)); + // Pi resolves bundled workers beside the bundle (PI_BUNDLED_NODE). + for (const [entry, outfile] of [ + ["dist/utils/image-resize-worker.js", "image-resize-worker.js"], + ["dist/extensions/codemode/worker.js", "codemode-worker.js"], + ] as const) { + const workerExit = yield* spawner.exitCode( + ChildProcess.make( + process.execPath, + [ + "build", + path.join(piDir, entry), + "--target", + "bun", + "--define", + "PI_BUNDLED_NODE=true", + "--outfile", + path.join(outDir, outfile), + ], + { stdout: "inherit", stderr: "inherit" }, + ), + ); + if (workerExit !== 0) { + yield* Effect.die(new Error(`Building ${outfile} failed: ${workerExit}`)); + } } // Keep Pi's package layout so getPackageDir(), documentation and HTML export diff --git a/packages/server/test/harness/pi/rpc-bundle.test.ts b/packages/server/test/harness/pi/rpc-bundle.test.ts index d18cedc47..b6e2364cb 100644 --- a/packages/server/test/harness/pi/rpc-bundle.test.ts +++ b/packages/server/test/harness/pi/rpc-bundle.test.ts @@ -244,6 +244,112 @@ layer(NodeServices.layer, { excludeTestServices: true })("Pi RPC bundle", (it) = ); } + it.effect("runs codemode scripts in the bundled sandbox outside the bundle directory", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "pie-rpc-codemode-" }); + const runtimeDir = path.join(cwd, "runtime"); + yield* fs.copy(path.dirname(bundle), runtimeDir); + yield* fs.copy(builtIsland, path.join(cwd, "fff")); + yield* fs.makeDirectory(path.join(cwd, "extensions")); + // codemode is registered inactive; MCP activates it in practice. + yield* fs.writeFileString( + path.join(cwd, "settings.json"), + JSON.stringify({ defaultTools: ["read", "codemode"] }), + ); + // Calls codemode on the first request, then answers with text. + yield* fs.writeFileString( + path.join(cwd, "extensions/codemode-provider.ts"), + `import { createAssistantMessageEventStream } from "@earendil-works/pi-ai"; + export default function (pi) { + pi.registerProvider("codemode-test", { + baseUrl: "http://127.0.0.1:1", + apiKey: "test-only", + api: "codemode-test", + models: [{ id: "model", name: "Codemode test", reasoning: false, input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200000, maxTokens: 4096 }], + streamSimple(model, context) { + const stream = createAssistantMessageEventStream(); + const call = context.messages.at(-1)?.role === "user"; + const output = { role: "assistant", content: [], api: model.api, + provider: model.provider, model: model.id, + usage: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0, totalTokens: 2, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }, + stopReason: call ? "toolUse" : "stop", timestamp: Date.now() }; + stream.push({ type: "start", partial: output }); + if (call) { + const toolCall = { type: "toolCall", id: "call-1", name: "codemode", + arguments: { code: "return 6 * 7" } }; + output.content.push(toolCall); + stream.push({ type: "toolcall_start", contentIndex: 0, partial: output }); + stream.push({ type: "toolcall_end", contentIndex: 0, toolCall, partial: output }); + } else { + output.content.push({ type: "text", text: "done" }); + stream.push({ type: "text_start", contentIndex: 0, partial: output }); + stream.push({ type: "text_end", contentIndex: 0, content: "done", partial: output }); + } + stream.push({ type: "done", reason: output.stopReason, message: output }); + stream.end(); + return stream; + }, + }); + }`, + ); + const turnEnded = yield* Deferred.make(); + const child = yield* spawner.spawn( + ChildProcess.make( + "bun", + [ + "--no-install", + path.join(runtimeDir, "pi-process.js"), + "--mode", + "rpc", + "--provider", + "codemode-test", + "--model", + "model", + ], + { + cwd, + env: { PATH: process.env.PATH, HOME: cwd, PI_CODING_AGENT_DIR: cwd, PI_OFFLINE: "1" }, + // Keep stdin open until the turn finishes; EOF shuts the RPC child down. + stdin: Stream.make('{"type":"prompt","message":"run"}\n').pipe( + Stream.concat(Stream.fromEffect(Deferred.await(turnEnded)).pipe(Stream.drain)), + Stream.encodeText, + ), + }, + ), + ); + const lines = yield* child.stdout.pipe( + Stream.decodeText(), + Stream.splitLines, + Stream.tap((line) => + line.includes('"type":"agent_end"') + ? Deferred.succeed(turnEnded, undefined) + : Effect.void, + ), + Stream.takeUntil((line) => line.includes('"type":"agent_end"')), + Stream.runCollect, + ); + const result = Array.from(lines) + .map( + (line) => + JSON.parse(line) as { + type?: string; + toolName?: string; + isError?: boolean; + result?: { content: Array<{ text?: string }> }; + }, + ) + .find((event) => event.type === "tool_execution_end" && event.toolName === "codemode"); + assert.ok(result, lines.join("\n")); + assert.equal(result.isError, false, JSON.stringify(result)); + assert.equal(result.result?.content.at(-1)?.text, "42"); + }), + ); + it.effect("routes queued RPC input through extension input handlers", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; diff --git a/patches/@earendil-works__pi-coding-agent@1.0.2.patch b/patches/@earendil-works__pi-coding-agent@1.0.2.patch index c723394f3..5579db722 100644 --- a/patches/@earendil-works__pi-coding-agent@1.0.2.patch +++ b/patches/@earendil-works__pi-coding-agent@1.0.2.patch @@ -1,3 +1,24 @@ +diff --git a/dist/bun/runtime-setup.js b/dist/bun/runtime-setup.js +index 282e8d3d54897a7c032616542596be5221937d73..718f7067e5432179faaec935fca6971f9a7dde0a 100644 +--- a/dist/bun/runtime-setup.js ++++ b/dist/bun/runtime-setup.js +@@ -1,6 +1,7 @@ + import { bedrockProviderModule } from "@earendil-works/pi-ai/bedrock-provider"; + import { registerBunOAuthFlows } from "@earendil-works/pi-ai/bun-oauth"; + import { setBedrockProviderModule } from "@earendil-works/pi-ai/compat"; ++import { fileURLToPath } from "node:url"; + // Bun loads .wasm imports as files: embedded in compiled executables, evaluating to a readable path. + import quickjsWasmPath from "quickjs-wasi/quickjs.wasm"; + import { APP_NAME, setEmbeddedQuickJSWasmPath } from "../config.js"; +@@ -8,5 +9,6 @@ process.title = APP_NAME; + process.emitWarning = (() => { }); + registerBunOAuthFlows(); + setBedrockProviderModule(bedrockProviderModule); +-setEmbeddedQuickJSWasmPath(quickjsWasmPath); ++// A non-compiled `bun build --outdir` bundle gets a path relative to the bundle, not the cwd. ++setEmbeddedQuickJSWasmPath(quickjsWasmPath.startsWith(".") ? fileURLToPath(new URL(quickjsWasmPath, import.meta.url)) : quickjsWasmPath); + //# sourceMappingURL=runtime-setup.js.map +\ No newline at end of file diff --git a/dist/core/agent-session.js b/dist/core/agent-session.js index 33338d518743ba2fd4ee2d710c5496a6ddcd1856..5cafd057a0fae8632caa4fae539917f199005500 100644 --- a/dist/core/agent-session.js diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fcb6533e2..760882400 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -300,7 +300,7 @@ overrides: packageExtensionsChecksum: sha256-krQ8w+xxiZ9Mgc7ennDQqwconiHv6V6eTOTbkuDELx4= patchedDependencies: - '@earendil-works/pi-coding-agent@1.0.2': 445289b980f1ab50ac4cbac660f890b2e4db530b655b32289ce0d65d31adc151 + '@earendil-works/pi-coding-agent@1.0.2': eab4da72ab2caaf074fd07981377bfb8ff912baef828cb4b4c2d29eb91288032 '@shadcn/lint@0.1.0': 273b110768f4b3df665b11a0b5b87b5814ecc513382db2cc9647529606554bbf react-scan@0.5.7: 40dc21f57d476ff8623ca33e24df589d1e7ce31695648f2f80a72a3359a76b65 @@ -655,7 +655,7 @@ importers: dependencies: '@earendil-works/pi-coding-agent': specifier: catalog:pi - version: 1.0.2(patch_hash=445289b980f1ab50ac4cbac660f890b2e4db530b655b32289ce0d65d31adc151)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.6.5))(@smithy/signature-v4@5.7.3)(supports-color@10.2.2)(ws@8.21.3)(zod@4.6.5) + version: 1.0.2(patch_hash=eab4da72ab2caaf074fd07981377bfb8ff912baef828cb4b4c2d29eb91288032)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.6.5))(@smithy/signature-v4@5.7.3)(supports-color@10.2.2)(ws@8.21.3)(zod@4.6.5) '@orpc/contract': specifier: catalog:orpc version: 2.0.0-beta.35 @@ -727,7 +727,7 @@ importers: devDependencies: '@earendil-works/pi-coding-agent': specifier: catalog:pi - version: 1.0.2(patch_hash=445289b980f1ab50ac4cbac660f890b2e4db530b655b32289ce0d65d31adc151)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.6.5))(@smithy/signature-v4@5.7.3)(supports-color@10.2.2)(ws@8.21.3)(zod@4.6.5) + version: 1.0.2(patch_hash=eab4da72ab2caaf074fd07981377bfb8ff912baef828cb4b4c2d29eb91288032)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.6.5))(@smithy/signature-v4@5.7.3)(supports-color@10.2.2)(ws@8.21.3)(zod@4.6.5) '@getpie/typescript': specifier: workspace:* version: link:../../tools/typescript @@ -749,7 +749,7 @@ importers: devDependencies: '@earendil-works/pi-coding-agent': specifier: catalog:pi - version: 1.0.2(patch_hash=445289b980f1ab50ac4cbac660f890b2e4db530b655b32289ce0d65d31adc151)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.6.5))(@smithy/signature-v4@5.7.3)(supports-color@10.2.2)(ws@8.21.3)(zod@4.6.5) + version: 1.0.2(patch_hash=eab4da72ab2caaf074fd07981377bfb8ff912baef828cb4b4c2d29eb91288032)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.6.5))(@smithy/signature-v4@5.7.3)(supports-color@10.2.2)(ws@8.21.3)(zod@4.6.5) '@effect/platform-node': specifier: 4.0.0-rc.115 version: 4.0.0-rc.115(effect@4.0.0-rc.115)(redis@6.2.1(@opentelemetry/api@1.9.1)) @@ -809,13 +809,13 @@ importers: dependencies: '@earendil-works/pi-coding-agent': specifier: catalog:pi - version: 1.0.2(patch_hash=445289b980f1ab50ac4cbac660f890b2e4db530b655b32289ce0d65d31adc151)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.6.5))(@smithy/signature-v4@5.7.3)(supports-color@10.2.2)(ws@8.21.3)(zod@4.6.5) + version: 1.0.2(patch_hash=eab4da72ab2caaf074fd07981377bfb8ff912baef828cb4b4c2d29eb91288032)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.6.5))(@smithy/signature-v4@5.7.3)(supports-color@10.2.2)(ws@8.21.3)(zod@4.6.5) '@effect/platform-node': specifier: 4.0.0-rc.115 version: 4.0.0-rc.115(effect@4.0.0-rc.115)(redis@6.2.1(@opentelemetry/api@1.9.1)) '@ff-labs/pi-fff': specifier: 0.10.6 - version: 0.10.6(@earendil-works/pi-coding-agent@1.0.2(patch_hash=445289b980f1ab50ac4cbac660f890b2e4db530b655b32289ce0d65d31adc151)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.6.5))(@smithy/signature-v4@5.7.3)(supports-color@10.2.2)(ws@8.21.3)(zod@4.6.5))(@earendil-works/pi-tui@1.0.2)(@sinclair/typebox@0.34.52) + version: 0.10.6(@earendil-works/pi-coding-agent@1.0.2(patch_hash=eab4da72ab2caaf074fd07981377bfb8ff912baef828cb4b4c2d29eb91288032)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.6.5))(@smithy/signature-v4@5.7.3)(supports-color@10.2.2)(ws@8.21.3)(zod@4.6.5))(@earendil-works/pi-tui@1.0.2)(@sinclair/typebox@0.34.52) '@getpie/contract': specifier: workspace:* version: link:../contract @@ -9762,7 +9762,7 @@ snapshots: dependencies: quickjs-wasi: 3.6.2 - '@earendil-works/pi-coding-agent@1.0.2(patch_hash=445289b980f1ab50ac4cbac660f890b2e4db530b655b32289ce0d65d31adc151)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.6.5))(@smithy/signature-v4@5.7.3)(supports-color@10.2.2)(ws@8.21.3)(zod@4.6.5)': + '@earendil-works/pi-coding-agent@1.0.2(patch_hash=eab4da72ab2caaf074fd07981377bfb8ff912baef828cb4b4c2d29eb91288032)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.6.5))(@smithy/signature-v4@5.7.3)(supports-color@10.2.2)(ws@8.21.3)(zod@4.6.5)': dependencies: '@earendil-works/chord': 1.0.2 '@earendil-works/pi-agent-core': 1.0.2(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.6.5))(@smithy/signature-v4@5.7.3)(supports-color@10.2.2)(undici@8.10.2)(ws@8.21.3)(zod@4.6.5) @@ -10273,9 +10273,9 @@ snapshots: '@ff-labs/fff-bin-win32-arm64': 0.10.6 '@ff-labs/fff-bin-win32-x64': 0.10.6 - '@ff-labs/pi-fff@0.10.6(@earendil-works/pi-coding-agent@1.0.2(patch_hash=445289b980f1ab50ac4cbac660f890b2e4db530b655b32289ce0d65d31adc151)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.6.5))(@smithy/signature-v4@5.7.3)(supports-color@10.2.2)(ws@8.21.3)(zod@4.6.5))(@earendil-works/pi-tui@1.0.2)(@sinclair/typebox@0.34.52)': + '@ff-labs/pi-fff@0.10.6(@earendil-works/pi-coding-agent@1.0.2(patch_hash=eab4da72ab2caaf074fd07981377bfb8ff912baef828cb4b4c2d29eb91288032)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.6.5))(@smithy/signature-v4@5.7.3)(supports-color@10.2.2)(ws@8.21.3)(zod@4.6.5))(@earendil-works/pi-tui@1.0.2)(@sinclair/typebox@0.34.52)': dependencies: - '@earendil-works/pi-coding-agent': 1.0.2(patch_hash=445289b980f1ab50ac4cbac660f890b2e4db530b655b32289ce0d65d31adc151)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.6.5))(@smithy/signature-v4@5.7.3)(supports-color@10.2.2)(ws@8.21.3)(zod@4.6.5) + '@earendil-works/pi-coding-agent': 1.0.2(patch_hash=eab4da72ab2caaf074fd07981377bfb8ff912baef828cb4b4c2d29eb91288032)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.6.5))(@smithy/signature-v4@5.7.3)(supports-color@10.2.2)(ws@8.21.3)(zod@4.6.5) '@earendil-works/pi-tui': 1.0.2 '@ff-labs/fff-bun': 0.10.6 '@ff-labs/fff-node': 0.10.6