From f2a3ac53eff548323e62e0bb48fb8caa4e79b9c0 Mon Sep 17 00:00:00 2001 From: "vercel-gh-bot-3[bot]" <282332853+vercel-gh-bot-3[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 19:50:22 +0000 Subject: [PATCH] fix(eve): restore Bash process substitution in Vercel sandboxes Co-authored-by: jfonturbel <171843908+jfonturbel@users.noreply.github.com> --- .changeset/repair-vercel-sandbox-dev-fd.md | 5 ++ .../sandbox/bindings/docker-base-setup.ts | 14 +++-- .../execution/sandbox/bindings/vercel.test.ts | 51 +++++++++++-------- .../src/execution/sandbox/bindings/vercel.ts | 2 +- 4 files changed, 46 insertions(+), 26 deletions(-) create mode 100644 .changeset/repair-vercel-sandbox-dev-fd.md diff --git a/.changeset/repair-vercel-sandbox-dev-fd.md b/.changeset/repair-vercel-sandbox-dev-fd.md new file mode 100644 index 0000000000..64f2e4d58b --- /dev/null +++ b/.changeset/repair-vercel-sandbox-dev-fd.md @@ -0,0 +1,5 @@ +--- +"eve": patch +--- + +Restore Bash process substitution in Vercel sandboxes by providing the standard `/dev/fd` path whenever eve creates or resumes a sandbox session. diff --git a/packages/eve/src/execution/sandbox/bindings/docker-base-setup.ts b/packages/eve/src/execution/sandbox/bindings/docker-base-setup.ts index 959568f1ef..77724a50f3 100644 --- a/packages/eve/src/execution/sandbox/bindings/docker-base-setup.ts +++ b/packages/eve/src/execution/sandbox/bindings/docker-base-setup.ts @@ -1,15 +1,21 @@ import { WORKSPACE_ROOT } from "#runtime/workspace/types.js"; /** - * One-time setup applied to containers created from the raw base image - * (template builds and template-less sessions). Keeps the framework-owned - * base layer deliberately tiny: create `/workspace` and verify Bash, - * because the sandbox `bash` tool and command execution depend on it. + * Base setup applied to containers created from the raw base image and to + * Vercel sessions when they are attached. Keeps the framework-owned + * base layer deliberately tiny: create `/workspace`, verify Bash, and provide + * the standard file-descriptor path that Bash process substitution requires. */ export function buildDockerBaseSetupScript(): string { return [ "set -e", `mkdir -p ${WORKSPACE_ROOT}`, 'command -v bash >/dev/null 2>&1 || { echo "the sandbox image must provide bash" >&2; exit 70; }', + "if [ ! /dev/fd -ef /proc/self/fd ]; then", + ' [ ! -e /dev/fd ] || { echo "the sandbox runtime must expose open descriptors through /dev/fd" >&2; exit 70; }', + ' [ -d /proc/self/fd ] || { echo "the sandbox runtime must provide /proc/self/fd" >&2; exit 70; }', + " ln -s /proc/self/fd /dev/fd 2>/dev/null || [ /dev/fd -ef /proc/self/fd ]", + "fi", + 'test /dev/fd -ef /proc/self/fd || { echo "the sandbox runtime must expose open descriptors through /dev/fd" >&2; exit 70; }', ].join("\n"); } diff --git a/packages/eve/src/execution/sandbox/bindings/vercel.test.ts b/packages/eve/src/execution/sandbox/bindings/vercel.test.ts index cab2e0fe45..01447af1dd 100644 --- a/packages/eve/src/execution/sandbox/bindings/vercel.test.ts +++ b/packages/eve/src/execution/sandbox/bindings/vercel.test.ts @@ -1069,6 +1069,11 @@ describe("createVercelSandbox", () => { name: "persisted-sandbox-name", resume: false, }); + expect(sessionSandbox.runCommand).toHaveBeenCalledTimes(1); + expect(sessionSandbox.runCommand).toHaveBeenCalledWith({ + args: ["-lc", expect.stringContaining("ln -s /proc/self/fd /dev/fd")], + cmd: "bash", + }); expect(handle.session).toBeDefined(); const state = await handle.captureState(); @@ -1753,27 +1758,31 @@ describe("createVercelSandbox", () => { templateKey: "template-key", }); - const templateCalls = vi.mocked(templateSandbox.runCommand).mock.calls; - expect(templateCalls).toHaveLength(1); - - const setupCall = templateCalls[0]?.[0] as { - args?: string[]; - cmd?: string; - sudo?: boolean; - }; - expect(setupCall).toMatchObject({ cmd: "bash" }); - expect(setupCall.sudo).toBeUndefined(); - const setupScript = setupCall.args?.[1] ?? ""; - expect(setupScript).toContain("mkdir -p /workspace"); - expect(setupScript).toContain("command -v bash"); - expect(setupScript).not.toContain("apt-get"); - expect(setupScript).not.toContain("gpgv"); - expect(setupScript).not.toContain("node --version"); - expect(setupScript).not.toContain("npm"); - expect(setupScript).not.toContain("python3"); - expect(setupScript).not.toContain("ripgrep"); - expect(setupScript).not.toContain("sudo mkdir"); - expect(setupScript).not.toContain("chown"); + for (const sandbox of [templateSandbox, sessionSandbox]) { + const calls = vi.mocked(sandbox.runCommand).mock.calls; + expect(calls).toHaveLength(1); + + const setupCall = calls[0]?.[0] as { + args?: string[]; + cmd?: string; + sudo?: boolean; + }; + expect(setupCall).toMatchObject({ cmd: "bash" }); + expect(setupCall.sudo).toBeUndefined(); + const setupScript = setupCall.args?.[1] ?? ""; + expect(setupScript).toContain("mkdir -p /workspace"); + expect(setupScript).toContain("command -v bash"); + expect(setupScript).toContain("ln -s /proc/self/fd /dev/fd"); + expect(setupScript).toContain("test /dev/fd -ef /proc/self/fd"); + expect(setupScript).not.toContain("apt-get"); + expect(setupScript).not.toContain("gpgv"); + expect(setupScript).not.toContain("node --version"); + expect(setupScript).not.toContain("npm"); + expect(setupScript).not.toContain("python3"); + expect(setupScript).not.toContain("ripgrep"); + expect(setupScript).not.toContain("sudo mkdir"); + expect(setupScript).not.toContain("chown"); + } }); it("retries base runtime setup through sudo when the default user fails", async () => { diff --git a/packages/eve/src/execution/sandbox/bindings/vercel.ts b/packages/eve/src/execution/sandbox/bindings/vercel.ts index 2ce7c5e653..c01adbcb0b 100644 --- a/packages/eve/src/execution/sandbox/bindings/vercel.ts +++ b/packages/eve/src/execution/sandbox/bindings/vercel.ts @@ -136,8 +136,8 @@ export function createVercelSandbox( ); } + await ensureVercelSandboxBaseRuntime(session.sandbox); if (template === null && session.created) { - await ensureVercelSandboxBaseRuntime(session.sandbox); await applyInitialVercelNetworkPolicy(session.sandbox, createOptions.networkPolicy); }