fix(coding-agent,ai): fallback to /proc/self/environ in Bun sandbox (#3801)
Bun compiled binaries have an empty process.env when running inside sandbox environments (e.g. nono on Linux/macOS). This broke API key detection and model discovery because all process.env.* lookups returned undefined. - Add restoreSandboxEnv() helper that reads /proc/self/environ when Bun is detected and process.env is empty, populating process.env before any other code runs (coding-agent/src/bun/cli.ts entry point) - Add getProcEnv() fallback in env-api-keys.ts for direct @mariozechner/pi-ai consumers that may not go through the coding-agent entry point - Add unit tests for restoreSandboxEnv
This commit is contained in:
@@ -25,6 +25,39 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version
|
||||
|
||||
import type { KnownProvider } from "./types.js";
|
||||
|
||||
let _procEnvCache: Map<string, string> | null = null;
|
||||
|
||||
/**
|
||||
* Fallback for https://github.com/oven-sh/bun/issues/27802
|
||||
* Bun compiled binaries have an empty `process.env` inside sandbox
|
||||
* environments on Linux. We can recover the env from `/proc/self/environ`.
|
||||
*/
|
||||
function getProcEnv(key: string): string | undefined {
|
||||
if (!process.versions?.bun) return undefined;
|
||||
if (typeof process === "undefined") return undefined;
|
||||
|
||||
// If process.env already has entries, the bug is not triggered.
|
||||
if (Object.keys(process.env).length > 0) return undefined;
|
||||
|
||||
if (_procEnvCache === null) {
|
||||
_procEnvCache = new Map();
|
||||
try {
|
||||
const { readFileSync } = require("node:fs") as typeof import("node:fs");
|
||||
const data = readFileSync("/proc/self/environ", "utf-8");
|
||||
for (const entry of data.split("\0")) {
|
||||
const idx = entry.indexOf("=");
|
||||
if (idx > 0) {
|
||||
_procEnvCache.set(entry.slice(0, idx), entry.slice(idx + 1));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// /proc/self/environ may not be readable.
|
||||
}
|
||||
}
|
||||
|
||||
return _procEnvCache.get(key);
|
||||
}
|
||||
|
||||
let cachedVertexAdcCredentialsExists: boolean | null = null;
|
||||
|
||||
function hasVertexAdcCredentials(): boolean {
|
||||
@@ -42,7 +75,7 @@ function hasVertexAdcCredentials(): boolean {
|
||||
}
|
||||
|
||||
// Check GOOGLE_APPLICATION_CREDENTIALS env var first (standard way)
|
||||
const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
||||
const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS || getProcEnv("GOOGLE_APPLICATION_CREDENTIALS");
|
||||
if (gacPath) {
|
||||
cachedVertexAdcCredentialsExists = _existsSync(gacPath);
|
||||
} else {
|
||||
@@ -104,7 +137,7 @@ export function findEnvKeys(provider: string): string[] | undefined {
|
||||
const envVars = getApiKeyEnvVars(provider);
|
||||
if (!envVars) return undefined;
|
||||
|
||||
const found = envVars.filter((envVar) => !!process.env[envVar]);
|
||||
const found = envVars.filter((envVar) => !!process.env[envVar] || !!getProcEnv(envVar));
|
||||
return found.length > 0 ? found : undefined;
|
||||
}
|
||||
|
||||
@@ -117,14 +150,21 @@ export function getEnvApiKey(provider: KnownProvider): string | undefined;
|
||||
export function getEnvApiKey(provider: string): string | undefined;
|
||||
export function getEnvApiKey(provider: string): string | undefined {
|
||||
const envKeys = findEnvKeys(provider);
|
||||
if (envKeys?.[0]) return process.env[envKeys[0]];
|
||||
if (envKeys?.[0]) {
|
||||
return process.env[envKeys[0]] || getProcEnv(envKeys[0]);
|
||||
}
|
||||
|
||||
// Vertex AI supports either an explicit API key or Application Default Credentials.
|
||||
// Auth is configured via `gcloud auth application-default login`.
|
||||
if (provider === "google-vertex") {
|
||||
const hasCredentials = hasVertexAdcCredentials();
|
||||
const hasProject = !!(process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT);
|
||||
const hasLocation = !!process.env.GOOGLE_CLOUD_LOCATION;
|
||||
const hasProject = !!(
|
||||
process.env.GOOGLE_CLOUD_PROJECT ||
|
||||
process.env.GCLOUD_PROJECT ||
|
||||
getProcEnv("GOOGLE_CLOUD_PROJECT") ||
|
||||
getProcEnv("GCLOUD_PROJECT")
|
||||
);
|
||||
const hasLocation = !!(process.env.GOOGLE_CLOUD_LOCATION || getProcEnv("GOOGLE_CLOUD_LOCATION"));
|
||||
|
||||
if (hasCredentials && hasProject && hasLocation) {
|
||||
return "<authenticated>";
|
||||
@@ -145,7 +185,13 @@ export function getEnvApiKey(provider: string): string | undefined {
|
||||
process.env.AWS_BEARER_TOKEN_BEDROCK ||
|
||||
process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI ||
|
||||
process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI ||
|
||||
process.env.AWS_WEB_IDENTITY_TOKEN_FILE
|
||||
process.env.AWS_WEB_IDENTITY_TOKEN_FILE ||
|
||||
getProcEnv("AWS_PROFILE") ||
|
||||
(getProcEnv("AWS_ACCESS_KEY_ID") && getProcEnv("AWS_SECRET_ACCESS_KEY")) ||
|
||||
getProcEnv("AWS_BEARER_TOKEN_BEDROCK") ||
|
||||
getProcEnv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") ||
|
||||
getProcEnv("AWS_CONTAINER_CREDENTIALS_FULL_URI") ||
|
||||
getProcEnv("AWS_WEB_IDENTITY_TOKEN_FILE")
|
||||
) {
|
||||
return "<authenticated>";
|
||||
}
|
||||
|
||||
@@ -4,5 +4,9 @@ import { APP_NAME } from "../config.js";
|
||||
process.title = APP_NAME;
|
||||
process.emitWarning = (() => {}) as typeof process.emitWarning;
|
||||
|
||||
import { restoreSandboxEnv } from "./restore-sandbox-env.js";
|
||||
|
||||
restoreSandboxEnv();
|
||||
|
||||
await import("./register-bedrock.js");
|
||||
await import("../cli.js");
|
||||
|
||||
32
packages/coding-agent/src/bun/restore-sandbox-env.ts
Normal file
32
packages/coding-agent/src/bun/restore-sandbox-env.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Workaround for https://github.com/oven-sh/bun/issues/27802
|
||||
*
|
||||
* Bun compiled binaries have an empty `process.env` when running inside
|
||||
* sandbox environments (e.g. nono on Linux/macOS). On Linux we can recover
|
||||
* the environment from `/proc/self/environ`.
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
/**
|
||||
* Restore environment variables from `/proc/self/environ` when running
|
||||
* inside a sandbox where Bun's `process.env` is empty.
|
||||
*/
|
||||
export function restoreSandboxEnv(): void {
|
||||
if (!process.versions?.bun) return;
|
||||
|
||||
// If process.env already has entries, nothing to fix.
|
||||
if (Object.keys(process.env).length > 0) return;
|
||||
|
||||
try {
|
||||
const data = readFileSync("/proc/self/environ", "utf-8");
|
||||
for (const entry of data.split("\0")) {
|
||||
const idx = entry.indexOf("=");
|
||||
if (idx > 0) {
|
||||
process.env[entry.slice(0, idx)] = entry.slice(idx + 1);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// /proc/self/environ may not be readable; ignore.
|
||||
}
|
||||
}
|
||||
77
packages/coding-agent/test/restore-sandbox-env.test.ts
Normal file
77
packages/coding-agent/test/restore-sandbox-env.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const readFileSync = vi.fn();
|
||||
|
||||
vi.mock("node:fs", () => ({
|
||||
readFileSync,
|
||||
}));
|
||||
|
||||
const { restoreSandboxEnv } = await import("../src/bun/restore-sandbox-env.js");
|
||||
|
||||
describe("restoreSandboxEnv", () => {
|
||||
it("does nothing when not running under bun", () => {
|
||||
const originalVersions = Object.getOwnPropertyDescriptor(process, "versions");
|
||||
Object.defineProperty(process, "versions", {
|
||||
value: { node: "20.0.0" },
|
||||
});
|
||||
const envBefore = { ...process.env };
|
||||
|
||||
restoreSandboxEnv();
|
||||
|
||||
expect(process.env).toEqual(envBefore);
|
||||
|
||||
if (originalVersions) {
|
||||
Object.defineProperty(process, "versions", originalVersions);
|
||||
}
|
||||
});
|
||||
|
||||
it("does nothing when process.env already has entries", () => {
|
||||
const originalVersions = Object.getOwnPropertyDescriptor(process, "versions");
|
||||
Object.defineProperty(process, "versions", {
|
||||
value: { bun: "1.2.0", node: "20.0.0" },
|
||||
});
|
||||
process.env.RESTORE_SANDBOX_ENV_TEST = "1";
|
||||
const envBefore = { ...process.env };
|
||||
|
||||
restoreSandboxEnv();
|
||||
|
||||
expect(process.env).toEqual(envBefore);
|
||||
delete process.env.RESTORE_SANDBOX_ENV_TEST;
|
||||
|
||||
if (originalVersions) {
|
||||
Object.defineProperty(process, "versions", originalVersions);
|
||||
}
|
||||
});
|
||||
|
||||
it("restores environment from /proc/self/environ when bun env is empty", () => {
|
||||
const originalVersions = Object.getOwnPropertyDescriptor(process, "versions");
|
||||
Object.defineProperty(process, "versions", {
|
||||
value: { bun: "1.2.0", node: "20.0.0" },
|
||||
});
|
||||
|
||||
// Clear env to simulate the bun sandbox bug.
|
||||
const envBackup = { ...process.env };
|
||||
for (const key of Object.keys(process.env)) {
|
||||
delete process.env[key];
|
||||
}
|
||||
|
||||
readFileSync.mockReturnValue("FOO=bar\0BAZ=qux\0");
|
||||
|
||||
restoreSandboxEnv();
|
||||
|
||||
expect(readFileSync).toHaveBeenCalledWith("/proc/self/environ", "utf-8");
|
||||
expect(process.env.FOO).toBe("bar");
|
||||
expect(process.env.BAZ).toBe("qux");
|
||||
|
||||
// Restore.
|
||||
for (const key of Object.keys(process.env)) {
|
||||
delete process.env[key];
|
||||
}
|
||||
Object.assign(process.env, envBackup);
|
||||
|
||||
if (originalVersions) {
|
||||
Object.defineProperty(process, "versions", originalVersions);
|
||||
}
|
||||
readFileSync.mockReset();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user