Skip to content

Commit 5694e18

Browse files
committed
feat(nodejs): add includeNodeShims option to opt out of globalThis polyfills
Fixes #63 — adds ability to disable Node.js polyfill shims (fs, http, process, Buffer, etc.) on globalThis. When includeNodeShims: false is passed to createNodeRuntime(): - globalThis.fs, globalThis.http, globalThis.process, globalThis.Buffer are NOT injected into the isolate - Useful for AI agents that need a clean globalThis scope - fs/http are still accessible via require('fs') / await import('fs') when host filesystem is permitted via permissions API: createNodeRuntime({ includeNodeShims: false }) Default: true (existing behavior unchanged). Changes: - packages/nodejs/src/kernel-runtime.ts: Added includeNodeShims to NodeRuntimeOptions - packages/nodejs/src/driver.ts: Added includeNodeShims to NodeDriverOptions, pass to runtime config - packages/nodejs/src/execution-driver.ts: buildFullBridgeCode() now keyed by includeNodeShims in a Map<boolean,string> cache; per-driver bridge code built in constructor - packages/nodejs/test/kernel-runtime.test.ts: 3 new tests for includeNodeShims=false/true
1 parent 6a7652a commit 5694e18

4 files changed

Lines changed: 106 additions & 11 deletions

File tree

packages/nodejs/src/driver.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export interface NodeDriverOptions {
4040
loopbackExemptPorts?: number[];
4141
processConfig?: ProcessConfig;
4242
osConfig?: OSConfig;
43+
/** Include Node.js shims (fs, http, process, Buffer, etc.) on globalThis. Default: true. */
44+
includeNodeShims?: boolean;
4345
}
4446

4547
export interface NodeRuntimeDriverFactoryOptions {
@@ -260,6 +262,10 @@ export function createNodeDriver(options: NodeDriverOptions = {}): SystemDriver
260262
os: {
261263
...(options.osConfig ?? {}),
262264
},
265+
// @ts-ignore-next-line — internal field used by NodeExecutionDriver to gate bridge shims
266+
...(options.includeNodeShims !== undefined
267+
? { includeNodeShims: options.includeNodeShims }
268+
: {}),
263269
},
264270
};
265271
}

packages/nodejs/src/execution-driver.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ interface DriverState {
140140
resolutionCache: ResolutionCache;
141141
onPtySetRawMode?: (mode: boolean) => void;
142142
liveStdinSource?: NodeExecutionDriverOptions["liveStdinSource"];
143+
/** Pre-built bridge code for this driver, based on includeNodeShims setting. */
144+
bridgeCode: string;
145+
/** Whether this driver includes Node.js polyfill shims on globalThis. */
146+
includeNodeShims: boolean;
143147
}
144148

145149
// Shared V8 runtime process — one per Node.js process, lazy-initialized
@@ -151,8 +155,8 @@ async function getSharedV8Runtime(): Promise<V8Runtime> {
151155
if (sharedV8Runtime?.isAlive) return sharedV8Runtime;
152156
if (sharedV8RuntimePromise) return sharedV8RuntimePromise;
153157

154-
// Build bridge code for snapshot warmup
155-
const bridgeCode = buildFullBridgeCode();
158+
// Build bridge code for snapshot warmup (always with shims for the shared runtime)
159+
const bridgeCode = buildFullBridgeCode(true);
156160

157161
sharedV8RuntimePromise = createV8Runtime({
158162
warmupBridgeCode: bridgeCode,
@@ -765,10 +769,12 @@ function buildBridgeDispatchShim(): string {
765769
const BRIDGE_DISPATCH_SHIM = buildBridgeDispatchShim();
766770

767771
// Cache assembled bridge code (same across all executions)
768-
let bridgeCodeCache: string | null = null;
772+
// Keyed by includeNodeShims flag so drivers with different settings get correct code
773+
const bridgeCodeCache = new Map<boolean, string>();
769774

770-
function buildFullBridgeCode(): string {
771-
if (bridgeCodeCache) return bridgeCodeCache;
775+
function buildFullBridgeCode(includeNodeShims: boolean = true): string {
776+
const cached = bridgeCodeCache.get(includeNodeShims);
777+
if (cached !== undefined) return cached;
772778

773779
// Assemble the full bridge code IIFE from component scripts.
774780
// Only include code that can run without bridge calls (snapshot phase).
@@ -778,12 +784,18 @@ function buildFullBridgeCode(): string {
778784
V8_POLYFILLS,
779785
getIsolateRuntimeSource("globalExposureHelpers"),
780786
getInitialBridgeGlobalsSetupCode(),
781-
getRawBridgeCode(),
782-
getBridgeAttachCode(),
783787
];
784788

785-
bridgeCodeCache = parts.join("\n");
786-
return bridgeCodeCache;
789+
// Only include Node.js shims (fs, http, process globals, etc.) when explicitly requested.
790+
// For AI agent use cases, users may want a clean globalThis with no injected polyfills.
791+
if (includeNodeShims) {
792+
parts.push(getRawBridgeCode());
793+
parts.push(getBridgeAttachCode());
794+
}
795+
796+
const code = parts.join("\n");
797+
bridgeCodeCache.set(includeNodeShims, code);
798+
return code;
787799
}
788800

789801
export class NodeExecutionDriver implements RuntimeDriver {
@@ -853,6 +865,11 @@ export class NodeExecutionDriver implements RuntimeDriver {
853865
osConfig.homedir ??= DEFAULT_SANDBOX_HOME;
854866
osConfig.tmpdir ??= DEFAULT_SANDBOX_TMPDIR;
855867

868+
// Determine whether to include Node.js polyfill shims on globalThis.
869+
// When false, globalThis has no injected fs, http, process, Buffer, etc.
870+
// Useful for AI agent use cases that need a clean global scope.
871+
const includeNodeShims = (options.runtime as any).includeNodeShims ?? true;
872+
856873
const bridgeBase64TransferLimitBytes = normalizePayloadLimit(
857874
options.payloadLimits?.base64TransferBytes,
858875
DEFAULT_BRIDGE_BASE64_TRANSFER_BYTES,
@@ -893,6 +910,8 @@ export class NodeExecutionDriver implements RuntimeDriver {
893910
resolutionCache: createResolutionCache(),
894911
onPtySetRawMode: options.onPtySetRawMode,
895912
liveStdinSource: options.liveStdinSource,
913+
bridgeCode: buildFullBridgeCode(includeNodeShims),
914+
includeNodeShims,
896915
};
897916

898917
// Validate and flatten bindings once at construction time
@@ -1284,8 +1303,8 @@ export class NodeExecutionDriver implements RuntimeDriver {
12841303
}
12851304
}
12861305

1287-
// Build bridge code with embedded config
1288-
const bridgeCode = buildFullBridgeCode();
1306+
// Use the pre-built bridge code from constructor (respects includeNodeShims)
1307+
const bridgeCode = this.state.bridgeCode;
12891308

12901309
// Build post-restore script with per-execution config
12911310
const bindingKeys = this.flattenedBindings

packages/nodejs/src/kernel-runtime.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,18 @@ export interface NodeRuntimeOptions {
7272
* before the CWD-based node_modules fallback in the ModuleAccessFileSystem.
7373
*/
7474
packageRoots?: Array<{ hostPath: string; vmPath: string }>;
75+
/**
76+
* Include Node.js polyfill shims (fs, http, process, Buffer, etc.) on globalThis.
77+
*
78+
* When false: globalThis is clean — useful for AI agents that need full
79+
* control over the global scope without any injected Node.js globals.
80+
*
81+
* You can still access these modules via `require('fs')` or `await import('fs')`
82+
* when the host filesystem is accessible via permissions.
83+
*
84+
* Default: true (include shims, for backward compatibility).
85+
*/
86+
includeNodeShims?: boolean;
7587
}
7688

7789
const allowKernelProcSelfRead: Pick<Permissions, 'fs'> = {
@@ -409,6 +421,7 @@ class NodeRuntimeDriver implements RuntimeDriver {
409421
private _loopbackExemptPorts?: number[];
410422
private _moduleAccessCwd?: string;
411423
private _packageRoots?: Array<{ hostPath: string; vmPath: string }>;
424+
private _includeNodeShims: boolean;
412425

413426
constructor(options?: NodeRuntimeOptions) {
414427
this._memoryLimit = options?.memoryLimit ?? 128;
@@ -417,6 +430,7 @@ class NodeRuntimeDriver implements RuntimeDriver {
417430
this._loopbackExemptPorts = options?.loopbackExemptPorts;
418431
this._moduleAccessCwd = options?.moduleAccessCwd;
419432
this._packageRoots = options?.packageRoots;
433+
this._includeNodeShims = options?.includeNodeShims ?? true;
420434
}
421435

422436
async init(kernel: KernelInterface): Promise<void> {
@@ -724,6 +738,7 @@ class NodeRuntimeDriver implements RuntimeDriver {
724738
homedir: ctx.env.HOME || '/root',
725739
tmpdir: ctx.env.TMPDIR || '/tmp',
726740
},
741+
includeNodeShims: this._includeNodeShims,
727742
});
728743

729744
// Wire PTY raw mode callback when stdin is a terminal

packages/nodejs/test/kernel-runtime.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -969,3 +969,58 @@ describe('Node RuntimeDriver', () => {
969969
}, 10_000);
970970
});
971971
});
972+
973+
describe('includeNodeShims option', () => {
974+
let kernel: Kernel;
975+
976+
afterEach(async () => {
977+
await kernel?.dispose();
978+
});
979+
980+
it('globalThis.fs is undefined when includeNodeShims is false', async () => {
981+
// With includeNodeShims: false, the bridge does NOT inject fs/http/etc.
982+
// onto globalThis. This is useful for AI agents that need a clean scope.
983+
const vfs = new SimpleVFS();
984+
kernel = createKernel({ filesystem: vfs as any });
985+
await kernel.mount(createNodeRuntime({ includeNodeShims: false }));
986+
987+
// Use exec() with node fallback (fixes #64 — exec falls back to node
988+
// when sh is not registered)
989+
const result = await kernel.exec(
990+
991+
ode -e "console.log(typeof fs)",
992+
);
993+
994+
expect(result.exitCode).toBe(0);
995+
expect(result.stdout.trim()).toBe('undefined');
996+
});
997+
998+
it('globalThis.fs is an object when includeNodeShims is true (default)', async () => {
999+
// Default behavior: bridge injects fs, http, process, Buffer etc. onto globalThis.
1000+
const vfs = new SimpleVFS();
1001+
kernel = createKernel({ filesystem: vfs as any });
1002+
await kernel.mount(createNodeRuntime({ includeNodeShims: true }));
1003+
1004+
const result = await kernel.exec(
1005+
1006+
ode -e "console.log(typeof fs)",
1007+
);
1008+
1009+
expect(result.exitCode).toBe(0);
1010+
expect(result.stdout.trim()).toBe('object');
1011+
});
1012+
1013+
it('exec with includeNodeShims=false still works via node fallback', async () => {
1014+
const vfs = new SimpleVFS();
1015+
kernel = createKernel({ filesystem: vfs as any });
1016+
await kernel.mount(createNodeRuntime({ includeNodeShims: false }));
1017+
1018+
const result = await kernel.exec(
1019+
1020+
ode -e "console.log('hello from no-shims runtime')",
1021+
);
1022+
1023+
expect(result.exitCode).toBe(0);
1024+
expect(result.stdout.trim()).toBe('hello from no-shims runtime');
1025+
});
1026+
});

0 commit comments

Comments
 (0)