From cbe421f877fc998977a1f1547cca803e5a42c4b5 Mon Sep 17 00:00:00 2001 From: mdsjip <2284562+mdsjip@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:34:20 +0200 Subject: [PATCH] fix(coding-agent): work around Bun empty process.env inside sandbox (#3807) Bun bug oven-sh/bun#27802 causes process.env to be empty when running inside a Linux sandbox (Landlock/seccomp). This breaks npm resolution because PATH is missing, crashing pi on startup with: Failed to run npm root -g: Executable not found in $PATH: "npm" Add getSandboxEnv() to read /proc/self/environ as a fallback on Linux when process.env has zero keys. Pass this env to all spawn calls in package-manager.ts. Also fix runCommandSync error handling: check result.error in addition to result.status, and include result.error?.message in the error text so spawn failures (e.g. ENOENT) produce useful diagnostics instead of "null". Fixes #3806 --- .../coding-agent/src/core/package-manager.ts | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index 0c41b977..a90e041d 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -2,6 +2,26 @@ import { type ChildProcess, type ChildProcessByStdio, spawn, spawnSync } from "n import { createHash } from "node:crypto"; import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; import { homedir, tmpdir } from "node:os"; + +function getEnv(): NodeJS.ProcessEnv { + if (process.platform !== "linux" || Object.keys(process.env).length > 0) { + return process.env; + } + try { + const data = readFileSync("/proc/self/environ", "utf-8"); + const env: NodeJS.ProcessEnv = {}; + for (const entry of data.split("\0")) { + const idx = entry.indexOf("="); + if (idx > 0) { + env[entry.slice(0, idx)] = entry.slice(idx + 1); + } + } + return env; + } catch { + return process.env; + } +} + import { basename, dirname, join, relative, resolve, sep } from "node:path"; import type { Readable } from "node:stream"; import { globSync } from "glob"; @@ -2326,6 +2346,7 @@ export class DefaultPackageManager implements PackageManager { cwd: options?.cwd, stdio: isStdoutTakenOver() ? ["ignore", 2, 2] : "inherit", shell: this.shouldUseWindowsShell(command), + env: getEnv(), }); } @@ -2334,11 +2355,12 @@ export class DefaultPackageManager implements PackageManager { args: string[], options?: { cwd?: string; env?: Record }, ): ChildProcessByStdio { + const baseEnv = getEnv(); return spawn(command, args, { cwd: options?.cwd, stdio: ["ignore", "pipe", "pipe"], shell: this.shouldUseWindowsShell(command), - env: options?.env ? { ...process.env, ...options.env } : process.env, + env: options?.env ? { ...baseEnv, ...options.env } : baseEnv, }); } @@ -2405,9 +2427,12 @@ export class DefaultPackageManager implements PackageManager { stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8", shell: this.shouldUseWindowsShell(command), + env: getEnv(), }); - if (result.status !== 0) { - throw new Error(`Failed to run ${command} ${args.join(" ")}: ${result.stderr || result.stdout}`); + if (result.error || result.status !== 0) { + throw new Error( + `Failed to run ${command} ${args.join(" ")}: ${result.error?.message || result.stderr || result.stdout}`, + ); } return (result.stdout || result.stderr || "").trim(); }