@utdk/isolate 0.1.0-dev.646adf4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Options for a single sandboxed tool call.
3
+ */
4
+ export interface ExecuteOptions {
5
+ /**
6
+ * Provider module specifier, e.g. `@utdk/github`.
7
+ * Resolved from the host Node.js module system, not from inside the sandbox.
8
+ */
9
+ module: string;
10
+ /**
11
+ * Dot-notation path to the operation on the client object, e.g. `users.getByUsername`.
12
+ */
13
+ operation: string;
14
+ /**
15
+ * Arguments passed to the operation.
16
+ */
17
+ args?: Record<string, unknown>;
18
+ /**
19
+ * Credentials injected by the gateway at call time.
20
+ * Keys are environment-variable-style names (e.g. `GITHUB_TOKEN`).
21
+ * These are NEVER written to `process.env`; the sandbox context has no
22
+ * access to `process` at all.
23
+ */
24
+ credentials?: Record<string, string>;
25
+ /**
26
+ * Execution timeout in milliseconds (default: 30 000).
27
+ * Once exceeded, the isolate rejects with a `TimeoutError`.
28
+ */
29
+ timeout?: number;
30
+ }
31
+ /**
32
+ * Result of a sandboxed execution.
33
+ */
34
+ export interface ExecuteResult {
35
+ /** The value returned by the operation. */
36
+ data: unknown;
37
+ /** Wall-clock duration of the execution in milliseconds. */
38
+ durationMs: number;
39
+ }
40
+ /**
41
+ * A single entry from a provider's `utdk.auth` config array in `package.json`.
42
+ */
43
+ export interface UtdkAuthConfig {
44
+ auth_type: "api_key" | "oauth2" | "basic" | "none";
45
+ /** For api_key: the header value pattern, e.g. "Bearer ${GITHUB_TOKEN}" */
46
+ api_key?: string;
47
+ /** For api_key: the header name, e.g. "Authorization" */
48
+ var_name?: string;
49
+ /** For oauth2: client id pattern, e.g. "${SPOTIFY_CLIENT_ID}" */
50
+ client_id?: string;
51
+ /** For oauth2: client secret pattern, e.g. "${SPOTIFY_CLIENT_SECRET}" */
52
+ client_secret?: string;
53
+ /** For oauth2: token URL */
54
+ token_url?: string;
55
+ /** For oauth2: flow type */
56
+ flow?: string;
57
+ }
58
+ /**
59
+ * Error thrown when an execution exceeds its timeout.
60
+ */
61
+ export declare class TimeoutError extends Error {
62
+ constructor(timeoutMs: number);
63
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Error thrown when an execution exceeds its timeout.
3
+ */
4
+ export class TimeoutError extends Error {
5
+ constructor(timeoutMs) {
6
+ super(`Isolate execution timed out after ${timeoutMs}ms`);
7
+ this.name = "TimeoutError";
8
+ }
9
+ }
10
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAgEA;;GAEG;AACH,MAAM,OAAO,YAAa,SAAQ,KAAK;IACrC,YAAY,SAAiB;QAC3B,KAAK,CAAC,qCAAqC,SAAS,IAAI,CAAC,CAAC;QAC1D,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;IAC7B,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@utdk/isolate",
3
+ "version": "0.1.0-dev.646adf4",
4
+ "type": "module",
5
+ "description": "Sandboxed Node.js vm.createContext execution runtime for @utdk tool calls",
6
+ "keywords": [
7
+ "sandbox",
8
+ "isolate",
9
+ "vm",
10
+ "utdk"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "dependencies": {
20
+ "@utdk/common": "0.1.0-dev.646adf4"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^25.5.0",
24
+ "typescript": "^5.7.3",
25
+ "vitest": "2.1.5"
26
+ },
27
+ "engines": {
28
+ "node": ">=20.0.0"
29
+ },
30
+ "scripts": {
31
+ "build": "tsc -p tsconfig.json",
32
+ "check-types": "tsc -p tsconfig.json --noEmit",
33
+ "typecheck": "tsc -p tsconfig.json --noEmit",
34
+ "clean": "rm -rf dist",
35
+ "test": "vitest run"
36
+ }
37
+ }
package/src/bridge.ts ADDED
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Credential bridge for the Isolate runtime.
3
+ *
4
+ * The bridge is the ONLY channel through which credentials and provider
5
+ * operations enter the sandboxed vm context. It is created in the host context
6
+ * (where Node.js module loading works normally) and exposed as `__bridge__`
7
+ * inside the sandbox.
8
+ *
9
+ * Design:
10
+ * - The provider module is dynamically imported in the HOST context, so it
11
+ * can resolve its own dependencies normally.
12
+ * - Credentials are mapped to an `AuthProvider` from `@utdk/common`, which
13
+ * injects them into HTTP request headers at call time.
14
+ * - The operation function is resolved via dot-notation path on the client.
15
+ * - The sandbox script calls `await __bridge__.call(args)` — it never
16
+ * touches `process.env` or any host filesystem API.
17
+ */
18
+
19
+ import { ApiKey, BearerToken, type AuthProvider } from "@utdk/common";
20
+ import type { UtdkAuthConfig } from "./types.js";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Credential resolution
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /**
27
+ * Resolves an `AuthProvider` from a flat credentials map and the provider's
28
+ * `utdk.auth` config array (from its `package.json`).
29
+ *
30
+ * Supported patterns:
31
+ * api_key – Bearer header: `api_key: "Bearer ${TOKEN_NAME}"`
32
+ * api_key – Raw header: `api_key: "${TOKEN_NAME}"`
33
+ * oauth2 – Pre-resolved: looks for `PROVIDER_ACCESS_TOKEN` key
34
+ *
35
+ * Falls back to `BearerToken(firstValue)` when the auth config is absent or
36
+ * the pattern is not recognised.
37
+ */
38
+ export function resolveAuthProvider(
39
+ credentials: Record<string, string>,
40
+ authConfigs: UtdkAuthConfig[] = [],
41
+ ): AuthProvider | undefined {
42
+ if (Object.keys(credentials).length === 0) {
43
+ return undefined;
44
+ }
45
+
46
+ for (const config of authConfigs) {
47
+ if (config.auth_type === "api_key" && config.api_key) {
48
+ // Pattern: "Bearer ${TOKEN_NAME}"
49
+ const bearerMatch = config.api_key.match(/^Bearer \$\{([^}]+)\}$/);
50
+ if (bearerMatch) {
51
+ const varName = bearerMatch[1];
52
+ if (varName && credentials[varName]) {
53
+ return new BearerToken(credentials[varName]);
54
+ }
55
+ }
56
+
57
+ // Pattern: "${TOKEN_NAME}" (raw header value)
58
+ const rawMatch = config.api_key.match(/^\$\{([^}]+)\}$/);
59
+ if (rawMatch) {
60
+ const varName = rawMatch[1];
61
+ if (varName && credentials[varName]) {
62
+ return new ApiKey({
63
+ headerName: config.var_name ?? "X-Api-Key",
64
+ value: credentials[varName],
65
+ });
66
+ }
67
+ }
68
+ }
69
+
70
+ if (config.auth_type === "oauth2") {
71
+ // Gateway pre-resolves OAuth2 tokens and passes them as ACCESS_TOKEN
72
+ // e.g. credentials = { SPOTIFY_ACCESS_TOKEN: 'eyJ...' }
73
+ const tokenKey = Object.keys(credentials).find(
74
+ (k) =>
75
+ k.endsWith("_ACCESS_TOKEN") ||
76
+ k.endsWith("_TOKEN"),
77
+ );
78
+ const tokenValue = tokenKey ? credentials[tokenKey] : undefined;
79
+ if (tokenKey && tokenValue) {
80
+ return new BearerToken(tokenValue);
81
+ }
82
+ }
83
+ }
84
+
85
+ // Fallback: treat the first credential value as a Bearer token
86
+ const firstValue = Object.values(credentials)[0];
87
+ if (firstValue !== undefined) {
88
+ return new BearerToken(firstValue);
89
+ }
90
+
91
+ return undefined;
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Auth config extraction
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /** Shape of the `utdk` field in a provider's package.json. */
99
+ interface UtdkField {
100
+ auth?: UtdkAuthConfig[];
101
+ [key: string]: unknown;
102
+ }
103
+
104
+ /** Shape of a provider's package.json. */
105
+ interface ProviderPackageJson {
106
+ utdk?: UtdkField;
107
+ [key: string]: unknown;
108
+ }
109
+
110
+ /**
111
+ * Extracts the `utdk.auth` array from a provider module's package.json.
112
+ * Returns an empty array if the module does not export a `packageJson`
113
+ * property or if the config is absent.
114
+ */
115
+ export function extractAuthConfigs(providerModule: Record<string, unknown>): UtdkAuthConfig[] {
116
+ const pkg = providerModule["packageJson"] as ProviderPackageJson | undefined;
117
+ return pkg?.utdk?.auth ?? [];
118
+ }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Operation resolution
122
+ // ---------------------------------------------------------------------------
123
+
124
+ /**
125
+ * Resolves a dot-notation operation path on a client object.
126
+ *
127
+ * @example
128
+ * resolveOperation(client, 'users.getByUsername')
129
+ * // → client.users.getByUsername (as a bound function)
130
+ */
131
+ export function resolveOperation(
132
+ client: unknown,
133
+ operationPath: string,
134
+ ): (...args: unknown[]) => Promise<unknown> {
135
+ const segments = operationPath.split(".");
136
+ let current: unknown = client;
137
+
138
+ for (const segment of segments) {
139
+ if (current === null || current === undefined || typeof current !== "object") {
140
+ throw new TypeError(
141
+ `Cannot resolve operation path "${operationPath}": "${segment}" is not an object`,
142
+ );
143
+ }
144
+ current = (current as Record<string, unknown>)[segment];
145
+ }
146
+
147
+ if (typeof current !== "function") {
148
+ throw new TypeError(
149
+ `Operation "${operationPath}" resolved to ${typeof current}, expected a function`,
150
+ );
151
+ }
152
+
153
+ return current as (...args: unknown[]) => Promise<unknown>;
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Bridge creation
158
+ // ---------------------------------------------------------------------------
159
+
160
+ export interface BridgeOptions {
161
+ /** The resolved operation function (from host context) */
162
+ operation: (...args: unknown[]) => Promise<unknown>;
163
+ }
164
+
165
+ /**
166
+ * Creates a bridge object that is safe to expose inside a vm context.
167
+ *
168
+ * The bridge has a single method, `call(args)`, which invokes the pre-resolved
169
+ * provider operation with the given arguments. Credentials are already baked
170
+ * into the operation via the auth provider — the sandbox script never sees
171
+ * them.
172
+ */
173
+ export function createBridge(options: BridgeOptions): { call: (args: Record<string, unknown>) => Promise<unknown> } {
174
+ return {
175
+ async call(args: Record<string, unknown>): Promise<unknown> {
176
+ return options.operation(args);
177
+ },
178
+ };
179
+ }
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // Provider loading
183
+ // ---------------------------------------------------------------------------
184
+
185
+ /** Name of the factory function pattern in provider modules. */
186
+ const CREATE_CLIENT_PATTERN = /^create[A-Z].+Client$/;
187
+
188
+ /**
189
+ * Locates and invokes the `create*Client` factory in a provider module.
190
+ *
191
+ * Provider modules export a `create<Name>Client(options?)` function that
192
+ * returns a `Promise<Client>`. This helper finds it and calls it with the
193
+ * supplied auth provider.
194
+ *
195
+ * @throws if no factory function matching `create*Client` is found.
196
+ */
197
+ export async function loadProviderClient(
198
+ providerModule: Record<string, unknown>,
199
+ auth: AuthProvider | undefined,
200
+ ): Promise<unknown> {
201
+ // Find the first create*Client export
202
+ const factoryEntry = Object.entries(providerModule).find(([name, value]) =>
203
+ CREATE_CLIENT_PATTERN.test(name) && typeof value === "function",
204
+ );
205
+
206
+ if (!factoryEntry) {
207
+ throw new Error(
208
+ "Provider module does not export a create*Client function. " +
209
+ `Exports found: ${Object.keys(providerModule).join(", ")}`,
210
+ );
211
+ }
212
+
213
+ const [, factory] = factoryEntry;
214
+ const client = await (factory as (options?: unknown) => Promise<unknown>)(
215
+ auth ? { auth } : {},
216
+ );
217
+
218
+ return client;
219
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { Isolate } from "./isolate.js";
2
+ export { createSandboxContext } from "./sandbox.js";
3
+ export {
4
+ createBridge,
5
+ extractAuthConfigs,
6
+ loadProviderClient,
7
+ resolveAuthProvider,
8
+ resolveOperation,
9
+ } from "./bridge.js";
10
+ export type { ExecuteOptions, ExecuteResult, TimeoutError, UtdkAuthConfig } from "./types.js";
11
+ export { TimeoutError as IsolateTimeoutError } from "./types.js";
package/src/isolate.ts ADDED
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Isolate — sandboxed Node.js vm execution runtime for @utdk tool calls.
3
+ *
4
+ * Each call to `execute()` runs in a fresh `vm.createContext()` sandbox:
5
+ * - No access to `process.env`, `fs`, `child_process`, `require`, or any
6
+ * other Node.js host API that could leak credentials or touch the disk.
7
+ * - Credentials are injected by the gateway at call time via a controlled
8
+ * bridge object (`__bridge__`). The sandbox script only calls the bridge;
9
+ * it never receives raw credential strings.
10
+ * - Module cache is isolated per call (new context each time, no
11
+ * cross-call state leakage).
12
+ * - Timeout is enforced via `Promise.race`; runaway async operations are
13
+ * rejected with a `TimeoutError`.
14
+ *
15
+ * For synchronous CPU spin-loops an additional `vm.Script` timeout guard is
16
+ * applied; the async watchdog covers everything else.
17
+ *
18
+ * ## Architecture
19
+ *
20
+ * ```
21
+ * caller
22
+ * │
23
+ * ▼
24
+ * Isolate.execute(options)
25
+ * │
26
+ * ├─ [host] dynamically import provider module
27
+ * ├─ [host] resolve auth provider from credentials + utdk.auth config
28
+ * ├─ [host] create provider client with auth provider
29
+ * ├─ [host] resolve operation function (dot-notation path)
30
+ * ├─ [host] create bridge { call(args) => operationFn(args) }
31
+ * │
32
+ * ├─ [vm] createContext(minimal globals + __bridge__ + __args__)
33
+ * └─ [vm] run `__bridge__.call(__args__)` inside sandbox
34
+ * └─ [host, via bridge] call provider with credentials
35
+ * └─ [network] HTTP request with injected auth headers
36
+ * ```
37
+ *
38
+ * ## ESM / vm.Module note
39
+ *
40
+ * Node.js 20+ supports `vm.SourceTextModule` (ESM) with the
41
+ * `--experimental-vm-modules` flag. The current implementation uses the
42
+ * stable `vm.Script` API for the thin orchestration script, which is
43
+ * sufficient because:
44
+ * - The script is one line (`__bridge__.call(__args__)`).
45
+ * - Full provider code runs outside the sandbox (in host context) behind
46
+ * the bridge abstraction.
47
+ *
48
+ * If future requirements call for running provider code itself inside an ESM
49
+ * module sandbox, upgrade to `vm.SourceTextModule` with a custom linker
50
+ * (see `upgrade notes` in the README).
51
+ */
52
+
53
+ import vm from "node:vm";
54
+ import {
55
+ createBridge,
56
+ extractAuthConfigs,
57
+ loadProviderClient,
58
+ resolveAuthProvider,
59
+ resolveOperation,
60
+ } from "./bridge.js";
61
+ import { createSandboxContext } from "./sandbox.js";
62
+ import { TimeoutError, type ExecuteOptions, type ExecuteResult } from "./types.js";
63
+
64
+ /** Script executed inside the sandbox for every tool call. */
65
+ const SANDBOX_SCRIPT = new vm.Script("__bridge__.call(__args__)");
66
+
67
+ /**
68
+ * Sandboxed execution runtime for @utdk tool calls.
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * const isolate = new Isolate();
73
+ * const result = await isolate.execute({
74
+ * module: '@utdk/github',
75
+ * operation: 'users.getByUsername',
76
+ * args: { username: 'octocat' },
77
+ * credentials: { GITHUB_TOKEN: '<injected-by-gateway>' },
78
+ * timeout: 10_000,
79
+ * });
80
+ * console.log(result.data, `(${result.durationMs}ms)`);
81
+ * ```
82
+ */
83
+ export class Isolate {
84
+ /**
85
+ * Executes a single @utdk tool call inside a fresh vm sandbox.
86
+ *
87
+ * Steps:
88
+ * 1. Dynamically import the provider module (host context, cached by Node.js).
89
+ * 2. Resolve an `AuthProvider` from the supplied credentials and the
90
+ * provider's `utdk.auth` config. Credentials are NEVER written to
91
+ * `process.env`.
92
+ * 3. Instantiate the provider client with the auth provider.
93
+ * 4. Resolve the operation function via dot-notation path.
94
+ * 5. Create a bridge and a fresh vm context.
95
+ * 6. Run the sandbox script `__bridge__.call(__args__)` inside the context.
96
+ * 7. Race the execution against the timeout.
97
+ *
98
+ * @throws `TimeoutError` if execution exceeds `options.timeout`.
99
+ * @throws `TypeError` if the operation path does not resolve to a function.
100
+ * @throws `Error` on provider loading or HTTP failures.
101
+ */
102
+ async execute(options: ExecuteOptions): Promise<ExecuteResult> {
103
+ const {
104
+ module: moduleName,
105
+ operation,
106
+ args = {},
107
+ credentials = {},
108
+ timeout = 30_000,
109
+ } = options;
110
+
111
+ const startMs = performance.now();
112
+
113
+ // ------------------------------------------------------------------
114
+ // Step 1: Load the provider module in HOST context
115
+ // ------------------------------------------------------------------
116
+ // Dynamic import is Node.js-cached so repeated calls to the same
117
+ // provider do not re-parse the module. The provider runs in host
118
+ // context (not in the sandbox) — it is trusted code.
119
+ const providerModule = (await import(moduleName)) as Record<string, unknown>;
120
+
121
+ // ------------------------------------------------------------------
122
+ // Step 2: Resolve auth provider from credentials
123
+ // ------------------------------------------------------------------
124
+ const authConfigs = extractAuthConfigs(providerModule);
125
+ const auth = resolveAuthProvider(credentials, authConfigs);
126
+
127
+ // ------------------------------------------------------------------
128
+ // Step 3: Create the provider client with credential injection
129
+ // ------------------------------------------------------------------
130
+ const client = await loadProviderClient(providerModule, auth);
131
+
132
+ // ------------------------------------------------------------------
133
+ // Step 4: Resolve the operation function
134
+ // ------------------------------------------------------------------
135
+ const operationFn = resolveOperation(client, operation);
136
+
137
+ // ------------------------------------------------------------------
138
+ // Step 5: Create the credential bridge and sandboxed vm context
139
+ // ------------------------------------------------------------------
140
+ // The bridge is created in host context and passed into the sandbox.
141
+ // The sandbox script can only call bridge.call(args) — it cannot
142
+ // reach process.env, require, fs, or any host API.
143
+ const bridge = createBridge({ operation: operationFn });
144
+
145
+ const context = createSandboxContext({
146
+ extraGlobals: {
147
+ __bridge__: bridge,
148
+ __args__: args,
149
+ },
150
+ });
151
+
152
+ // ------------------------------------------------------------------
153
+ // Step 6 + 7: Run inside sandbox, race against timeout
154
+ // ------------------------------------------------------------------
155
+ const data = await this._runWithTimeout(context, timeout);
156
+
157
+ const durationMs = performance.now() - startMs;
158
+ return { data, durationMs };
159
+ }
160
+
161
+ /**
162
+ * Runs `SANDBOX_SCRIPT` in the given context and races against a timeout.
163
+ *
164
+ * The synchronous `vm.Script` timeout (`timeout` option in runInContext)
165
+ * guards against infinite synchronous loops. The async watchdog (Promise.race)
166
+ * guards against long-running await chains.
167
+ */
168
+ private _runWithTimeout(context: vm.Context, timeoutMs: number): Promise<unknown> {
169
+ // The synchronous portion of the script (if any) is bounded by the vm timeout.
170
+ // For the async portion we use Promise.race.
171
+ let resultPromise: Promise<unknown>;
172
+
173
+ try {
174
+ // runInContext returns whatever the script evaluates to.
175
+ // For our one-liner that calls an async bridge method, this is a Promise.
176
+ const scriptResult = SANDBOX_SCRIPT.runInContext(context, {
177
+ // Guard against synchronous infinite loops / busy spins.
178
+ // This only applies to the synchronous execution phase; async
179
+ // continuations are handled by the Promise.race below.
180
+ timeout: timeoutMs,
181
+ }) as unknown;
182
+
183
+ resultPromise = Promise.resolve(scriptResult);
184
+ } catch (err) {
185
+ return Promise.reject(err);
186
+ }
187
+
188
+ // Async watchdog
189
+ const timeoutPromise = new Promise<never>((_, reject) => {
190
+ const timer = setTimeout(() => {
191
+ reject(new TimeoutError(timeoutMs));
192
+ }, timeoutMs);
193
+
194
+ // Don't keep the process alive if only the timer is pending
195
+ if (typeof (timer as NodeJS.Timeout).unref === "function") {
196
+ (timer as NodeJS.Timeout).unref();
197
+ }
198
+ });
199
+
200
+ return Promise.race([resultPromise, timeoutPromise]);
201
+ }
202
+ }
package/src/sandbox.ts ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Sandbox context creation for the Isolate runtime.
3
+ *
4
+ * A sandboxed vm context is created per tool call. The context provides a
5
+ * minimal set of safe globals and explicitly excludes `process`, `require`,
6
+ * `fs`, `child_process`, and other Node.js host APIs that could leak
7
+ * environment variables or allow filesystem/subprocess access.
8
+ *
9
+ * The credential bridge (`__bridge__`) and call arguments (`__args__`) are
10
+ * the only channels through which external state enters the sandbox.
11
+ */
12
+
13
+ import vm from "node:vm";
14
+
15
+ type ConsoleMethod = "log" | "error" | "warn" | "info" | "debug";
16
+
17
+ const PREFIXED_METHODS: ConsoleMethod[] = ["log", "error", "warn", "info", "debug"];
18
+
19
+ function createPrefixedConsole(prefix: string): Record<ConsoleMethod, (...args: unknown[]) => void> {
20
+ const console_: Pick<Console, ConsoleMethod> = console;
21
+ return Object.fromEntries(
22
+ PREFIXED_METHODS.map((method) => [method, (...args: unknown[]) => console_[method](prefix, ...args)]),
23
+ ) as Record<ConsoleMethod, (...args: unknown[]) => void>;
24
+ }
25
+
26
+ const sandboxConsole = createPrefixedConsole("[sandbox]");
27
+
28
+ export interface SandboxOptions {
29
+ /**
30
+ * Additional globals to expose inside the vm context.
31
+ * Use sparingly; every addition is a potential escape hatch.
32
+ */
33
+ extraGlobals?: Record<string, unknown>;
34
+ }
35
+
36
+ /**
37
+ * Creates a fresh, restricted vm context.
38
+ *
39
+ * Accessible globals:
40
+ * console, JSON, Math, Date, Promise, Array, Object, String, Number,
41
+ * Boolean, Error, TypeError, RangeError, Map, Set, WeakMap, WeakSet,
42
+ * Symbol, typed arrays, ArrayBuffer, URL, URLSearchParams,
43
+ * TextEncoder, TextDecoder, setTimeout, clearTimeout, setInterval,
44
+ * clearInterval, parseInt, parseFloat, isNaN, isFinite, encodeURI,
45
+ * decodeURI, encodeURIComponent, decodeURIComponent, Infinity, NaN,
46
+ * undefined.
47
+ *
48
+ * Deliberately excluded:
49
+ * process, require, global, Buffer, __dirname, __filename,
50
+ * fs, child_process, net, http, https, os, crypto (node:crypto),
51
+ * and any other Node.js built-in that exposes host resources.
52
+ *
53
+ * @param options - Optional extra globals to inject.
54
+ * @returns A contextified vm sandbox object.
55
+ */
56
+ export function createSandboxContext(options: SandboxOptions = {}): vm.Context {
57
+ const sandbox: Record<string, unknown> = {
58
+ // Safe builtins
59
+ console: sandboxConsole,
60
+ JSON,
61
+ Math,
62
+ Date,
63
+ Promise,
64
+ Array,
65
+ Object,
66
+ String,
67
+ Number,
68
+ Boolean,
69
+ Error,
70
+ EvalError,
71
+ RangeError,
72
+ ReferenceError,
73
+ SyntaxError,
74
+ TypeError,
75
+ URIError,
76
+ Map,
77
+ Set,
78
+ WeakMap,
79
+ WeakSet,
80
+ WeakRef,
81
+ Symbol,
82
+ BigInt,
83
+
84
+ // Typed arrays
85
+ ArrayBuffer,
86
+ SharedArrayBuffer,
87
+ DataView,
88
+ Int8Array,
89
+ Uint8Array,
90
+ Uint8ClampedArray,
91
+ Int16Array,
92
+ Uint16Array,
93
+ Int32Array,
94
+ Uint32Array,
95
+ Float32Array,
96
+ Float64Array,
97
+ BigInt64Array,
98
+ BigUint64Array,
99
+
100
+ // Web-compatible globals
101
+ URL,
102
+ URLSearchParams,
103
+ TextEncoder,
104
+ TextDecoder,
105
+
106
+ // Timers (these are host-side and safe to expose)
107
+ setTimeout,
108
+ clearTimeout,
109
+ setInterval,
110
+ clearInterval,
111
+
112
+ // Standard globals
113
+ parseInt,
114
+ parseFloat,
115
+ isNaN,
116
+ isFinite,
117
+ encodeURI,
118
+ decodeURI,
119
+ encodeURIComponent,
120
+ decodeURIComponent,
121
+ Infinity,
122
+ NaN,
123
+ undefined,
124
+
125
+ // Explicitly do NOT add:
126
+ // process — would expose process.env (credential leak)
127
+ // require — would allow arbitrary module imports
128
+ // global — alias for Node.js global; exposes process
129
+ // Buffer — not needed in sandboxed operations
130
+ // __dirname — filesystem information leak
131
+ // __filename — filesystem information leak
132
+ // fetch — injected separately per-operation via bridge (see bridge.ts)
133
+ // crypto — would allow subtle attacks; excluded for now
134
+
135
+ ...options.extraGlobals,
136
+ };
137
+
138
+ return vm.createContext(sandbox);
139
+ }