/** * SandboxEngine — Zero-Trust V8 Isolate for Computation Delegation * * Allows LLMs to send JavaScript functions as strings to be executed * in a sealed V8 isolate. The data stays on the client's machine, * only the result travels back to the LLM. * * Architecture (V8 Engineering Rules): * 1. ONE Isolate per SandboxEngine (boot ~5-10ms), reused across requests * 2. NEW Context per execute() call (~0.1ms), pristine and empty * 3. ExternalCopy + Script + Context are ALWAYS released in `finally` * 4. Execution is ALWAYS async (script.run, never runSync) * 5. Context is empty — no process, require, fs, globalThis injected * 6. AbortSignal kills isolate.dispose() instantly (Connection Watchdog) * * The `isolated-vm` package is a peerDependency (optional). * If not installed, the engine throws a clear error at construction time. * * ┌─────────────────────────────────────────────────────────┐ * │ SandboxEngine (owns 1 Isolate) │ * │ │ * │ execute(code, data, { signal? }) │ * │ ┌──────────┐ │ * │ │ Abort? │ pre-flight signal check │ * │ ├──────────┤ │ * │ │ Guard │ fail-fast syntax check │ * │ ├──────────┤ │ * │ │ Context │ new per request (empty!) │ * │ ├──────────┤ │ * │ │ Copy In │ ExternalCopy (deep, no refs) │ * │ ├──────────┤ │ * │ │ Compile │ isolate.compileScript │ * │ ├──────────┤ │ * │ │ Run │ script.run (ASYNC, with timeout + abort) │ * │ ├──────────┤ │ * │ │ Copy Out │ JSON.parse result │ * │ └──────────┘ │ * │ │ * │ finally: signal.removeEventListener() │ * │ inputCopy.release() │ * │ script.release() │ * │ context.release() │ * └─────────────────────────────────────────────────────────┘ * * @module */ import { type TelemetrySink } from '../observability/TelemetryEvent.js'; /** * Configuration for a SandboxEngine instance. * * All fields are optional — sensible defaults are applied. * * @example * ```typescript * const engine = new SandboxEngine({ * timeout: 3000, // Kill after 3s * memoryLimit: 64, // 64MB per isolate * maxOutputBytes: 512_000, // 500KB max result * }); * ``` */ export interface SandboxConfig { /** * Maximum execution time in milliseconds. * If the script exceeds this, the V8 isolate kills it. * @default 5000 */ timeout?: number; /** * Maximum memory for the V8 isolate in megabytes. * If exceeded, the isolate dies and is recreated on next call. * @default 128 */ memoryLimit?: number; /** * Maximum size of the serialized output in bytes. * Prevents a malicious script from returning gigabytes of data. * @default 1_048_576 (1MB) */ maxOutputBytes?: number; } /** * Error codes for sandbox execution failures. * * - `TIMEOUT`: Script exceeded the time limit * - `MEMORY`: Isolate ran out of memory (auto-recovered) * - `SYNTAX`: JavaScript syntax error in the provided code * - `RUNTIME`: Script threw an error during execution * - `OUTPUT_TOO_LARGE`: Result exceeds `maxOutputBytes` * - `INVALID_CODE`: Failed the SandboxGuard fail-fast check * - `INVALID_DATA`: Input data contains non-serializable values * - `UNAVAILABLE`: `isolated-vm` is not installed * - `ABORTED`: Execution was cancelled via AbortSignal (client disconnect) */ export type SandboxErrorCode = 'TIMEOUT' | 'MEMORY' | 'SYNTAX' | 'RUNTIME' | 'OUTPUT_TOO_LARGE' | 'INVALID_CODE' | 'INVALID_DATA' | 'UNAVAILABLE' | 'ABORTED'; /** * Result of a sandbox execution. * * @example * ```typescript * const result = await engine.execute('(data) => data.length', [1, 2, 3]); * if (result.ok) { * console.log(result.value); // 3 * console.log(result.executionMs); // 0.42 * } else { * console.log(result.code); // 'TIMEOUT' * console.log(result.error); // 'Script execution timed out (5000ms)' * } * ``` */ export type SandboxResult = { readonly ok: true; readonly value: T; readonly executionMs: number; } | { readonly ok: false; readonly error: string; readonly code: SandboxErrorCode; }; /** * Reset the cached isolated-vm module reference. * Exported exclusively for testing — allows mock/unmock cycles * without process restart (Bug #137). * @internal */ export declare function resetIvmCache(): void; /** * Zero-trust V8 sandbox for executing LLM-provided JavaScript. * * Creates a single V8 `Isolate` at construction time and reuses it * across all `execute()` calls. Each call gets a fresh, empty `Context` * with no dangerous globals (no `process`, `require`, `fs`, etc.). * * If the isolate dies (e.g., OOM), it is automatically recreated * on the next `execute()` call. * * @example * ```typescript * const sandbox = new SandboxEngine({ timeout: 3000, memoryLimit: 64 }); * * const result = await sandbox.execute( * '(data) => data.filter(d => d.risk > 90)', * [{ name: 'A', risk: 95 }, { name: 'B', risk: 30 }], * ); * * if (result.ok) { * console.log(result.value); // [{ name: 'A', risk: 95 }] * } * * // IMPORTANT: dispose when no longer needed * sandbox.dispose(); * ``` */ export declare class SandboxEngine { private readonly _timeout; private readonly _memoryLimit; private readonly _maxOutputBytes; private _isolate; private _disposed; private _telemetry?; /** Number of in-flight execute() calls sharing the isolate. */ private _activeExecutions; constructor(config?: SandboxConfig); /** * Set the telemetry sink for `sandbox.exec` event emission. * When set, every `execute()` call emits a telemetry event * visible in the Inspector TUI. */ telemetry(sink: TelemetrySink): this; /** * Execute a JavaScript function string against the provided data. * * The function is compiled and run in a sealed V8 isolate with: * - No `process`, `require`, `fs`, or network access * - Strict timeout enforcement (async, non-blocking) * - Memory limit enforcement * - Automatic C++ pointer cleanup (ExternalCopy, Script, Context) * - Cooperative cancellation via AbortSignal (Connection Watchdog) * * @param code - A JavaScript function expression as a string. * Must be an arrow function or function expression. * Example: `(data) => data.filter(d => d.value > 10)` * * @param data - The data to pass into the function. * Deeply copied into the isolate (no references leak). * * @param options - Optional execution options. * @param options.signal - AbortSignal for cooperative cancellation. * When the signal fires (e.g., MCP client disconnects), the engine * calls `isolate.dispose()` to kill the V8 C++ threads instantly. * The isolate is auto-recovered on the next `.execute()` call. * * @returns A `SandboxResult` with the computed value or an error. */ execute(code: string, data: unknown, options?: { signal?: AbortSignal; }): Promise>; /** * Release all resources held by this engine. * * After calling `dispose()`, any subsequent `execute()` calls * will return `{ ok: false, code: 'UNAVAILABLE' }`. */ dispose(): void; /** * Check if the engine has been disposed. */ get isDisposed(): boolean; /** * Emit a `sandbox.exec` telemetry event if a sink is configured. * @internal */ private _emitTelemetry; /** * Ensure the isolate is alive. If it died (OOM), create a new one. * @internal */ private _ensureIsolate; /** * Classify an error from V8 execution into a typed SandboxResult. * @internal */ private _classifyError; } //# sourceMappingURL=SandboxEngine.d.ts.map