From cb6af7c2cca6e01d1acf0845f6802042bce29f41 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Tue, 5 May 2026 08:00:49 -0700 Subject: [PATCH] Stage stdin to a temp file so the e2b sandbox executor delivers it reliably (#5278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - The e2b sandbox provider implements `onEnvironmentExecute` so adapters can spawn CLIs in an e2b sandbox > - For commands that need stdin (e.g. piping a hello prompt to a CLI), the previous implementation awaited a foreground `commands.run({ stdin: true, ... })` and then tried to call `sendStdin(pid)` on the now-dead PID > - That call resolves only after the process exits, so stdin was never delivered and e2b raised "process not found" > - This pull request stages stdin to `/tmp/paperclip-stdin-` inside the sandbox and shell-redirects it (`exec '' '' < ''`), making the command synchronous regardless of whether stdin is supplied > - The benefit is adapter Test probes that pipe a hello prompt to a CLI inside an e2b sandbox now actually deliver the prompt ## What Changed - `packages/plugins/sandbox-providers/e2b/src/plugin.ts`: replace the broken async `commands.run` + `sendStdin` flow with stdin-staging to a sandbox temp file and shell-redirection - Staged file is removed in a `finally` block; write failures propagate after best-effort cleanup ## Verification - `pnpm vitest run --no-coverage --project @paperclipai/sandbox-e2b` — all 17 unit tests pass - `pnpm typecheck` clean - Manual: a sandboxed adapter Test probe that pipes a hello prompt now receives the prompt ## Risks Low risk — `plugin.test.ts` already encodes the temp-file design; the change brings the implementation in line with the test. ## Model Used Claude Opus 4.7 (1M context) ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable — existing tests already encode the new design - [x] If this change affects the UI, I have included before/after screenshots — N/A (no UI) - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --- .../sandbox-providers/e2b/src/plugin.ts | 99 +++++++++---------- 1 file changed, 46 insertions(+), 53 deletions(-) diff --git a/packages/plugins/sandbox-providers/e2b/src/plugin.ts b/packages/plugins/sandbox-providers/e2b/src/plugin.ts index 723cc810..142bcad9 100644 --- a/packages/plugins/sandbox-providers/e2b/src/plugin.ts +++ b/packages/plugins/sandbox-providers/e2b/src/plugin.ts @@ -1,5 +1,11 @@ import path from "node:path"; -import { CommandExitError, Sandbox, SandboxNotFoundError, TimeoutError } from "e2b"; +import { randomUUID } from "node:crypto"; +import { + CommandExitError, + Sandbox, + SandboxNotFoundError, + TimeoutError, +} from "e2b"; import { definePlugin } from "@paperclipai/plugin-sdk"; import type { PluginEnvironmentAcquireLeaseParams, @@ -345,79 +351,66 @@ const plugin = definePlugin({ const config = parseDriverConfig(params.config); const sandbox = await connectSandbox(config, params.lease.providerLeaseId); - const command = buildCommandLine(params.command, params.args); - if (params.stdin == null) { + const baseCommand = buildCommandLine(params.command, params.args); + const timeoutMs = params.timeoutMs ?? config.timeoutMs; + + // For commands with stdin, stage the payload to a temp file inside the + // sandbox and shell-redirect it. Streaming stdin via `sendStdin` raced + // with fast-failing commands (the process exits before the RPC lands), + // and the previous code awaited a foreground `run` before sending stdin + // at all, so the data was never delivered. The staged-file approach + // keeps execution synchronous, avoids the race, and is unaffected by + // whether the command exits in microseconds or minutes. + let stagedStdinPath: string | null = null; + if (params.stdin != null) { + stagedStdinPath = `/tmp/paperclip-stdin-${randomUUID()}`; try { - const result = await sandbox.commands.run(command, { - cwd: params.cwd, - envs: params.env, - timeoutMs: params.timeoutMs ?? config.timeoutMs, - }) as Awaited> & { - exitCode: number; - stdout: string; - stderr: string; - }; - return { - exitCode: result.exitCode, - timedOut: false, - stdout: result.stdout, - stderr: result.stderr, - }; + await sandbox.files.write(stagedStdinPath, params.stdin); } catch (error) { - if (error instanceof CommandExitError) { - const commandError = error as CommandExitError; - return { - exitCode: commandError.exitCode, - timedOut: false, - stdout: commandError.stdout, - stderr: commandError.stderr, - }; - } - if (error instanceof TimeoutError) { - return buildTimeoutExecuteResult(error); - } + // Best-effort cleanup in case the write partially succeeded; ignore + // remove failures so the original error is what propagates. + await sandbox.files.remove(stagedStdinPath).catch(() => undefined); throw error; } } - const started = await sandbox.commands.run(command, { - stdin: true, - cwd: params.cwd, - envs: params.env, - timeoutMs: params.timeoutMs ?? config.timeoutMs, - }) as Awaited> & { - pid: number; - exitCode: number; - stdout: string; - stderr: string; - }; + const command = stagedStdinPath + ? `${baseCommand} < ${shellQuote(stagedStdinPath)}` + : baseCommand; try { - try { - await sandbox.commands.sendStdin(started.pid, params.stdin); - } finally { - await sandbox.commands.closeStdin(started.pid); - } + const result = await sandbox.commands.run(command, { + cwd: params.cwd, + envs: params.env, + timeoutMs, + }) as Awaited> & { + exitCode: number; + stdout: string; + stderr: string; + }; return { - exitCode: started.exitCode, + exitCode: result.exitCode, timedOut: false, - stdout: started.stdout, - stderr: started.stderr, + stdout: result.stdout, + stderr: result.stderr, }; } catch (error) { if (error instanceof CommandExitError) { - const commandError = error as CommandExitError; return { - exitCode: commandError.exitCode, + exitCode: error.exitCode, timedOut: false, - stdout: commandError.stdout, - stderr: commandError.stderr, + stdout: error.stdout, + stderr: error.stderr, }; } if (error instanceof TimeoutError) { return buildTimeoutExecuteResult(error); } throw error; + } finally { + if (stagedStdinPath) { + await sandbox.files.remove(stagedStdinPath).catch(() => undefined); + } } }, });