The PTY runtime exported an empty NODE_CHANNEL_FD to override the daemon's IPC descriptor, because bun-pty merges the native environ back into the child and a JS-only delete does not stick. Node CLIs launched from the shell (opencode, claude) then printed "warn: Failed to parse IPC channel number ''" on exit. The Linux-only env -u ARGV0 wrapper now applies on macOS and Linux and also unsets NODE_CHANNEL_FD, so the variable is gone instead of empty. Testing: runtime and inherited-env tests updated for the POSIX wrapper; verified in the running app that exiting opencode no longer prints the warning.
82 lines
3.0 KiB
JavaScript
82 lines
3.0 KiB
JavaScript
/**
|
|
* Sanitize environment objects inherited by user-facing child processes.
|
|
*
|
|
* Linux AppImage runtimes export `ARGV0` as the AppImage path before launching
|
|
* the packaged app. zsh treats an exported `ARGV0` as the argv[0] for every
|
|
* external command it spawns, which corrupts Python venv detection and any
|
|
* other program that reads argv[0]/$0 while leaving `/proc/self/exe` correct.
|
|
*
|
|
* See openchamber/openchamber#2588 and pingdotgg/t3code#2509.
|
|
*/
|
|
|
|
import { createRequire } from 'node:module';
|
|
import { existsSync } from 'node:fs';
|
|
|
|
const POSIX_ENV_BINARIES = ['/usr/bin/env', '/bin/env'];
|
|
/** Variables a PTY shell must never inherit from the OpenChamber host process. */
|
|
const PTY_HOST_PRIVATE_VARIABLES = Object.freeze(['ARGV0', 'NODE_CHANNEL_FD']);
|
|
|
|
/**
|
|
* Remove AppImage `ARGV0` from a mutable env object (or `process.env`).
|
|
* @param {NodeJS.ProcessEnv | Record<string, string | undefined> | null | undefined} env
|
|
* @returns {typeof env}
|
|
*/
|
|
export function stripAppImageArgv0Leak(env) {
|
|
if (!env || typeof env !== 'object') return env;
|
|
if (Object.prototype.hasOwnProperty.call(env, 'ARGV0')) {
|
|
delete env.ARGV0;
|
|
}
|
|
return env;
|
|
}
|
|
|
|
/**
|
|
* Clear AppImage `ARGV0` from this process.
|
|
*
|
|
* Bun keeps a native environ that `bun-pty` inherits even after
|
|
* `delete process.env.ARGV0`. On Linux under Bun we also call libc `unsetenv`.
|
|
*/
|
|
export function clearAppImageArgv0FromProcessEnv() {
|
|
delete process.env.ARGV0;
|
|
if (process.platform !== 'linux' || typeof Bun === 'undefined') return;
|
|
try {
|
|
const require = createRequire(import.meta.url);
|
|
const { dlopen } = require('bun:ffi');
|
|
const libc = dlopen('libc.so.6', {
|
|
unsetenv: { args: ['cstring'], returns: 'i32' },
|
|
});
|
|
libc.symbols.unsetenv(Buffer.from('ARGV0\0'));
|
|
} catch {
|
|
// Node/Electron and environments without bun:ffi rely on explicit child envs.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve a POSIX PTY launch that unsets host-private variables before the
|
|
* shell starts.
|
|
*
|
|
* `bun-pty` merges the OS environ into the child, so deleting a variable from
|
|
* the JS env object alone is not enough: AppImage `ARGV0` came back on Linux,
|
|
* and the daemon's `NODE_CHANNEL_FD` came back everywhere (Node then warns
|
|
* "Failed to parse IPC channel number" when a CLI such as opencode exits).
|
|
* Wrapping with `env -u NAME ...` unsets them before execing the real shell.
|
|
* No-op on Windows and when no `env` binary is found.
|
|
*
|
|
* @param {string} executable
|
|
* @param {string[]} args
|
|
* @param {readonly string[]} variables
|
|
* @returns {{ executable: string, args: string[] }}
|
|
*/
|
|
export function resolvePosixPtyLaunch(executable, args = [], variables = PTY_HOST_PRIVATE_VARIABLES) {
|
|
if (process.platform === 'win32' || variables.length === 0) {
|
|
return { executable, args };
|
|
}
|
|
const envBinary = POSIX_ENV_BINARIES.find((candidate) => existsSync(candidate));
|
|
if (!envBinary) {
|
|
return { executable, args };
|
|
}
|
|
return {
|
|
executable: envBinary,
|
|
args: [...variables.flatMap((name) => ['-u', name]), executable, ...args],
|
|
};
|
|
}
|