/** * Public types for `@directive-run/sandbox`. * * The package executes user-supplied Directive snippets in a bounded * worker_threads sandbox and returns a structured transcript. The * shape below is what the MCP `run_in_sandbox` tool returns to the * AI client and what `directive.run/playground`'s `/api/run-sandbox` * route serializes to JSON. */ interface PlaygroundFile { /** * Relative path inside the project, e.g. "src/main.ts" or * "src/counter.ts". One file should be "src/main.ts" — that's * the entry point the runner targets. */ path: string; /** File contents. */ source: string; } interface RunInSandboxInput { /** Single-file shortcut. Mapped onto "src/main.ts" internally. */ source?: string; /** Multi-file payload (the `generate_module` paired output shape). */ files?: PlaygroundFile[]; /** Wall-clock timeout in milliseconds. Defaults to 5000. Clamped to [100, 10000]. */ timeoutMs?: number; /** * Optional cancellation signal. Wire this to your HTTP request's * AbortSignal (Next.js / Express both expose one) so a client that * disconnects mid-flight releases its worker slot immediately * instead of leaking it. Without a signal, an abandoned `runInSandbox` * call still queues into the per-process worker cap and only frees * when the worker times out — under load this drives the cap to * permanent deadlock. */ signal?: AbortSignal; } interface SandboxResult { /** * Captured `console.log` / `console.warn` / `console.error` lines, * in dispatch order. Each entry is the stringified arguments * joined by " " (matches Node's default console format). */ logs: string[]; /** * Final `system.facts.$store.toObject()` snapshot at end-of-run. * Empty when no system was constructed (e.g. validator rejection). */ facts: Record; /** * Final `system.derive` snapshot — every derivation declared in the * module config, evaluated by reading `system.derive[key]`. Empty * when the module has no `derive:` block or when validation rejected * before bundle. The June 2026 security audit flagged the original * sandbox for snapshotting only facts; modules whose primary product * is a derivation (`status`, `isReady`, etc.) returned an empty- * looking transcript. */ derived: Record; /** * Structured error messages from validation, bundling, or runtime * exceptions. Empty on a clean run. */ errors: string[]; /** Elapsed wall-clock duration of the worker execution. */ durationMs: number; /** True when the wall-clock budget elapsed before settle()/destroy(). */ timedOut: boolean; } type SandboxErrorCode = "validation-failed" | "bundle-failed" | "worker-error" | "timeout" | "input-invalid"; declare class SandboxError extends Error { readonly code: SandboxErrorCode; constructor(message: string, code: SandboxErrorCode); } /** * AST allowlist validator. Pre-flights every file in the payload * BEFORE the bundler so a hostile snippet never reaches the runtime * surface. Without this layer, `worker_threads` resource limits * (heap-only) leak FS + network access through — a snippet that * `import("node:fs")` would still pwn the host process. * * Allowlist: * * - Imports: must match `@directive-run/*` (specifically `core`, `ai`, * `query`) OR a relative path ending in `.js` (the multi-file * payload's own files). * - Identifier accesses: only the allowlisted Directive API surface * plus `console.*`, `Math.*`, `JSON.*`. Anything that touches * global Node namespaces (`process`, `require`, `fs`, `child_process`, * `net`, `dgram`, `cluster`, etc.) is rejected. * * Strict by default — we'd rather reject a valid pattern (and learn * about it via a real-world report) than ship a "mostly safe" sandbox. * The Phase 2 plan calls out that we expand based on actual failures. * * Returns the list of validation errors; an empty list means safe to * bundle + execute. Callers should bail on any non-empty result. */ interface ValidationError { path: string; line: number; column: number; message: string; } /** * Host-side worker_threads orchestration. Mirrors the lint-runner * pattern in `@directive-run/mcp`: spawn a fresh worker per call, * race the response against a wall-clock timer, terminate on overrun. * * Workers are NOT pooled. Each call gets a clean process state — no * carry-over globals between snippets, no shared `console` patches, * no leaked timers from a prior run. Cold-start is ~5ms which is * cheap relative to the 50-200ms a typical Directive demo actually * spends in `system.settle()`. */ /** * Override the per-process worker cap. Pass `Infinity` to disable. * Returns the previous value. * * Lowering the cap below `activeWorkers` does NOT terminate running * workers — the new ceiling applies as those workers drain. New * `acquireSlot()` callers queue until `activeWorkers` falls below * the new cap. Raising the cap immediately drains any waiters the * new ceiling can absorb. * * @example Cap the worker pool at boot for a Next.js API route * ```ts * // app/api/sandbox/route.ts — runs once per cold start * import { setMaxConcurrentWorkers } from "@directive-run/sandbox"; * setMaxConcurrentWorkers(8); * ``` */ declare function setMaxConcurrentWorkers(value: number): number; declare function sanitizeStack(s: string | undefined): string; /** * Public API for `@directive-run/sandbox`. * * Single entry point: `runInSandbox({files, timeoutMs})` validates the * payload against the AST allowlist, bundles the multi-file payload via * esbuild, and executes the result in a bounded worker_threads sandbox. * Returns a structured `SandboxResult` with captured logs, the final * facts snapshot, and any errors that occurred at any stage. * * Consumers: * * - `@directive-run/mcp` — the `run_in_sandbox` MCP tool returns the * result to AI clients alongside a `playground_link` URL. * - `directive-docs` — the `/api/run-sandbox` Next.js route wraps this * for the playground page's live DevTools transcript view. */ declare function runInSandbox(input: RunInSandboxInput): Promise; export { type PlaygroundFile, type RunInSandboxInput, SandboxError, type SandboxResult, type ValidationError, runInSandbox, sanitizeStack, setMaxConcurrentWorkers };