@directive-run/sandbox 0.3.13 → 0.4.0
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.
- package/README.md +36 -2
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -2
- package/dist/index.d.ts +45 -2
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/worker.js +3 -3
- package/dist/worker.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/bundler.ts","../src/host.ts","../src/key-extractor.ts","../src/types.ts","../src/validator.ts","../src/index.ts"],"sourcesContent":["/**\n * esbuild adapter for the sandbox. Takes the user's multi-file payload\n * and produces a single ESM string the worker can evaluate.\n *\n * `@directive-run/*` packages are marked **external** — the worker\n * imports them at runtime via Node's normal ESM resolver, which finds\n * them in `node_modules`. We don't inline them because (a) bundle size\n * would explode, (b) they're already cached in the parent process so\n * the runtime resolver is fast.\n *\n * The bundler uses the in-memory plugin pattern: every file in the\n * payload is registered as a virtual entry, and relative imports\n * resolve against the in-memory map. esbuild handles TS → JS, top-\n * level await, and ESM linking in one pass.\n */\n\nimport { createRequire } from \"node:module\";\nimport { pathToFileURL } from \"node:url\";\nimport { build } from \"esbuild\";\nimport type { PlaygroundFile } from \"./types.js\";\n\n/**\n * Resolve a `@directive-run/*` bare specifier to an absolute file://\n * URL using the host process's node_modules. Required because the\n * worker imports the bundle from `/tmp` (Phase A P0-A1) where Node's\n * ESM resolver can't walk up to find `@directive-run/*`. By rewriting\n * to absolute file URLs at bundle time, the worker doesn't need a\n * node_modules anchor.\n *\n * Returns null when the package can't be resolved (e.g. consumer has\n * a different install layout); the caller falls back to leaving the\n * bare specifier in place, which works when the worker IS next to\n * node_modules.\n */\nfunction resolveDirectivePackageToFileUrl(specifier: string): string | null {\n try {\n const require = createRequire(import.meta.url);\n const resolved = require.resolve(specifier);\n return pathToFileURL(resolved).href;\n } catch {\n return null;\n }\n}\n\nconst ENTRY_PATH = \"src/main.ts\";\nconst VIRTUAL_NAMESPACE = \"directive-sandbox-vfs\";\n\nexport interface BundleResult {\n /** Single ESM string ready to evaluate inside the worker. */\n source: string;\n /** Total bytes of the bundled JS (informational). */\n bytes: number;\n}\n\nexport class BundleError extends Error {\n constructor(\n message: string,\n public readonly cause?: unknown,\n ) {\n super(message);\n this.name = \"BundleError\";\n }\n}\n\n/**\n * Find the `var system = createSystem(...)` declaration esbuild\n * emits for the runner and inject a side-channel global assignment\n * immediately after it. Captures the system BEFORE any subsequent\n * `system.start()`, dispatches, or `await system.settle()` can\n * throw — so the worker can post-mortem facts even on a runtime\n * error inside the user's runner.\n *\n * esbuild's output uses `var` for hoisted top-level bindings, and\n * the entry's `const system = createSystem(...)` becomes one of:\n *\n * var system = createSystem({ ... });\n * var system2 = createSystem({ ... }); // when \"system\" collides\n *\n * The regex tolerates both. If no match, returns the source\n * unchanged (the entry-file epilogue still handles the clean-run\n * case via the appended assignment).\n */\nfunction injectEarlyCapture(bundled: string): string {\n const re = /(var\\s+(system\\d*)\\s*=\\s*createSystem\\s*\\([\\s\\S]*?\\)\\s*;)/;\n const match = bundled.match(re);\n if (!match) {\n return bundled;\n }\n const decl = match[1]!;\n const localName = match[2]!;\n const capture = `\\n(globalThis).__directiveSandbox_system__ = ${localName};\\n`;\n return bundled.replace(decl, decl + capture);\n}\n\nexport async function bundleSandboxFiles(\n files: PlaygroundFile[],\n): Promise<BundleResult> {\n const entry = files.find((f) => f.path === ENTRY_PATH);\n if (!entry) {\n throw new BundleError(\n `payload must include \"${ENTRY_PATH}\" — that's the entry point the runner targets`,\n );\n }\n\n const fileMap = new Map<string, string>();\n for (const file of files) {\n fileMap.set(file.path, file.source);\n }\n\n try {\n const result = await build({\n entryPoints: [`${VIRTUAL_NAMESPACE}:${ENTRY_PATH}`],\n bundle: true,\n format: \"esm\",\n target: \"node20\",\n platform: \"node\",\n write: false,\n logLevel: \"silent\",\n // The plugin's onResolve below intercepts every `@directive-run/*`\n // import and rewrites it to an absolute file:// URL (so the\n // worker can import the bundle from /tmp without needing\n // node_modules above it). The wildcard external is the safety\n // net for any specifier the plugin doesn't recognize.\n external: [\"@directive-run/*\"],\n // Top-level await is required (the runner does `await system.settle()`)\n // and node20 supports it natively.\n supported: { \"top-level-await\": true },\n plugins: [\n {\n name: \"directive-sandbox-vfs\",\n setup(b) {\n b.onResolve({ filter: /.*/ }, (args) => {\n if (args.kind === \"entry-point\") {\n const path = args.path.replace(`${VIRTUAL_NAMESPACE}:`, \"\");\n return { path, namespace: VIRTUAL_NAMESPACE };\n }\n if (args.namespace !== VIRTUAL_NAMESPACE) {\n return null;\n }\n if (args.path.startsWith(\"@directive-run/\")) {\n // Phase A audit P0-A1: rewrite to absolute file:// URL\n // so the worker's `/tmp/.../bundle.mjs` can import the\n // package without needing node_modules above /tmp.\n // Falls back to the bare specifier when resolution\n // fails — the legacy \"write next to node_modules\"\n // pattern still works as a backup.\n const resolved = resolveDirectivePackageToFileUrl(args.path);\n if (resolved) {\n return { external: true, path: resolved };\n }\n return { external: true, path: args.path };\n }\n if (args.path.startsWith(\"./\") || args.path.startsWith(\"../\")) {\n // Resolve relative to the importer's path.\n const importerDir = args.importer.replace(/\\/[^/]+$/, \"\");\n // The runner imports `./counter.js`; the actual virtual\n // file lives at `src/counter.ts`. Strip the `.js` and\n // try both extensions so users can write either.\n const stripped = args.path\n .replace(/^\\.\\//, \"\")\n .replace(/\\.js$/, \"\");\n const candidates = [\n `${importerDir}/${stripped}.ts`,\n `${importerDir}/${stripped}.js`,\n `${importerDir}/${stripped}`,\n // Also try the raw path in case the importer's dir\n // resolution misses the leading slash.\n `${stripped}.ts`,\n `${stripped}.js`,\n ].map((p) => p.replace(/\\/+/g, \"/\"));\n\n for (const candidate of candidates) {\n if (fileMap.has(candidate)) {\n return { path: candidate, namespace: VIRTUAL_NAMESPACE };\n }\n }\n throw new BundleError(\n `cannot resolve \"${args.path}\" from \"${args.importer}\" — tried ${candidates.join(\", \")}`,\n );\n }\n throw new BundleError(\n `unexpected import \"${args.path}\" from \"${args.importer}\" — only relative or @directive-run/* allowed`,\n );\n });\n b.onLoad({ filter: /.*/, namespace: VIRTUAL_NAMESPACE }, (args) => {\n const contents = fileMap.get(args.path);\n if (contents === undefined) {\n throw new BundleError(`virtual file missing: ${args.path}`);\n }\n // For the entry file, append a trailing assignment so the\n // runner's top-level `const system = …` binding ends up on\n // a side-channel global the worker reads after execution.\n // ESM module exports are sealed, so we can't patch\n // `createSystem` post-import; instead we lift the binding\n // out of the entry module's scope here. The convention is\n // documented by `@directive-run/scaffold`'s `generateRunner`\n // which always names the local binding `system`.\n if (args.path === ENTRY_PATH) {\n const epilogue = [\n \"\",\n \"// directive-sandbox: lift the runner's `system` binding\",\n \"// onto a side-channel global so the worker can read\",\n \"// system.facts.$store.toObject() after execution.\",\n \"if (typeof system !== 'undefined') {\",\n \" (globalThis).__directiveSandbox_system__ = system;\",\n \"}\",\n ].join(\"\\n\");\n return { contents: contents + epilogue, loader: \"ts\" };\n }\n return { contents, loader: \"ts\" };\n });\n },\n },\n ],\n });\n const rawSource = result.outputFiles[0]?.text;\n if (!rawSource) {\n throw new BundleError(\"esbuild produced no output\");\n }\n // Inject an early-capture immediately after `createSystem(...)` so\n // the worker can read facts even when subsequent runner code throws\n // (e.g. an async settle() rejection from a bad event payload). The\n // entry-file epilogue (appended in onLoad) only fires on a clean\n // run; this regex fires regardless. Idempotent — duplicate\n // assignments to the same global are harmless.\n const source = injectEarlyCapture(rawSource);\n return { source, bytes: Buffer.byteLength(source, \"utf8\") };\n } catch (err) {\n if (err instanceof BundleError) {\n throw err;\n }\n throw new BundleError(\n `bundle failed: ${(err as Error).message ?? String(err)}`,\n err,\n );\n }\n}\n","/**\n * Host-side worker_threads orchestration. Mirrors the lint-runner\n * pattern in `@directive-run/mcp`: spawn a fresh worker per call,\n * race the response against a wall-clock timer, terminate on overrun.\n *\n * Workers are NOT pooled. Each call gets a clean process state — no\n * carry-over globals between snippets, no shared `console` patches,\n * no leaked timers from a prior run. Cold-start is ~5ms which is\n * cheap relative to the 50-200ms a typical Directive demo actually\n * spends in `system.settle()`.\n */\n\nimport { existsSync, mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { Worker } from \"node:worker_threads\";\nimport type {\n SandboxResult,\n WorkerInputMessage,\n WorkerOutputMessage,\n} from \"./types.js\";\n\nconst MIN_TIMEOUT_MS = 100;\nconst MAX_TIMEOUT_MS = 10_000;\nconst DEFAULT_TIMEOUT_MS = 5_000;\n\nasync function resolveWorkerPath(): Promise<string> {\n // PRIMARY: static URL relative to this module's own file. Bundlers\n // (Next.js outputFileTracing, esbuild, webpack, etc.) follow this\n // static reference at build time and include `worker.js` in the\n // output bundle automatically. Works on Vercel + AWS Lambda + Cloud\n // Run without any consumer-side config.\n //\n // FALLBACK (Vitest dev path): `import.meta.url` resolves to the\n // .ts source file in tests, so `./worker.js` doesn't exist there —\n // fall through to `createRequire(import.meta.url).resolve(...)`\n // which Vitest handles correctly via its SSR loader.\n try {\n const staticUrl = new URL(\"./worker.js\", import.meta.url);\n const path = fileURLToPath(staticUrl);\n if (existsSync(path)) {\n return path;\n }\n } catch {\n // fall through\n }\n const { createRequire } = await import(\"node:module\");\n const require = createRequire(import.meta.url);\n return require.resolve(\"@directive-run/sandbox/worker\");\n}\n\nexport interface HostRunInput {\n bundledSource: string;\n /** Derivation key names extracted from the payload's source files. */\n derivationKeys: string[];\n timeoutMs?: number;\n}\n\nexport class WorkerExecError extends Error {\n constructor(\n message: string,\n public readonly code: \"worker-error\" | \"timeout\",\n ) {\n super(message);\n this.name = \"WorkerExecError\";\n }\n}\n\nfunction clampTimeout(value: number | undefined): number {\n const raw = value ?? DEFAULT_TIMEOUT_MS;\n if (!Number.isFinite(raw)) {\n return DEFAULT_TIMEOUT_MS;\n }\n return Math.min(MAX_TIMEOUT_MS, Math.max(MIN_TIMEOUT_MS, Math.floor(raw)));\n}\n\n/**\n * Try `os.tmpdir()` first (Vercel-friendly: /tmp is the only writable\n * location on serverless functions). Fall back to the sandbox package\n * dir if /tmp ISN'T writable for some reason. The fallback inherits\n * the package's node_modules walking chain so bare specifiers like\n * `@directive-run/core` resolve naturally.\n */\nasync function getTempBundleDir(): Promise<string> {\n const osTmp = tmpdir();\n if (existsSync(osTmp)) {\n return osTmp;\n }\n // Reuse the worker-resolution path so we land at the same package\n // regardless of whether the consumer is in production or Vitest dev.\n const workerPath = await resolveWorkerPath();\n // dist/worker.js → ../ → package root.\n return dirname(dirname(workerPath));\n}\n\n/**\n * Write the bundled snippet to a temp file Node's ESM loader can\n * import via a file:// URL.\n *\n * Phase A audit P0-A1: previous versions wrote inside the sandbox\n * package's own directory so Node's resolver could walk up to find\n * `@directive-run/core` in node_modules — but Vercel / AWS Lambda /\n * Cloud Run all ship read-only FS outside `/tmp`. The bundler now\n * rewrites `@directive-run/*` imports to ABSOLUTE `file://` URLs of\n * the host's resolved paths (see `bundleSandboxFiles`), so the temp\n * file can live in `/tmp` without needing a node_modules anchor.\n *\n * Caller MUST clean the directory up in a finally block.\n */\nasync function writeBundleToTemp(bundledSource: string): Promise<{\n bundlePath: string;\n cleanup: () => void;\n}> {\n const baseDir = await getTempBundleDir();\n const dir = mkdtempSync(join(baseDir, \"directive-sandbox-\"));\n const bundlePath = join(dir, \"bundle.mjs\");\n writeFileSync(bundlePath, bundledSource, \"utf8\");\n return {\n bundlePath,\n cleanup: () => {\n try {\n rmSync(dir, { recursive: true, force: true });\n } catch {\n // best-effort\n }\n },\n };\n}\n\nexport async function execInWorker(\n input: HostRunInput,\n): Promise<SandboxResult> {\n const timeoutMs = clampTimeout(input.timeoutMs);\n const workerPath = await resolveWorkerPath();\n const { bundlePath, cleanup: cleanupTempDir } = await writeBundleToTemp(\n input.bundledSource,\n );\n const worker = new Worker(workerPath, {\n // 32 MB heap ceiling — bounded enough to prevent runaway allocations\n // without crowding the typical demo footprint (~2-5 MB).\n resourceLimits: {\n maxOldGenerationSizeMb: 32,\n maxYoungGenerationSizeMb: 8,\n codeRangeSizeMb: 16,\n },\n // Bypass stderr noise from the worker process showing up in the\n // host's logs. The transcript captures everything we care about.\n stderr: false,\n });\n\n let timer: NodeJS.Timeout | null = null;\n let timedOut = false;\n const startMs = Date.now();\n\n try {\n const result = await new Promise<SandboxResult>((resolve, reject) => {\n let settled = false;\n\n worker.once(\"message\", (msg: WorkerOutputMessage) => {\n settled = true;\n if (msg.ok) {\n resolve(msg.result);\n } else {\n reject(new WorkerExecError(msg.error, \"worker-error\"));\n }\n });\n\n worker.once(\"error\", (err: Error) => {\n settled = true;\n reject(new WorkerExecError(err.message, \"worker-error\"));\n });\n\n worker.once(\"exit\", (code) => {\n if (!settled && code !== 0 && code !== null) {\n reject(\n new WorkerExecError(\n `worker exited with code ${code} before responding`,\n \"worker-error\",\n ),\n );\n }\n });\n\n timer = setTimeout(() => {\n timedOut = true;\n worker.terminate();\n reject(\n new WorkerExecError(\n `wall-clock budget of ${timeoutMs}ms elapsed`,\n \"timeout\",\n ),\n );\n }, timeoutMs);\n\n const message: WorkerInputMessage = {\n bundlePath: pathToFileURL(bundlePath).href,\n timeoutMs,\n derivationKeys: input.derivationKeys,\n };\n worker.postMessage(message);\n });\n\n return result;\n } catch (err) {\n if (err instanceof WorkerExecError && err.code === \"timeout\") {\n // The worker captured nothing because we killed it; surface\n // structured timeout info to the caller.\n return {\n logs: [],\n facts: {},\n derived: {},\n errors: [err.message],\n durationMs: Date.now() - startMs,\n timedOut: true,\n };\n }\n throw err;\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n await worker.terminate().catch(() => undefined);\n cleanupTempDir();\n void timedOut;\n }\n}\n","/**\n * Pure regex-based extractor for `derive:` / `derivations:` block keys\n * across a payload of source files. Mirrors the lightweight pattern the\n * docs site's DevTools panel uses for static-structure parsing.\n *\n * Phase A AE audit (P0-DM2): `system.derive` is a Proxy with no\n * `ownKeys` trap, so the worker can't enumerate derivations from inside\n * the sandbox. Instead, the host pre-scans source files for the\n * declared keys and forwards them to the worker, which then reads\n * `system.derive[key]` for each.\n *\n * Best-effort: a module that builds derivation keys dynamically (e.g.\n * `derive: Object.fromEntries(keys.map(k => [k, fn]))`) won't be\n * extracted. That's acceptable for the audit's stated goal of\n * \"transcript reflects what the module declared.\"\n */\n\nimport type { PlaygroundFile } from \"./types.js\";\n\n/**\n * Find the matching `}` for the `{` at `openBrace`. Returns the index\n * of the close brace, or -1 if unbalanced.\n */\nfunction findMatchingClose(source: string, openBrace: number): number {\n let depth = 0;\n for (let i = openBrace; i < source.length; i++) {\n const ch = source[i];\n if (ch === \"{\") depth += 1;\n else if (ch === \"}\") {\n depth -= 1;\n if (depth === 0) return i;\n }\n }\n return -1;\n}\n\n/**\n * Pull top-level `key:` segments out of a brace-balanced block. Keys\n * are found between `,` or `\\n` or the block start at brace depth 0\n * AND paren depth 0. Works for both multi-line and compact `{ a: 1,\n * b: 2 }` forms.\n */\nfunction collectTopLevelKeys(block: string): string[] {\n const keys: string[] = [];\n let segStart = 0;\n let braceDepth = 0;\n let parenDepth = 0;\n let bracketDepth = 0;\n\n const flushSegment = (end: number) => {\n const seg = block.slice(segStart, end).trim();\n segStart = end + 1;\n if (!seg) return;\n // Tolerate leading quotes for `\"foo\":` quoted keys.\n const m = seg.match(/^['\"]?(\\w+)['\"]?\\s*:/);\n if (m) keys.push(m[1]!);\n };\n\n for (let j = 0; j < block.length; j++) {\n const ch = block[j];\n if (ch === \"{\") braceDepth += 1;\n else if (ch === \"}\") braceDepth -= 1;\n else if (ch === \"(\") parenDepth += 1;\n else if (ch === \")\") parenDepth -= 1;\n else if (ch === \"[\") bracketDepth += 1;\n else if (ch === \"]\") bracketDepth -= 1;\n else if (\n (ch === \",\" || ch === \"\\n\") &&\n braceDepth === 0 &&\n parenDepth === 0 &&\n bracketDepth === 0\n ) {\n flushSegment(j);\n }\n }\n // Final segment (no trailing comma/newline).\n flushSegment(block.length);\n return keys;\n}\n\nfunction extractTopLevelKeys(source: string, sectionName: string): string[] {\n const headerRe = new RegExp(`\\\\b${sectionName}\\\\s*:\\\\s*\\\\{`);\n const headerMatch = source.match(headerRe);\n if (!headerMatch || headerMatch.index === undefined) {\n return [];\n }\n const openBrace = source.indexOf(\"{\", headerMatch.index);\n if (openBrace === -1) {\n return [];\n }\n const closeBrace = findMatchingClose(source, openBrace);\n if (closeBrace === -1) {\n return [];\n }\n return collectTopLevelKeys(source.slice(openBrace + 1, closeBrace));\n}\n\n/**\n * Collect the union of derivation key names declared across all files\n * in the payload. Looks at both `derive:` (module config block) and\n * `derivations:` (schema block). De-duplicates.\n */\nexport function extractDerivationKeys(files: PlaygroundFile[]): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const file of files) {\n for (const k of extractTopLevelKeys(file.source, \"derive\")) {\n if (!seen.has(k)) {\n seen.add(k);\n out.push(k);\n }\n }\n for (const k of extractTopLevelKeys(file.source, \"derivations\")) {\n if (!seen.has(k)) {\n seen.add(k);\n out.push(k);\n }\n }\n }\n return out;\n}\n","/**\n * Public types for `@directive-run/sandbox`.\n *\n * The package executes user-supplied Directive snippets in a bounded\n * worker_threads sandbox and returns a structured transcript. The\n * shape below is what the MCP `run_in_sandbox` tool returns to the\n * AI client and what `directive.run/playground`'s `/api/run-sandbox`\n * route serializes to JSON.\n */\n\nexport interface PlaygroundFile {\n /**\n * Relative path inside the project, e.g. \"src/main.ts\" or\n * \"src/counter.ts\". One file should be \"src/main.ts\" — that's\n * the entry point the runner targets.\n */\n path: string;\n /** File contents. */\n source: string;\n}\n\nexport interface RunInSandboxInput {\n /** Single-file shortcut. Mapped onto \"src/main.ts\" internally. */\n source?: string;\n /** Multi-file payload (the `generate_module` paired output shape). */\n files?: PlaygroundFile[];\n /** Wall-clock timeout in milliseconds. Defaults to 5000. Clamped to [100, 10000]. */\n timeoutMs?: number;\n}\n\nexport interface SandboxResult {\n /**\n * Captured `console.log` / `console.warn` / `console.error` lines,\n * in dispatch order. Each entry is the stringified arguments\n * joined by \" \" (matches Node's default console format).\n */\n logs: string[];\n /**\n * Final `system.facts.$store.toObject()` snapshot at end-of-run.\n * Empty when no system was constructed (e.g. validator rejection).\n */\n facts: Record<string, unknown>;\n /**\n * Final `system.derive` snapshot — every derivation declared in the\n * module config, evaluated by reading `system.derive[key]`. Empty\n * when the module has no `derive:` block or when validation rejected\n * before bundle. The Phase A AE audit (P0-DM2) flagged the original\n * sandbox for snapshotting only facts; modules whose primary product\n * is a derivation (`status`, `isReady`, etc.) returned an empty-\n * looking transcript.\n */\n derived: Record<string, unknown>;\n /**\n * Structured error messages from validation, bundling, or runtime\n * exceptions. Empty on a clean run.\n */\n errors: string[];\n /** Elapsed wall-clock duration of the worker execution. */\n durationMs: number;\n /** True when the wall-clock budget elapsed before settle()/destroy(). */\n timedOut: boolean;\n}\n\nexport type SandboxErrorCode =\n | \"validation-failed\"\n | \"bundle-failed\"\n | \"worker-error\"\n | \"timeout\"\n | \"input-invalid\";\n\nexport class SandboxError extends Error {\n constructor(\n message: string,\n public readonly code: SandboxErrorCode,\n ) {\n super(message);\n this.name = \"SandboxError\";\n }\n}\n\n/**\n * Wire shape for messages from host → worker. The host writes the\n * bundled snippet to a temp file (so Node's ESM loader can resolve\n * `@directive-run/core` against the parent's node_modules; bare\n * specifiers don't resolve from data: URLs) and passes the file URL.\n * Replies on the same channel with a `SandboxResult`.\n */\nexport interface WorkerInputMessage {\n /** `file://` URL of the temp .mjs bundle the host wrote. */\n bundlePath: string;\n timeoutMs: number;\n /**\n * Derivation key names the host extracted from the source files.\n * `system.derive` is a Proxy with no `ownKeys` trap, so we can't\n * enumerate from the worker; the host's pre-bundle pass scans the\n * source for `derive:` and `derivations:` blocks and forwards the\n * keys here. Worker reads `system.derive[key]` for each.\n */\n derivationKeys: string[];\n}\n\n/** Wire shape for messages from worker → host. */\nexport type WorkerOutputMessage =\n | { ok: true; result: SandboxResult }\n | { ok: false; error: string };\n","/**\n * AST allowlist validator. Pre-flights every file in the payload\n * BEFORE the bundler so a hostile snippet never reaches the runtime\n * surface. Without this layer, `worker_threads` resource limits\n * (heap-only) leak FS + network access through — a snippet that\n * `import(\"node:fs\")` would still pwn the host process.\n *\n * Allowlist:\n *\n * - Imports: must match `@directive-run/*` (specifically `core`, `ai`,\n * `query`) OR a relative path ending in `.js` (the multi-file\n * payload's own files).\n * - Identifier accesses: only the allowlisted Directive API surface\n * plus `console.*`, `Math.*`, `JSON.*`. Anything that touches\n * global Node namespaces (`process`, `require`, `fs`, `child_process`,\n * `net`, `dgram`, `cluster`, etc.) is rejected.\n *\n * Strict by default — we'd rather reject a valid pattern (and learn\n * about it via a real-world report) than ship a \"mostly safe\" sandbox.\n * The Phase 2 plan calls out that we expand based on actual failures.\n *\n * Returns the list of validation errors; an empty list means safe to\n * bundle + execute. Callers should bail on any non-empty result.\n */\n\nimport { Project, SyntaxKind } from \"ts-morph\";\nimport type { PlaygroundFile } from \"./types.js\";\n\n/**\n * Consumer-safe @directive-run/* packages. These are the things a\n * realistic Directive demo might import: the runtime + UI adapters +\n * data primitives. Each has been audited for whether it can pull in a\n * sandbox-escape surface (process, fs, child_process, etc.); the ones\n * listed here can't.\n *\n * Packages deliberately EXCLUDED:\n *\n * - `@directive-run/cli` — uses process.argv + fs.write.\n * - `@directive-run/mcp` — speaks MCP over process.stdin/stdout; we\n * don't want sandboxed snippets opening a transport.\n * - `@directive-run/sandbox` — sandbox-in-sandbox; needs esbuild +\n * worker_threads. No legitimate use case.\n * - `@directive-run/vite-plugin-api-proxy` — build tooling, expects a\n * vite config that isn't present.\n *\n * The denylist below catches anyone who tries to import one of these\n * by pattern; the allowlist catches everything else.\n */\nconst ALLOWED_DIRECTIVE_PACKAGES = new Set<string>([\n \"core\",\n \"ai\",\n \"query\",\n \"el\",\n \"react\",\n \"vue\",\n \"svelte\",\n \"solid\",\n \"lit\",\n \"optimistic\",\n \"timeline\",\n \"mutator\",\n \"knowledge\",\n \"scaffold\",\n \"claude-plugin\",\n \"lint\",\n \"sources\",\n]);\n\nconst DENIED_DIRECTIVE_PACKAGES = new Set<string>([\n \"cli\",\n \"mcp\",\n \"sandbox\",\n \"vite-plugin-api-proxy\",\n]);\n\n/**\n * Extract the package name segment from a @directive-run/* specifier.\n *\n * \"@directive-run/core\" → \"core\"\n * \"@directive-run/ai/openai\" → \"ai\"\n * \"@directive-run/react/hooks\" → \"react\"\n *\n * Returns null when the specifier doesn't match the scope.\n */\nfunction extractDirectivePackage(specifier: string): string | null {\n const match = specifier.match(/^@directive-run\\/([^/]+)/);\n return match ? match[1]! : null;\n}\n\nfunction isAllowedImport(specifier: string): boolean {\n if (/^\\.{1,2}\\/.+\\.js$/.test(specifier)) {\n return true;\n }\n const dpkg = extractDirectivePackage(specifier);\n if (dpkg === null) {\n return false;\n }\n if (DENIED_DIRECTIVE_PACKAGES.has(dpkg)) {\n return false;\n }\n return ALLOWED_DIRECTIVE_PACKAGES.has(dpkg);\n}\n\n/**\n * Globals/identifiers the snippet may touch at top level. The runner\n * shape `generateRunner` emits + idiomatic Directive demos. Anything\n * else (process, require, fetch, fs, child_process, net, etc.) is a\n * sandbox escape attempt.\n */\nconst ALLOWED_GLOBALS = new Set<string>([\n // Directive runtime surface\n \"createSystem\",\n // We also allow the destructured exports the runner might use:\n \"system\",\n // Standard JS we'll let through\n \"console\",\n \"Math\",\n \"JSON\",\n \"Object\",\n \"Array\",\n \"Number\",\n \"String\",\n \"Boolean\",\n \"Symbol\",\n \"Promise\",\n \"Error\",\n \"Date\",\n \"Map\",\n \"Set\",\n \"WeakMap\",\n \"WeakSet\",\n \"Reflect\",\n \"globalThis\",\n // Top-level await + iteration sugar\n \"undefined\",\n \"null\",\n \"NaN\",\n \"Infinity\",\n]);\n\n/**\n * Identifiers that ALWAYS represent a sandbox escape — even if they'd\n * be allowed in some other context. Listed explicitly so a reader of\n * the validator can audit the threat model in one place.\n */\nconst DENIED_GLOBALS = new Set<string>([\n \"process\",\n \"require\",\n \"module\",\n \"__dirname\",\n \"__filename\",\n \"fetch\",\n \"XMLHttpRequest\",\n \"WebSocket\",\n \"eval\",\n \"Function\",\n // The MCP server runs on Node; these are Node-only globals that\n // bypass workers' resource isolation entirely.\n \"Buffer\",\n \"setImmediate\",\n \"queueMicrotask\",\n \"setTimeout\",\n \"setInterval\",\n \"clearTimeout\",\n \"clearInterval\",\n]);\n\nexport interface ValidationError {\n path: string;\n line: number;\n column: number;\n message: string;\n}\n\nfunction importRejectionMessage(specifier: string): string {\n const dpkg = extractDirectivePackage(specifier);\n if (dpkg && DENIED_DIRECTIVE_PACKAGES.has(dpkg)) {\n return `import \"${specifier}\" is denied — @directive-run/${dpkg} is a build/CLI/sandbox tool, not for use inside a sandboxed demo`;\n }\n return `import \"${specifier}\" is not allowed in the sandbox. Allowed: relative \"./X.js\" paths or any of @directive-run/{${Array.from(\n ALLOWED_DIRECTIVE_PACKAGES,\n )\n .sort()\n .join(\",\")}}.`;\n}\n\nfunction checkImports(\n fileLabel: string,\n project: Project,\n errors: ValidationError[],\n): void {\n const sourceFile = project.getSourceFileOrThrow(fileLabel);\n for (const decl of sourceFile.getImportDeclarations()) {\n const moduleSpecifier = decl.getModuleSpecifierValue();\n if (!isAllowedImport(moduleSpecifier)) {\n const { line, column } = sourceFile.getLineAndColumnAtPos(\n decl.getStart(),\n );\n errors.push({\n path: fileLabel,\n line,\n column,\n message: importRejectionMessage(moduleSpecifier),\n });\n }\n }\n}\n\nfunction checkDynamicImportsAndCalls(\n fileLabel: string,\n project: Project,\n errors: ValidationError[],\n): void {\n const sourceFile = project.getSourceFileOrThrow(fileLabel);\n // Reject `import(\"…\")` (dynamic), `require(\"…\")` (CommonJS), and\n // `new Function(\"…\")` (string-to-code) at any depth.\n sourceFile.forEachDescendant((node) => {\n if (node.getKind() === SyntaxKind.ImportKeyword) {\n const parent = node.getParent();\n if (parent && parent.getKind() === SyntaxKind.CallExpression) {\n const { line, column } = sourceFile.getLineAndColumnAtPos(\n node.getStart(),\n );\n errors.push({\n path: fileLabel,\n line,\n column,\n message: \"dynamic import() is not allowed in the sandbox\",\n });\n }\n }\n if (node.getKind() === SyntaxKind.NewExpression) {\n const text = node.getText();\n if (/^new\\s+Function\\s*\\(/.test(text)) {\n const { line, column } = sourceFile.getLineAndColumnAtPos(\n node.getStart(),\n );\n errors.push({\n path: fileLabel,\n line,\n column,\n message: \"new Function(...) is not allowed in the sandbox\",\n });\n }\n }\n });\n}\n\n/**\n * The Phase A AE security audit (docs/AE-AUDIT-SANDBOX.md) found that\n * the original \"skip identifiers in property-access position\" rule\n * (added to avoid `{module: x}` false-positives) was a TOTAL bypass:\n * `globalThis.process.exit()` worked because `process` was a property\n * name and got skipped. This pass closes that hole.\n *\n * The threat model: an allowed receiver (`globalThis`, `Object`,\n * `Reflect`, an allowed class) is reached freely, but accessing any\n * denied identifier name on it OR `.constructor` on any value OR\n * calling Function via property-access must be rejected.\n *\n * Rules:\n *\n * 1. Property access (`a.b`) where `b` is in DENIED_GLOBALS — reject.\n * Catches `globalThis.process`, `globalThis.fetch`, `obj.eval`,\n * `mod.Function`, etc.\n * 2. Property access where `b` is `constructor` — reject. Catches the\n * `({}).constructor.constructor(\"...\")()` Function-smuggle chain.\n * No legitimate Directive use.\n * 3. Element access (`a[\"b\"]`) where `b` is a STRING LITERAL matching\n * a denied name — reject. Catches `globalThis[\"process\"]`,\n * `globalThis[\"fetch\"]`, etc. String concatenation\n * (`globalThis[\"proc\" + \"ess\"]`) isn't catchable at AST time;\n * documented as a known gap.\n * 4. Element access on `globalThis` with ANY string literal — reject.\n * Stricter than (3); `globalThis[\"Object\"]` is denied even though\n * `Object` is allowlisted because there's no legitimate reason\n * to reach Object via bracket syntax.\n * 5. Call expression where callee is `Function` (free identifier or\n * property access on globalThis/Object) — reject. Catches\n * `Function(\"return process\")()` without `new`.\n * 6. `Reflect.get(globalThis, \"X\")` / `Reflect.has(globalThis, \"X\")` /\n * `Object.getOwnPropertyDescriptor(globalThis, \"X\")` — reject when\n * first arg is `globalThis` (or any allowed global) and second arg\n * is a string literal matching a denied name OR `constructor`.\n */\nfunction checkPropertyAccessEscapes(\n fileLabel: string,\n project: Project,\n errors: ValidationError[],\n): void {\n const sourceFile = project.getSourceFileOrThrow(fileLabel);\n\n const report = (node: import(\"ts-morph\").Node, message: string) => {\n const { line, column } = sourceFile.getLineAndColumnAtPos(node.getStart());\n errors.push({ path: fileLabel, line, column, message });\n };\n\n const stripStringLiteral = (text: string): string | null => {\n const trimmed = text.trim();\n if (\n (trimmed.startsWith('\"') && trimmed.endsWith('\"')) ||\n (trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\")) ||\n (trimmed.startsWith(\"`\") && trimmed.endsWith(\"`\"))\n ) {\n return trimmed.slice(1, -1);\n }\n return null;\n };\n\n sourceFile.forEachDescendant((node) => {\n const kind = node.getKind();\n\n // Rule 1 + 2: PropertyAccessExpression — receiver.name\n if (kind === SyntaxKind.PropertyAccessExpression) {\n const name = (node as { getName?: () => string }).getName?.();\n if (!name) {\n return;\n }\n if (name === \"constructor\") {\n report(\n node,\n \"`.constructor` access is denied in the sandbox (Function-constructor smuggle vector)\",\n );\n return;\n }\n if (DENIED_GLOBALS.has(name)) {\n report(\n node,\n `\\`.${name}\\` access is denied in the sandbox (FS/network/eval surface) — accessing a denied global via property syntax was the property-access bypass closed in v0.3.0`,\n );\n return;\n }\n }\n\n // Rule 3 + 4: ElementAccessExpression — receiver[\"string-literal\"]\n if (kind === SyntaxKind.ElementAccessExpression) {\n const expression = (\n node as { getExpression?: () => { getText: () => string } }\n ).getExpression?.();\n const argument = (\n node as {\n getArgumentExpression?: () => { getText: () => string } | undefined;\n }\n ).getArgumentExpression?.();\n const receiverText = expression?.getText() ?? \"\";\n const argText = argument?.getText() ?? \"\";\n const literal = stripStringLiteral(argText);\n\n // Rule 4: ANY bracket access on globalThis with a string literal.\n if (receiverText === \"globalThis\" && literal !== null) {\n report(\n node,\n \"bracket-access on `globalThis` with a string literal is denied in the sandbox (use direct identifier reference for allowlisted names)\",\n );\n return;\n }\n // Rule 3: bracket access whose literal matches a denied name OR\n // `constructor`, on ANY receiver.\n if (literal !== null) {\n if (literal === \"constructor\" || DENIED_GLOBALS.has(literal)) {\n report(\n node,\n `bracket-access \\`[\"${literal}\"]\\` is denied in the sandbox (would reach a denied name)`,\n );\n return;\n }\n }\n }\n\n // Rule 5: CallExpression with callee `Function(...)` (no `new`).\n if (kind === SyntaxKind.CallExpression) {\n const expression = (\n node as {\n getExpression?: () => {\n getKind: () => number;\n getText: () => string;\n };\n }\n ).getExpression?.();\n const exprText = expression?.getText() ?? \"\";\n if (exprText === \"Function\") {\n report(node, \"`Function(...)` call is denied in the sandbox\");\n return;\n }\n // Rule 6: Reflect.get / Reflect.has / Object.getOwnPropertyDescriptor\n // with `globalThis` (or any allowed global) + denied string-literal.\n const reflectAccess = exprText.match(/^(Reflect|Object)\\.(\\w+)$/);\n if (reflectAccess) {\n const callExpr = node as {\n getArguments?: () => { getText: () => string }[];\n };\n const args = callExpr.getArguments?.() ?? [];\n if (args.length >= 2) {\n const firstArg = args[0]!.getText();\n const secondArgLiteral = stripStringLiteral(args[1]!.getText());\n const denyMethods = new Set([\n \"get\",\n \"has\",\n \"getOwnPropertyDescriptor\",\n \"getOwnPropertyDescriptors\",\n \"ownKeys\",\n \"getPrototypeOf\",\n ]);\n if (\n denyMethods.has(reflectAccess[2]!) &&\n (firstArg === \"globalThis\" || ALLOWED_GLOBALS.has(firstArg)) &&\n secondArgLiteral !== null &&\n (secondArgLiteral === \"constructor\" ||\n DENIED_GLOBALS.has(secondArgLiteral))\n ) {\n report(\n node,\n `\\`${reflectAccess[1]}.${reflectAccess[2]}(${firstArg}, \"${secondArgLiteral}\")\\` would reach a denied name`,\n );\n }\n }\n }\n }\n });\n}\n\nfunction checkGlobalIdentifiers(\n fileLabel: string,\n project: Project,\n errors: ValidationError[],\n): void {\n const sourceFile = project.getSourceFileOrThrow(fileLabel);\n // Walk every Identifier and check whether it resolves to a\n // top-level binding. We only care about the \"free\" identifiers —\n // ones the snippet didn't declare via import, var/let/const, or\n // function/class declaration. Those are globals.\n const localBindings = new Set<string>();\n for (const decl of sourceFile.getImportDeclarations()) {\n for (const n of decl.getNamedImports()) {\n localBindings.add(n.getName());\n }\n const def = decl.getDefaultImport();\n if (def) localBindings.add(def.getText());\n const ns = decl.getNamespaceImport();\n if (ns) localBindings.add(ns.getText());\n }\n for (const v of sourceFile.getVariableDeclarations()) {\n localBindings.add(v.getName());\n }\n for (const f of sourceFile.getFunctions()) {\n const name = f.getName();\n if (name) localBindings.add(name);\n }\n for (const c of sourceFile.getClasses()) {\n const name = c.getName();\n if (name) localBindings.add(name);\n }\n\n sourceFile.forEachDescendant((node) => {\n if (node.getKind() !== SyntaxKind.Identifier) {\n return;\n }\n const name = node.getText();\n if (localBindings.has(name)) {\n return;\n }\n if (ALLOWED_GLOBALS.has(name)) {\n return;\n }\n\n // Skip identifiers in non-reference positions where the name is\n // just a label, not a binding reference:\n //\n // - Property-assignment KEYS in object literals: `{ module: x }`\n // — the LHS `module` is a label, not a reference to Node's `module`.\n // - Property-access NAMES: `obj.foo` — `foo` is a property selector.\n // - Property-access in shorthand: `{ foo }` — `foo` IS a reference;\n // ts-morph's ShorthandPropertyAssignment parents that case, so we\n // only filter PropertyAssignment.name and PropertyAccessExpression.name.\n // - Type-annotation positions: `let x: foo` — `foo` is a type.\n // - Import/export specifier names.\n const parent = node.getParent();\n if (parent) {\n const parentKind = parent.getKind();\n // `{ module: x }` — name child of a PropertyAssignment.\n if (parentKind === SyntaxKind.PropertyAssignment) {\n const propertyName = (\n parent as { getNameNode?: () => unknown }\n ).getNameNode?.();\n if (propertyName === node) {\n return;\n }\n }\n // `obj.foo` — `foo` is the .name on the right of a dot.\n if (parentKind === SyntaxKind.PropertyAccessExpression) {\n const accessName = (\n parent as { getNameNode?: () => unknown }\n ).getNameNode?.();\n if (accessName === node) {\n return;\n }\n }\n // `{ foo: bar }` LHS for shorthand-style method declarations.\n if (parentKind === SyntaxKind.MethodDeclaration) {\n return;\n }\n // import { foo } / import { foo as bar } / export { foo }\n if (\n parentKind === SyntaxKind.ImportSpecifier ||\n parentKind === SyntaxKind.ExportSpecifier ||\n parentKind === SyntaxKind.NamespaceImport ||\n parentKind === SyntaxKind.ImportClause\n ) {\n return;\n }\n // Function/method parameter names.\n if (parentKind === SyntaxKind.Parameter) {\n return;\n }\n // Type references — `let x: foo` etc.\n if (\n parentKind === SyntaxKind.TypeReference ||\n parentKind === SyntaxKind.TypeQuery\n ) {\n return;\n }\n }\n\n if (DENIED_GLOBALS.has(name)) {\n const { line, column } = sourceFile.getLineAndColumnAtPos(\n node.getStart(),\n );\n errors.push({\n path: fileLabel,\n line,\n column,\n message: `identifier \"${name}\" is denied in the sandbox (FS/network/eval surface)`,\n });\n }\n // We don't reject unknown identifiers — they might be legitimate\n // members of an allowlist-imported binding (e.g. `system.events.foo`\n // — `foo` is an identifier in property-access position). The\n // import + denylist + dynamic-import check above already covers\n // the actual escape paths.\n });\n}\n\nexport function validateSandboxInput(\n files: PlaygroundFile[],\n): ValidationError[] {\n const errors: ValidationError[] = [];\n const project = new Project({\n useInMemoryFileSystem: true,\n compilerOptions: {\n target: 99, // ESNext\n module: 99, // ESNext\n allowJs: true,\n strict: false,\n },\n });\n\n for (const file of files) {\n project.createSourceFile(file.path, file.source, { overwrite: true });\n }\n\n for (const file of files) {\n checkImports(file.path, project, errors);\n checkDynamicImportsAndCalls(file.path, project, errors);\n checkPropertyAccessEscapes(file.path, project, errors);\n checkGlobalIdentifiers(file.path, project, errors);\n }\n\n return errors;\n}\n","/**\n * Public API for `@directive-run/sandbox`.\n *\n * Single entry point: `runInSandbox({files, timeoutMs})` validates the\n * payload against the AST allowlist, bundles the multi-file payload via\n * esbuild, and executes the result in a bounded worker_threads sandbox.\n * Returns a structured `SandboxResult` with captured logs, the final\n * facts snapshot, and any errors that occurred at any stage.\n *\n * Consumers:\n *\n * - `@directive-run/mcp` — the `run_in_sandbox` MCP tool returns the\n * result to AI clients alongside a `playground_link` URL.\n * - `directive-docs` — the `/api/run-sandbox` Next.js route wraps this\n * for the playground page's live DevTools transcript view.\n */\n\nimport { BundleError, bundleSandboxFiles } from \"./bundler.js\";\nimport { WorkerExecError, execInWorker } from \"./host.js\";\nimport { extractDerivationKeys } from \"./key-extractor.js\";\nimport { SandboxError } from \"./types.js\";\nimport type {\n PlaygroundFile,\n RunInSandboxInput,\n SandboxResult,\n} from \"./types.js\";\nimport { validateSandboxInput } from \"./validator.js\";\n\nexport {\n SandboxError,\n type PlaygroundFile,\n type RunInSandboxInput,\n type SandboxResult,\n} from \"./types.js\";\nexport type { ValidationError } from \"./validator.js\";\n\nconst MAX_PAYLOAD_BYTES = 200_000;\nconst MAX_FILES = 10;\nconst MAIN_PATH = \"src/main.ts\";\n\nfunction normalizeInput(input: RunInSandboxInput): PlaygroundFile[] {\n if (input.files && input.source) {\n throw new SandboxError(\n \"pass either `source` or `files`, not both\",\n \"input-invalid\",\n );\n }\n if (!input.files && !input.source) {\n throw new SandboxError(\n \"must pass either `source` or `files`\",\n \"input-invalid\",\n );\n }\n const files: PlaygroundFile[] = input.files\n ? input.files\n : [{ path: MAIN_PATH, source: input.source ?? \"\" }];\n\n if (files.length === 0) {\n throw new SandboxError(\"files array is empty\", \"input-invalid\");\n }\n if (files.length > MAX_FILES) {\n throw new SandboxError(\n `payload exceeds ${MAX_FILES} files`,\n \"input-invalid\",\n );\n }\n let totalBytes = 0;\n for (const file of files) {\n if (typeof file.path !== \"string\" || file.path.length === 0) {\n throw new SandboxError(\n \"every file must have a non-empty path\",\n \"input-invalid\",\n );\n }\n if (typeof file.source !== \"string\" || file.source.length === 0) {\n throw new SandboxError(\n `file \"${file.path}\" has empty source`,\n \"input-invalid\",\n );\n }\n totalBytes += Buffer.byteLength(file.source, \"utf8\");\n }\n if (totalBytes > MAX_PAYLOAD_BYTES) {\n throw new SandboxError(\n `total payload is ${totalBytes} bytes (max ${MAX_PAYLOAD_BYTES})`,\n \"input-invalid\",\n );\n }\n // Ensure src/main.ts is present — that's the runner / entry point.\n if (!files.some((f) => f.path === MAIN_PATH)) {\n throw new SandboxError(\n `payload must include \"${MAIN_PATH}\" — the runner entry point`,\n \"input-invalid\",\n );\n }\n return files;\n}\n\nexport async function runInSandbox(\n input: RunInSandboxInput,\n): Promise<SandboxResult> {\n let files: PlaygroundFile[];\n try {\n files = normalizeInput(input);\n } catch (err) {\n if (err instanceof SandboxError) {\n return {\n logs: [],\n facts: {},\n derived: {},\n errors: [err.message],\n durationMs: 0,\n timedOut: false,\n };\n }\n throw err;\n }\n\n const validationErrors = validateSandboxInput(files);\n if (validationErrors.length > 0) {\n return {\n logs: [],\n facts: {},\n derived: {},\n errors: validationErrors.map(\n (e) => `${e.path}:${e.line}:${e.column} — ${e.message}`,\n ),\n durationMs: 0,\n timedOut: false,\n };\n }\n\n // Phase A audit P0-DM2: pre-extract derivation key names from the\n // source files so the worker can iterate them after settle. The\n // `system.derive` proxy has no `ownKeys` trap.\n const derivationKeys = extractDerivationKeys(files);\n\n let bundled: { source: string; bytes: number };\n try {\n bundled = await bundleSandboxFiles(files);\n } catch (err) {\n if (err instanceof BundleError) {\n return {\n logs: [],\n facts: {},\n derived: {},\n errors: [err.message],\n durationMs: 0,\n timedOut: false,\n };\n }\n throw err;\n }\n\n try {\n const result = await execInWorker({\n bundledSource: bundled.source,\n derivationKeys,\n timeoutMs: input.timeoutMs,\n });\n return result;\n } catch (err) {\n if (err instanceof WorkerExecError) {\n return {\n logs: [],\n facts: {},\n derived: {},\n errors: [err.message],\n durationMs: 0,\n timedOut: err.code === \"timeout\",\n };\n }\n throw err;\n }\n}\n"],"mappings":"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","names":["createRequire","pathToFileURL","build","resolveDirectivePackageToFileUrl","specifier","resolved","ENTRY_PATH","VIRTUAL_NAMESPACE","BundleError","message","cause","injectEarlyCapture","bundled","re","match","decl","capture","bundleSandboxFiles","files","f","fileMap","file","rawSource","b","args","importerDir","stripped","candidates","p","candidate","contents","epilogue","source","err","existsSync","mkdtempSync","rmSync","writeFileSync","tmpdir","dirname","join","fileURLToPath","pathToFileURL","Worker","MIN_TIMEOUT_MS","MAX_TIMEOUT_MS","DEFAULT_TIMEOUT_MS","resolveWorkerPath","staticUrl","path","createRequire","WorkerExecError","message","code","clampTimeout","value","raw","getTempBundleDir","osTmp","workerPath","writeBundleToTemp","bundledSource","baseDir","dir","bundlePath","execInWorker","input","timeoutMs","cleanupTempDir","worker","timer","timedOut","startMs","resolve","reject","settled","msg","err","findMatchingClose","source","openBrace","depth","i","ch","collectTopLevelKeys","block","keys","segStart","braceDepth","parenDepth","bracketDepth","flushSegment","end","seg","m","j","extractTopLevelKeys","sectionName","headerRe","headerMatch","closeBrace","extractDerivationKeys","files","seen","out","file","k","SandboxError","message","code","Project","SyntaxKind","ALLOWED_DIRECTIVE_PACKAGES","DENIED_DIRECTIVE_PACKAGES","extractDirectivePackage","specifier","match","isAllowedImport","dpkg","ALLOWED_GLOBALS","DENIED_GLOBALS","importRejectionMessage","checkImports","fileLabel","project","errors","sourceFile","decl","moduleSpecifier","line","column","checkDynamicImportsAndCalls","node","parent","text","checkPropertyAccessEscapes","report","message","stripStringLiteral","trimmed","kind","name","expression","argument","receiverText","argText","literal","exprText","reflectAccess","args","firstArg","secondArgLiteral","checkGlobalIdentifiers","localBindings","n","def","ns","v","f","c","parentKind","validateSandboxInput","files","file","MAX_PAYLOAD_BYTES","MAX_FILES","MAIN_PATH","normalizeInput","input","SandboxError","files","totalBytes","file","f","runInSandbox","err","validationErrors","validateSandboxInput","e","derivationKeys","extractDerivationKeys","bundled","bundleSandboxFiles","BundleError","execInWorker","WorkerExecError"]}
|
|
1
|
+
{"version":3,"sources":["../src/bundler.ts","../src/host.ts","../src/key-extractor.ts","../src/types.ts","../src/validator.ts","../src/sanitize-stack.ts","../src/index.ts"],"sourcesContent":["/**\n * esbuild adapter for the sandbox. Takes the user's multi-file payload\n * and produces a single ESM string the worker can evaluate.\n *\n * `@directive-run/*` packages are marked **external** — the worker\n * imports them at runtime via Node's normal ESM resolver, which finds\n * them in `node_modules`. We don't inline them because (a) bundle size\n * would explode, (b) they're already cached in the parent process so\n * the runtime resolver is fast.\n *\n * The bundler uses the in-memory plugin pattern: every file in the\n * payload is registered as a virtual entry, and relative imports\n * resolve against the in-memory map. esbuild handles TS → JS, top-\n * level await, and ESM linking in one pass.\n */\n\nimport { createRequire } from \"node:module\";\nimport { pathToFileURL } from \"node:url\";\nimport { build } from \"esbuild\";\nimport type { PlaygroundFile } from \"./types.js\";\n\n/**\n * Resolve a `@directive-run/*` bare specifier to an absolute file://\n * URL using the host process's node_modules. Required because the\n * worker imports the bundle from `/tmp` where Node's ESM resolver\n * can't walk up to find `@directive-run/*`. By rewriting\n * to absolute file URLs at bundle time, the worker doesn't need a\n * node_modules anchor.\n *\n * Returns null when the package can't be resolved (e.g. consumer has\n * a different install layout); the caller falls back to leaving the\n * bare specifier in place, which works when the worker IS next to\n * node_modules.\n */\nfunction resolveDirectivePackageToFileUrl(specifier: string): string | null {\n try {\n const require = createRequire(import.meta.url);\n const resolved = require.resolve(specifier);\n return pathToFileURL(resolved).href;\n } catch {\n return null;\n }\n}\n\nconst ENTRY_PATH = \"src/main.ts\";\nconst VIRTUAL_NAMESPACE = \"directive-sandbox-vfs\";\n\nexport interface BundleResult {\n /** Single ESM string ready to evaluate inside the worker. */\n source: string;\n /** Total bytes of the bundled JS (informational). */\n bytes: number;\n}\n\nexport class BundleError extends Error {\n constructor(\n message: string,\n public readonly cause?: unknown,\n ) {\n super(message);\n this.name = \"BundleError\";\n }\n}\n\n/**\n * Find the `var system = createSystem(...)` declaration esbuild\n * emits for the runner and inject a side-channel global assignment\n * immediately after it. Captures the system BEFORE any subsequent\n * `system.start()`, dispatches, or `await system.settle()` can\n * throw — so the worker can post-mortem facts even on a runtime\n * error inside the user's runner.\n *\n * esbuild's output uses `var` for hoisted top-level bindings, and\n * the entry's `const system = createSystem(...)` becomes one of:\n *\n * var system = createSystem({ ... });\n * var system2 = createSystem({ ... }); // when \"system\" collides\n *\n * The regex tolerates both. If no match, returns the source\n * unchanged (the entry-file epilogue still handles the clean-run\n * case via the appended assignment).\n */\nfunction injectEarlyCapture(bundled: string): string {\n const re = /(var\\s+(system\\d*)\\s*=\\s*createSystem\\s*\\([\\s\\S]*?\\)\\s*;)/;\n const match = bundled.match(re);\n if (!match) {\n return bundled;\n }\n const decl = match[1]!;\n const localName = match[2]!;\n const capture = `\\n(globalThis).__directiveSandbox_system__ = ${localName};\\n`;\n return bundled.replace(decl, decl + capture);\n}\n\nexport async function bundleSandboxFiles(\n files: PlaygroundFile[],\n): Promise<BundleResult> {\n const entry = files.find((f) => f.path === ENTRY_PATH);\n if (!entry) {\n throw new BundleError(\n `payload must include \"${ENTRY_PATH}\" — that's the entry point the runner targets`,\n );\n }\n\n const fileMap = new Map<string, string>();\n for (const file of files) {\n fileMap.set(file.path, file.source);\n }\n\n try {\n const result = await build({\n entryPoints: [`${VIRTUAL_NAMESPACE}:${ENTRY_PATH}`],\n bundle: true,\n format: \"esm\",\n target: \"node20\",\n platform: \"node\",\n write: false,\n logLevel: \"silent\",\n // The plugin's onResolve below intercepts every `@directive-run/*`\n // import and rewrites it to an absolute file:// URL (so the\n // worker can import the bundle from /tmp without needing\n // node_modules above it). The wildcard external is the safety\n // net for any specifier the plugin doesn't recognize.\n external: [\"@directive-run/*\"],\n // Top-level await is required (the runner does `await system.settle()`)\n // and node20 supports it natively.\n supported: { \"top-level-await\": true },\n plugins: [\n {\n name: \"directive-sandbox-vfs\",\n setup(b) {\n b.onResolve({ filter: /.*/ }, (args) => {\n if (args.kind === \"entry-point\") {\n const path = args.path.replace(`${VIRTUAL_NAMESPACE}:`, \"\");\n return { path, namespace: VIRTUAL_NAMESPACE };\n }\n if (args.namespace !== VIRTUAL_NAMESPACE) {\n return null;\n }\n if (args.path.startsWith(\"@directive-run/\")) {\n // Rewrite to absolute file:// URL so the worker's\n // `/tmp/.../bundle.mjs` can import the package without\n // needing node_modules above /tmp. Falls back to the\n // bare specifier when resolution fails — the legacy\n // \"write next to node_modules\" pattern still works\n // as a backup.\n const resolved = resolveDirectivePackageToFileUrl(args.path);\n if (resolved) {\n return { external: true, path: resolved };\n }\n return { external: true, path: args.path };\n }\n if (args.path.startsWith(\"./\") || args.path.startsWith(\"../\")) {\n // Resolve relative to the importer's path.\n const importerDir = args.importer.replace(/\\/[^/]+$/, \"\");\n // The runner imports `./counter.js`; the actual virtual\n // file lives at `src/counter.ts`. Strip the `.js` and\n // try both extensions so users can write either.\n const stripped = args.path\n .replace(/^\\.\\//, \"\")\n .replace(/\\.js$/, \"\");\n const candidates = [\n `${importerDir}/${stripped}.ts`,\n `${importerDir}/${stripped}.js`,\n `${importerDir}/${stripped}`,\n // Also try the raw path in case the importer's dir\n // resolution misses the leading slash.\n `${stripped}.ts`,\n `${stripped}.js`,\n ].map((p) => p.replace(/\\/+/g, \"/\"));\n\n for (const candidate of candidates) {\n if (fileMap.has(candidate)) {\n return { path: candidate, namespace: VIRTUAL_NAMESPACE };\n }\n }\n throw new BundleError(\n `cannot resolve \"${args.path}\" from \"${args.importer}\" — tried ${candidates.join(\", \")}`,\n );\n }\n throw new BundleError(\n `unexpected import \"${args.path}\" from \"${args.importer}\" — only relative or @directive-run/* allowed`,\n );\n });\n b.onLoad({ filter: /.*/, namespace: VIRTUAL_NAMESPACE }, (args) => {\n const contents = fileMap.get(args.path);\n if (contents === undefined) {\n throw new BundleError(`virtual file missing: ${args.path}`);\n }\n // For the entry file, append a trailing assignment so the\n // runner's top-level `const system = …` binding ends up on\n // a side-channel global the worker reads after execution.\n // ESM module exports are sealed, so we can't patch\n // `createSystem` post-import; instead we lift the binding\n // out of the entry module's scope here. The convention is\n // documented by `@directive-run/scaffold`'s `generateRunner`\n // which always names the local binding `system`.\n if (args.path === ENTRY_PATH) {\n const epilogue = [\n \"\",\n \"// directive-sandbox: lift the runner's `system` binding\",\n \"// onto a side-channel global so the worker can read\",\n \"// system.facts.$store.toObject() after execution.\",\n \"if (typeof system !== 'undefined') {\",\n \" (globalThis).__directiveSandbox_system__ = system;\",\n \"}\",\n ].join(\"\\n\");\n return { contents: contents + epilogue, loader: \"ts\" };\n }\n return { contents, loader: \"ts\" };\n });\n },\n },\n ],\n });\n const rawSource = result.outputFiles[0]?.text;\n if (!rawSource) {\n throw new BundleError(\"esbuild produced no output\");\n }\n // Inject an early-capture immediately after `createSystem(...)` so\n // the worker can read facts even when subsequent runner code throws\n // (e.g. an async settle() rejection from a bad event payload). The\n // entry-file epilogue (appended in onLoad) only fires on a clean\n // run; this regex fires regardless. Idempotent — duplicate\n // assignments to the same global are harmless.\n const source = injectEarlyCapture(rawSource);\n return { source, bytes: Buffer.byteLength(source, \"utf8\") };\n } catch (err) {\n if (err instanceof BundleError) {\n throw err;\n }\n throw new BundleError(\n `bundle failed: ${(err as Error).message ?? String(err)}`,\n err,\n );\n }\n}\n","/**\n * Host-side worker_threads orchestration. Mirrors the lint-runner\n * pattern in `@directive-run/mcp`: spawn a fresh worker per call,\n * race the response against a wall-clock timer, terminate on overrun.\n *\n * Workers are NOT pooled. Each call gets a clean process state — no\n * carry-over globals between snippets, no shared `console` patches,\n * no leaked timers from a prior run. Cold-start is ~5ms which is\n * cheap relative to the 50-200ms a typical Directive demo actually\n * spends in `system.settle()`.\n */\n\nimport { existsSync, mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { Worker } from \"node:worker_threads\";\nimport type {\n SandboxResult,\n WorkerInputMessage,\n WorkerOutputMessage,\n} from \"./types.js\";\n\nconst MIN_TIMEOUT_MS = 100;\nconst MAX_TIMEOUT_MS = 10_000;\nconst DEFAULT_TIMEOUT_MS = 5_000;\n\n/**\n * Concurrency cap on simultaneously-running workers within one host\n * process. Each worker reserves ~32 MB of heap + a thread; a malicious\n * burst (multi-tab playground, MCP client storm) can spawn enough\n * workers to OOM the surface. Cap to one-per-CPU by default; consumers\n * can override via `setMaxConcurrentWorkers()`. Excess calls queue\n * FIFO and run as slots free.\n */\nconst DEFAULT_MAX_WORKERS = Math.max(\n 1,\n (globalThis as { navigator?: { hardwareConcurrency?: number } }).navigator\n ?.hardwareConcurrency ?? 4,\n);\n\nlet maxConcurrentWorkers = DEFAULT_MAX_WORKERS;\nlet activeWorkers = 0;\n\n/**\n * A waiter holds the promise resolver for one caller blocked in\n * `acquireSlot()`. `aborted` lets a caller deregister BEFORE its\n * resolver fires — without this, an abandoned caller (HTTP client\n * disconnect, parent `Promise.race` rejection, AbortSignal trigger)\n * leaves a phantom resolver that bumps `activeWorkers` for a caller\n * that no longer exists, pinning a slot permanently. After enough\n * cancellations the pool deadlocks.\n */\ninterface Waiter {\n resolve: () => void;\n reject: (reason: unknown) => void;\n aborted: boolean;\n}\nconst waiters: Waiter[] = [];\n\n/**\n * Wake the first non-aborted waiter, skipping any that aborted while\n * still queued. The `activeWorkers` increment happens HERE (not in\n * the waiter's resolve path) so a higher new cap or a freed slot\n * cannot accidentally allow more than `maxConcurrentWorkers` to run.\n */\nfunction wakeNextWaiter(): void {\n while (waiters.length > 0 && activeWorkers < maxConcurrentWorkers) {\n const next = waiters.shift();\n if (!next || next.aborted) continue;\n activeWorkers++;\n next.resolve();\n return;\n }\n}\n\n/**\n * Override the per-process worker cap. Pass `Infinity` to disable.\n * Returns the previous value.\n *\n * Lowering the cap below `activeWorkers` does NOT terminate running\n * workers — the new ceiling applies as those workers drain. New\n * `acquireSlot()` callers queue until `activeWorkers` falls below\n * the new cap. Raising the cap immediately drains any waiters the\n * new ceiling can absorb.\n *\n * @example Cap the worker pool at boot for a Next.js API route\n * ```ts\n * // app/api/sandbox/route.ts — runs once per cold start\n * import { setMaxConcurrentWorkers } from \"@directive-run/sandbox\";\n * setMaxConcurrentWorkers(8);\n * ```\n */\nexport function setMaxConcurrentWorkers(value: number): number {\n const prev = maxConcurrentWorkers;\n if (!Number.isFinite(value) && value !== Number.POSITIVE_INFINITY) {\n return prev;\n }\n maxConcurrentWorkers = Math.max(1, Math.floor(value));\n // Drain waiters the new (higher) cap can absorb. wakeNextWaiter()\n // checks the cap before each increment so a LOWER cap does nothing\n // here — drains happen organically as releaseSlot() fires.\n while (waiters.length > 0 && activeWorkers < maxConcurrentWorkers) {\n wakeNextWaiter();\n }\n return prev;\n}\n\nfunction acquireSlot(signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(\n signal.reason instanceof Error\n ? signal.reason\n : new Error(\"sandbox: acquireSlot aborted\"),\n );\n }\n if (activeWorkers < maxConcurrentWorkers) {\n activeWorkers++;\n return Promise.resolve();\n }\n return new Promise<void>((resolve, reject) => {\n const waiter: Waiter = { resolve, reject, aborted: false };\n waiters.push(waiter);\n if (signal) {\n const onAbort = () => {\n waiter.aborted = true;\n reject(\n signal.reason instanceof Error\n ? signal.reason\n : new Error(\"sandbox: acquireSlot aborted\"),\n );\n // Don't splice — `wakeNextWaiter()` will skip aborted waiters\n // naturally. Splicing here would race with concurrent shifts.\n };\n signal.addEventListener(\"abort\", onAbort, { once: true });\n // Decorate resolve/reject to clean up the listener once the\n // waiter is finally settled (acquired OR aborted).\n const originalResolve = waiter.resolve;\n const originalReject = waiter.reject;\n waiter.resolve = () => {\n signal.removeEventListener(\"abort\", onAbort);\n originalResolve();\n };\n waiter.reject = (reason) => {\n signal.removeEventListener(\"abort\", onAbort);\n originalReject(reason);\n };\n }\n });\n}\n\nfunction releaseSlot(): void {\n activeWorkers = Math.max(0, activeWorkers - 1);\n // Only hand the slot off when the cap allows — protects against\n // `setMaxConcurrentWorkers(lower)` followed by releases that would\n // otherwise immediately re-saturate above the new ceiling.\n if (activeWorkers < maxConcurrentWorkers) {\n wakeNextWaiter();\n }\n}\n\nasync function resolveWorkerPath(): Promise<string> {\n // PRIMARY: static URL relative to this module's own file. Bundlers\n // (Next.js outputFileTracing, esbuild, webpack, etc.) follow this\n // static reference at build time and include `worker.js` in the\n // output bundle automatically. Works on Vercel + AWS Lambda + Cloud\n // Run without any consumer-side config.\n //\n // FALLBACK (Vitest dev path): `import.meta.url` resolves to the\n // .ts source file in tests, so `./worker.js` doesn't exist there —\n // fall through to `createRequire(import.meta.url).resolve(...)`\n // which Vitest handles correctly via its SSR loader.\n try {\n const staticUrl = new URL(\"./worker.js\", import.meta.url);\n const path = fileURLToPath(staticUrl);\n if (existsSync(path)) {\n return path;\n }\n } catch {\n // fall through\n }\n const { createRequire } = await import(\"node:module\");\n const require = createRequire(import.meta.url);\n return require.resolve(\"@directive-run/sandbox/worker\");\n}\n\nexport interface HostRunInput {\n bundledSource: string;\n /** Derivation key names extracted from the payload's source files. */\n derivationKeys: string[];\n timeoutMs?: number;\n /**\n * Optional AbortSignal. When the signal aborts BEFORE a worker slot\n * is acquired, the queued waiter is removed cleanly — no phantom\n * slot increment. When the signal aborts AFTER worker spawn, the\n * worker is terminated and the slot released. Callers wiring this\n * to an HTTP request signal close the connection-cancel → leaked-\n * slot DoS vector exposed in the AE review of v0.3.x.\n */\n signal?: AbortSignal;\n}\n\nexport class WorkerExecError extends Error {\n constructor(\n message: string,\n public readonly code: \"worker-error\" | \"timeout\",\n ) {\n super(message);\n this.name = \"WorkerExecError\";\n }\n}\n\nfunction clampTimeout(value: number | undefined): number {\n const raw = value ?? DEFAULT_TIMEOUT_MS;\n if (!Number.isFinite(raw)) {\n return DEFAULT_TIMEOUT_MS;\n }\n return Math.min(MAX_TIMEOUT_MS, Math.max(MIN_TIMEOUT_MS, Math.floor(raw)));\n}\n\n/**\n * Try `os.tmpdir()` first (Vercel-friendly: /tmp is the only writable\n * location on serverless functions). Fall back to the sandbox package\n * dir if /tmp ISN'T writable for some reason. The fallback inherits\n * the package's node_modules walking chain so bare specifiers like\n * `@directive-run/core` resolve naturally.\n */\nasync function getTempBundleDir(): Promise<string> {\n const osTmp = tmpdir();\n if (existsSync(osTmp)) {\n return osTmp;\n }\n // Reuse the worker-resolution path so we land at the same package\n // regardless of whether the consumer is in production or Vitest dev.\n const workerPath = await resolveWorkerPath();\n // dist/worker.js → ../ → package root.\n return dirname(dirname(workerPath));\n}\n\n/**\n * Write the bundled snippet to a temp file Node's ESM loader can\n * import via a file:// URL.\n *\n * Previous versions wrote inside the sandbox package's own directory\n * so Node's resolver could walk up to find `@directive-run/core` in\n * node_modules — but Vercel / AWS Lambda / Cloud Run all ship\n * read-only FS outside `/tmp`. The bundler now rewrites\n * `@directive-run/*` imports to ABSOLUTE `file://` URLs of the\n * host's resolved paths (see `bundleSandboxFiles`), so the temp\n * file can live in `/tmp` without needing a node_modules anchor.\n *\n * Caller MUST clean the directory up in a finally block.\n */\nasync function writeBundleToTemp(bundledSource: string): Promise<{\n bundlePath: string;\n cleanup: () => void;\n}> {\n const baseDir = await getTempBundleDir();\n const dir = mkdtempSync(join(baseDir, \"directive-sandbox-\"));\n const bundlePath = join(dir, \"bundle.mjs\");\n writeFileSync(bundlePath, bundledSource, \"utf8\");\n return {\n bundlePath,\n cleanup: () => {\n try {\n rmSync(dir, { recursive: true, force: true });\n } catch {\n // best-effort\n }\n },\n };\n}\n\nexport async function execInWorker(\n input: HostRunInput,\n): Promise<SandboxResult> {\n const timeoutMs = clampTimeout(input.timeoutMs);\n // Block here until a slot frees. A burst that exceeds the cap is\n // queued rather than amplified into worker-spawn pressure on the host.\n // The optional signal lets a cancelled caller deregister from the\n // wait queue without leaking a phantom slot.\n await acquireSlot(input.signal);\n let workerPath: string;\n let bundlePath: string;\n let cleanupTempDir: () => void;\n try {\n workerPath = await resolveWorkerPath();\n ({ bundlePath, cleanup: cleanupTempDir } = await writeBundleToTemp(\n input.bundledSource,\n ));\n } catch (err) {\n // Release the slot if we never made it to the worker construction.\n releaseSlot();\n throw err;\n }\n const worker = new Worker(workerPath, {\n // 32 MB heap ceiling — bounded enough to prevent runaway allocations\n // without crowding the typical demo footprint (~2-5 MB).\n resourceLimits: {\n maxOldGenerationSizeMb: 32,\n maxYoungGenerationSizeMb: 8,\n codeRangeSizeMb: 16,\n },\n // Bypass stderr noise from the worker process showing up in the\n // host's logs. The transcript captures everything we care about.\n stderr: false,\n });\n\n let timer: NodeJS.Timeout | null = null;\n let timedOut = false;\n let aborted = false;\n let abortListener: (() => void) | null = null;\n const startMs = Date.now();\n\n try {\n const result = await new Promise<SandboxResult>((resolve, reject) => {\n let settled = false;\n\n worker.once(\"message\", (msg: WorkerOutputMessage) => {\n settled = true;\n if (msg.ok) {\n resolve(msg.result);\n } else {\n reject(new WorkerExecError(msg.error, \"worker-error\"));\n }\n });\n\n worker.once(\"error\", (err: Error) => {\n settled = true;\n reject(new WorkerExecError(err.message, \"worker-error\"));\n });\n\n worker.once(\"exit\", (code) => {\n if (!settled && code !== 0 && code !== null) {\n reject(\n new WorkerExecError(\n `worker exited with code ${code} before responding`,\n \"worker-error\",\n ),\n );\n }\n });\n\n timer = setTimeout(() => {\n timedOut = true;\n worker.terminate();\n reject(\n new WorkerExecError(\n `wall-clock budget of ${timeoutMs}ms elapsed`,\n \"timeout\",\n ),\n );\n }, timeoutMs);\n\n // Post-acquisition signal wiring: when the caller's AbortSignal\n // fires (HTTP client disconnect, parent cancellation), terminate\n // the running worker so the slot frees IMMEDIATELY instead of\n // hanging until `timeoutMs` (up to 10s) elapses. Without this,\n // an abandoned caller still ties up the slot for the full\n // worker timeout — driving the per-process pool to deadlock\n // under sustained disconnect load.\n if (input.signal) {\n abortListener = () => {\n if (settled) return;\n settled = true;\n aborted = true;\n worker.terminate();\n reject(\n new WorkerExecError(\n \"sandbox: aborted by caller signal\",\n \"worker-error\",\n ),\n );\n };\n if (input.signal.aborted) {\n // Pre-acquired-then-aborted — fire synchronously.\n abortListener();\n } else {\n input.signal.addEventListener(\"abort\", abortListener, {\n once: true,\n });\n }\n }\n\n const message: WorkerInputMessage = {\n bundlePath: pathToFileURL(bundlePath).href,\n timeoutMs,\n derivationKeys: input.derivationKeys,\n };\n worker.postMessage(message);\n });\n\n return result;\n } catch (err) {\n if (err instanceof WorkerExecError && err.code === \"timeout\") {\n // The worker captured nothing because we killed it; surface\n // structured timeout info to the caller.\n return {\n logs: [],\n facts: {},\n derived: {},\n errors: [err.message],\n durationMs: Date.now() - startMs,\n timedOut: true,\n };\n }\n throw err;\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n if (abortListener && input.signal) {\n input.signal.removeEventListener(\"abort\", abortListener);\n }\n await worker.terminate().catch(() => undefined);\n cleanupTempDir();\n releaseSlot();\n void timedOut;\n void aborted;\n }\n}\n","/**\n * Pure regex-based extractor for `derive:` / `derivations:` block keys\n * across a payload of source files. Mirrors the lightweight pattern the\n * docs site's DevTools panel uses for static-structure parsing.\n *\n * `system.derive` is a Proxy with no `ownKeys` trap, so the worker\n * can't enumerate derivations from inside the sandbox. Instead, the\n * host pre-scans source files for the declared keys and forwards them\n * to the worker, which then reads `system.derive[key]` for each.\n *\n * Best-effort: a module that builds derivation keys dynamically (e.g.\n * `derive: Object.fromEntries(keys.map(k => [k, fn]))`) won't be\n * extracted. That's acceptable for the stated goal of \"transcript\n * reflects what the module declared.\"\n */\n\nimport type { PlaygroundFile } from \"./types.js\";\n\n/**\n * Find the matching `}` for the `{` at `openBrace`. Returns the index\n * of the close brace, or -1 if unbalanced.\n */\nfunction findMatchingClose(source: string, openBrace: number): number {\n let depth = 0;\n for (let i = openBrace; i < source.length; i++) {\n const ch = source[i];\n if (ch === \"{\") depth += 1;\n else if (ch === \"}\") {\n depth -= 1;\n if (depth === 0) return i;\n }\n }\n return -1;\n}\n\n/**\n * Pull top-level `key:` segments out of a brace-balanced block. Keys\n * are found between `,` or `\\n` or the block start at brace depth 0\n * AND paren depth 0. Works for both multi-line and compact `{ a: 1,\n * b: 2 }` forms.\n */\nfunction collectTopLevelKeys(block: string): string[] {\n const keys: string[] = [];\n let segStart = 0;\n let braceDepth = 0;\n let parenDepth = 0;\n let bracketDepth = 0;\n\n const flushSegment = (end: number) => {\n const seg = block.slice(segStart, end).trim();\n segStart = end + 1;\n if (!seg) return;\n // Tolerate leading quotes for `\"foo\":` quoted keys.\n const m = seg.match(/^['\"]?(\\w+)['\"]?\\s*:/);\n if (m) keys.push(m[1]!);\n };\n\n for (let j = 0; j < block.length; j++) {\n const ch = block[j];\n if (ch === \"{\") braceDepth += 1;\n else if (ch === \"}\") braceDepth -= 1;\n else if (ch === \"(\") parenDepth += 1;\n else if (ch === \")\") parenDepth -= 1;\n else if (ch === \"[\") bracketDepth += 1;\n else if (ch === \"]\") bracketDepth -= 1;\n else if (\n (ch === \",\" || ch === \"\\n\") &&\n braceDepth === 0 &&\n parenDepth === 0 &&\n bracketDepth === 0\n ) {\n flushSegment(j);\n }\n }\n // Final segment (no trailing comma/newline).\n flushSegment(block.length);\n return keys;\n}\n\nfunction extractTopLevelKeys(source: string, sectionName: string): string[] {\n const headerRe = new RegExp(`\\\\b${sectionName}\\\\s*:\\\\s*\\\\{`);\n const headerMatch = source.match(headerRe);\n if (!headerMatch || headerMatch.index === undefined) {\n return [];\n }\n const openBrace = source.indexOf(\"{\", headerMatch.index);\n if (openBrace === -1) {\n return [];\n }\n const closeBrace = findMatchingClose(source, openBrace);\n if (closeBrace === -1) {\n return [];\n }\n return collectTopLevelKeys(source.slice(openBrace + 1, closeBrace));\n}\n\n/**\n * Collect the union of derivation key names declared across all files\n * in the payload. Looks at both `derive:` (module config block) and\n * `derivations:` (schema block). De-duplicates.\n */\nexport function extractDerivationKeys(files: PlaygroundFile[]): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const file of files) {\n for (const k of extractTopLevelKeys(file.source, \"derive\")) {\n if (!seen.has(k)) {\n seen.add(k);\n out.push(k);\n }\n }\n for (const k of extractTopLevelKeys(file.source, \"derivations\")) {\n if (!seen.has(k)) {\n seen.add(k);\n out.push(k);\n }\n }\n }\n return out;\n}\n","/**\n * Public types for `@directive-run/sandbox`.\n *\n * The package executes user-supplied Directive snippets in a bounded\n * worker_threads sandbox and returns a structured transcript. The\n * shape below is what the MCP `run_in_sandbox` tool returns to the\n * AI client and what `directive.run/playground`'s `/api/run-sandbox`\n * route serializes to JSON.\n */\n\nexport interface PlaygroundFile {\n /**\n * Relative path inside the project, e.g. \"src/main.ts\" or\n * \"src/counter.ts\". One file should be \"src/main.ts\" — that's\n * the entry point the runner targets.\n */\n path: string;\n /** File contents. */\n source: string;\n}\n\nexport interface RunInSandboxInput {\n /** Single-file shortcut. Mapped onto \"src/main.ts\" internally. */\n source?: string;\n /** Multi-file payload (the `generate_module` paired output shape). */\n files?: PlaygroundFile[];\n /** Wall-clock timeout in milliseconds. Defaults to 5000. Clamped to [100, 10000]. */\n timeoutMs?: number;\n /**\n * Optional cancellation signal. Wire this to your HTTP request's\n * AbortSignal (Next.js / Express both expose one) so a client that\n * disconnects mid-flight releases its worker slot immediately\n * instead of leaking it. Without a signal, an abandoned `runInSandbox`\n * call still queues into the per-process worker cap and only frees\n * when the worker times out — under load this drives the cap to\n * permanent deadlock.\n */\n signal?: AbortSignal;\n}\n\nexport interface SandboxResult {\n /**\n * Captured `console.log` / `console.warn` / `console.error` lines,\n * in dispatch order. Each entry is the stringified arguments\n * joined by \" \" (matches Node's default console format).\n */\n logs: string[];\n /**\n * Final `system.facts.$store.toObject()` snapshot at end-of-run.\n * Empty when no system was constructed (e.g. validator rejection).\n */\n facts: Record<string, unknown>;\n /**\n * Final `system.derive` snapshot — every derivation declared in the\n * module config, evaluated by reading `system.derive[key]`. Empty\n * when the module has no `derive:` block or when validation rejected\n * before bundle. The June 2026 security audit flagged the original\n * sandbox for snapshotting only facts; modules whose primary product\n * is a derivation (`status`, `isReady`, etc.) returned an empty-\n * looking transcript.\n */\n derived: Record<string, unknown>;\n /**\n * Structured error messages from validation, bundling, or runtime\n * exceptions. Empty on a clean run.\n */\n errors: string[];\n /** Elapsed wall-clock duration of the worker execution. */\n durationMs: number;\n /** True when the wall-clock budget elapsed before settle()/destroy(). */\n timedOut: boolean;\n}\n\nexport type SandboxErrorCode =\n | \"validation-failed\"\n | \"bundle-failed\"\n | \"worker-error\"\n | \"timeout\"\n | \"input-invalid\";\n\nexport class SandboxError extends Error {\n constructor(\n message: string,\n public readonly code: SandboxErrorCode,\n ) {\n super(message);\n this.name = \"SandboxError\";\n }\n}\n\n/**\n * Wire shape for messages from host → worker. The host writes the\n * bundled snippet to a temp file (so Node's ESM loader can resolve\n * `@directive-run/core` against the parent's node_modules; bare\n * specifiers don't resolve from data: URLs) and passes the file URL.\n * Replies on the same channel with a `SandboxResult`.\n */\nexport interface WorkerInputMessage {\n /** `file://` URL of the temp .mjs bundle the host wrote. */\n bundlePath: string;\n timeoutMs: number;\n /**\n * Derivation key names the host extracted from the source files.\n * `system.derive` is a Proxy with no `ownKeys` trap, so we can't\n * enumerate from the worker; the host's pre-bundle pass scans the\n * source for `derive:` and `derivations:` blocks and forwards the\n * keys here. Worker reads `system.derive[key]` for each.\n */\n derivationKeys: string[];\n}\n\n/** Wire shape for messages from worker → host. */\nexport type WorkerOutputMessage =\n | { ok: true; result: SandboxResult }\n | { ok: false; error: string };\n","/**\n * AST allowlist validator. Pre-flights every file in the payload\n * BEFORE the bundler so a hostile snippet never reaches the runtime\n * surface. Without this layer, `worker_threads` resource limits\n * (heap-only) leak FS + network access through — a snippet that\n * `import(\"node:fs\")` would still pwn the host process.\n *\n * Allowlist:\n *\n * - Imports: must match `@directive-run/*` (specifically `core`, `ai`,\n * `query`) OR a relative path ending in `.js` (the multi-file\n * payload's own files).\n * - Identifier accesses: only the allowlisted Directive API surface\n * plus `console.*`, `Math.*`, `JSON.*`. Anything that touches\n * global Node namespaces (`process`, `require`, `fs`, `child_process`,\n * `net`, `dgram`, `cluster`, etc.) is rejected.\n *\n * Strict by default — we'd rather reject a valid pattern (and learn\n * about it via a real-world report) than ship a \"mostly safe\" sandbox.\n * The Phase 2 plan calls out that we expand based on actual failures.\n *\n * Returns the list of validation errors; an empty list means safe to\n * bundle + execute. Callers should bail on any non-empty result.\n */\n\nimport { Project, SyntaxKind } from \"ts-morph\";\nimport type { PlaygroundFile } from \"./types.js\";\n\n/**\n * Consumer-safe @directive-run/* packages. These are the things a\n * realistic Directive demo might import: the runtime + UI adapters +\n * data primitives. Each has been audited for whether it can pull in a\n * sandbox-escape surface (process, fs, child_process, etc.); the ones\n * listed here can't.\n *\n * Packages deliberately EXCLUDED:\n *\n * - `@directive-run/cli` — uses process.argv + fs.write.\n * - `@directive-run/mcp` — speaks MCP over process.stdin/stdout; we\n * don't want sandboxed snippets opening a transport.\n * - `@directive-run/sandbox` — sandbox-in-sandbox; needs esbuild +\n * worker_threads. No legitimate use case.\n * - `@directive-run/vite-plugin-api-proxy` — build tooling, expects a\n * vite config that isn't present.\n *\n * The denylist below catches anyone who tries to import one of these\n * by pattern; the allowlist catches everything else.\n */\nconst ALLOWED_DIRECTIVE_PACKAGES = new Set<string>([\n \"core\",\n \"ai\",\n \"query\",\n \"el\",\n \"react\",\n \"vue\",\n \"svelte\",\n \"solid\",\n \"lit\",\n \"optimistic\",\n \"timeline\",\n \"mutator\",\n \"knowledge\",\n \"scaffold\",\n \"claude-plugin\",\n \"lint\",\n \"sources\",\n]);\n\nconst DENIED_DIRECTIVE_PACKAGES = new Set<string>([\n \"cli\",\n \"mcp\",\n \"sandbox\",\n \"vite-plugin-api-proxy\",\n]);\n\n/**\n * Extract the package name segment from a @directive-run/* specifier.\n *\n * \"@directive-run/core\" → \"core\"\n * \"@directive-run/ai/openai\" → \"ai\"\n * \"@directive-run/react/hooks\" → \"react\"\n *\n * Returns null when the specifier doesn't match the scope.\n */\nfunction extractDirectivePackage(specifier: string): string | null {\n const match = specifier.match(/^@directive-run\\/([^/]+)/);\n return match ? match[1]! : null;\n}\n\nfunction isAllowedImport(specifier: string): boolean {\n if (/^\\.{1,2}\\/.+\\.js$/.test(specifier)) {\n return true;\n }\n const dpkg = extractDirectivePackage(specifier);\n if (dpkg === null) {\n return false;\n }\n if (DENIED_DIRECTIVE_PACKAGES.has(dpkg)) {\n return false;\n }\n return ALLOWED_DIRECTIVE_PACKAGES.has(dpkg);\n}\n\n/**\n * Globals/identifiers the snippet may touch at top level. The runner\n * shape `generateRunner` emits + idiomatic Directive demos. Anything\n * else (process, require, fetch, fs, child_process, net, etc.) is a\n * sandbox escape attempt.\n */\nconst ALLOWED_GLOBALS = new Set<string>([\n // Directive runtime surface\n \"createSystem\",\n // We also allow the destructured exports the runner might use:\n \"system\",\n // Standard JS we'll let through\n \"console\",\n \"Math\",\n \"JSON\",\n \"Object\",\n \"Array\",\n \"Number\",\n \"String\",\n \"Boolean\",\n \"Symbol\",\n \"Promise\",\n \"Error\",\n \"Date\",\n \"Map\",\n \"Set\",\n \"WeakMap\",\n \"WeakSet\",\n \"Reflect\",\n \"globalThis\",\n // Top-level await + iteration sugar\n \"undefined\",\n \"null\",\n \"NaN\",\n \"Infinity\",\n]);\n\n/**\n * Identifiers that ALWAYS represent a sandbox escape — even if they'd\n * be allowed in some other context. Listed explicitly so a reader of\n * the validator can audit the threat model in one place.\n */\nconst DENIED_GLOBALS = new Set<string>([\n \"process\",\n \"require\",\n \"module\",\n \"__dirname\",\n \"__filename\",\n \"fetch\",\n \"XMLHttpRequest\",\n \"WebSocket\",\n \"eval\",\n \"Function\",\n // The MCP server runs on Node; these are Node-only globals that\n // bypass workers' resource isolation entirely.\n \"Buffer\",\n \"setImmediate\",\n \"queueMicrotask\",\n \"setTimeout\",\n \"setInterval\",\n \"clearTimeout\",\n \"clearInterval\",\n]);\n\nexport interface ValidationError {\n path: string;\n line: number;\n column: number;\n message: string;\n}\n\nfunction importRejectionMessage(specifier: string): string {\n const dpkg = extractDirectivePackage(specifier);\n if (dpkg && DENIED_DIRECTIVE_PACKAGES.has(dpkg)) {\n return `import \"${specifier}\" is denied — @directive-run/${dpkg} is a build/CLI/sandbox tool, not for use inside a sandboxed demo`;\n }\n return `import \"${specifier}\" is not allowed in the sandbox. Allowed: relative \"./X.js\" paths or any of @directive-run/{${Array.from(\n ALLOWED_DIRECTIVE_PACKAGES,\n )\n .sort()\n .join(\",\")}}.`;\n}\n\nfunction checkImports(\n fileLabel: string,\n project: Project,\n errors: ValidationError[],\n): void {\n const sourceFile = project.getSourceFileOrThrow(fileLabel);\n for (const decl of sourceFile.getImportDeclarations()) {\n const moduleSpecifier = decl.getModuleSpecifierValue();\n if (!isAllowedImport(moduleSpecifier)) {\n const { line, column } = sourceFile.getLineAndColumnAtPos(\n decl.getStart(),\n );\n errors.push({\n path: fileLabel,\n line,\n column,\n message: importRejectionMessage(moduleSpecifier),\n });\n }\n }\n}\n\nfunction checkDynamicImportsAndCalls(\n fileLabel: string,\n project: Project,\n errors: ValidationError[],\n): void {\n const sourceFile = project.getSourceFileOrThrow(fileLabel);\n // Reject `import(\"…\")` (dynamic), `require(\"…\")` (CommonJS), and\n // `new Function(\"…\")` (string-to-code) at any depth.\n sourceFile.forEachDescendant((node) => {\n if (node.getKind() === SyntaxKind.ImportKeyword) {\n const parent = node.getParent();\n if (parent && parent.getKind() === SyntaxKind.CallExpression) {\n const { line, column } = sourceFile.getLineAndColumnAtPos(\n node.getStart(),\n );\n errors.push({\n path: fileLabel,\n line,\n column,\n message: \"dynamic import() is not allowed in the sandbox\",\n });\n }\n }\n if (node.getKind() === SyntaxKind.NewExpression) {\n const text = node.getText();\n if (/^new\\s+Function\\s*\\(/.test(text)) {\n const { line, column } = sourceFile.getLineAndColumnAtPos(\n node.getStart(),\n );\n errors.push({\n path: fileLabel,\n line,\n column,\n message: \"new Function(...) is not allowed in the sandbox\",\n });\n }\n }\n });\n}\n\n/**\n * The June 2026 security audit (docs/security/sandbox-audit-2026-06.md)\n * found that the original \"skip identifiers in property-access position\"\n * rule (added to avoid `{module: x}` false-positives) was a TOTAL bypass:\n * `globalThis.process.exit()` worked because `process` was a property\n * name and got skipped. This pass closes that hole.\n *\n * The threat model: an allowed receiver (`globalThis`, `Object`,\n * `Reflect`, an allowed class) is reached freely, but accessing any\n * denied identifier name on it OR `.constructor` on any value OR\n * calling Function via property-access must be rejected.\n *\n * Rules:\n *\n * 1. Property access (`a.b`) where `b` is in DENIED_GLOBALS — reject.\n * Catches `globalThis.process`, `globalThis.fetch`, `obj.eval`,\n * `mod.Function`, etc.\n * 2. Property access where `b` is `constructor` — reject. Catches the\n * `({}).constructor.constructor(\"...\")()` Function-smuggle chain.\n * No legitimate Directive use.\n * 3. Element access (`a[\"b\"]`) where `b` is a STRING LITERAL matching\n * a denied name — reject. Catches `globalThis[\"process\"]`,\n * `globalThis[\"fetch\"]`, etc. String concatenation\n * (`globalThis[\"proc\" + \"ess\"]`) isn't catchable at AST time;\n * documented as a known gap.\n * 4. Element access on `globalThis` with ANY string literal — reject.\n * Stricter than (3); `globalThis[\"Object\"]` is denied even though\n * `Object` is allowlisted because there's no legitimate reason\n * to reach Object via bracket syntax.\n * 5. Call expression where callee is `Function` (free identifier or\n * property access on globalThis/Object) — reject. Catches\n * `Function(\"return process\")()` without `new`.\n * 6. `Reflect.get(globalThis, \"X\")` / `Reflect.has(globalThis, \"X\")` /\n * `Object.getOwnPropertyDescriptor(globalThis, \"X\")` — reject when\n * first arg is `globalThis` (or any allowed global) and second arg\n * is a string literal matching a denied name OR `constructor`.\n */\nfunction checkPropertyAccessEscapes(\n fileLabel: string,\n project: Project,\n errors: ValidationError[],\n): void {\n const sourceFile = project.getSourceFileOrThrow(fileLabel);\n\n const report = (node: import(\"ts-morph\").Node, message: string) => {\n const { line, column } = sourceFile.getLineAndColumnAtPos(node.getStart());\n errors.push({ path: fileLabel, line, column, message });\n };\n\n const stripStringLiteral = (text: string): string | null => {\n const trimmed = text.trim();\n if (\n (trimmed.startsWith('\"') && trimmed.endsWith('\"')) ||\n (trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\")) ||\n (trimmed.startsWith(\"`\") && trimmed.endsWith(\"`\"))\n ) {\n return trimmed.slice(1, -1);\n }\n return null;\n };\n\n sourceFile.forEachDescendant((node) => {\n const kind = node.getKind();\n\n // Rule 1 + 2: PropertyAccessExpression — receiver.name\n if (kind === SyntaxKind.PropertyAccessExpression) {\n const name = (node as { getName?: () => string }).getName?.();\n if (!name) {\n return;\n }\n if (name === \"constructor\") {\n report(\n node,\n \"`.constructor` access is denied in the sandbox (Function-constructor smuggle vector)\",\n );\n return;\n }\n if (DENIED_GLOBALS.has(name)) {\n report(\n node,\n `\\`.${name}\\` access is denied in the sandbox (FS/network/eval surface) — accessing a denied global via property syntax was the property-access bypass closed in v0.3.0`,\n );\n return;\n }\n }\n\n // Rule 3 + 4: ElementAccessExpression — receiver[\"string-literal\"]\n if (kind === SyntaxKind.ElementAccessExpression) {\n const expression = (\n node as { getExpression?: () => { getText: () => string } }\n ).getExpression?.();\n const argument = (\n node as {\n getArgumentExpression?: () => { getText: () => string } | undefined;\n }\n ).getArgumentExpression?.();\n const receiverText = expression?.getText() ?? \"\";\n const argText = argument?.getText() ?? \"\";\n const literal = stripStringLiteral(argText);\n\n // Rule 4: ANY bracket access on globalThis with a string literal.\n if (receiverText === \"globalThis\" && literal !== null) {\n report(\n node,\n \"bracket-access on `globalThis` with a string literal is denied in the sandbox (use direct identifier reference for allowlisted names)\",\n );\n return;\n }\n // Rule 3: bracket access whose literal matches a denied name OR\n // `constructor`, on ANY receiver.\n if (literal !== null) {\n if (literal === \"constructor\" || DENIED_GLOBALS.has(literal)) {\n report(\n node,\n `bracket-access \\`[\"${literal}\"]\\` is denied in the sandbox (would reach a denied name)`,\n );\n return;\n }\n }\n }\n\n // Rule 5: CallExpression with callee `Function(...)` (no `new`).\n if (kind === SyntaxKind.CallExpression) {\n const expression = (\n node as {\n getExpression?: () => {\n getKind: () => number;\n getText: () => string;\n };\n }\n ).getExpression?.();\n const exprText = expression?.getText() ?? \"\";\n if (exprText === \"Function\") {\n report(node, \"`Function(...)` call is denied in the sandbox\");\n return;\n }\n // Rule 6: Reflect.get / Reflect.has / Object.getOwnPropertyDescriptor\n // with `globalThis` (or any allowed global) + denied string-literal.\n const reflectAccess = exprText.match(/^(Reflect|Object)\\.(\\w+)$/);\n if (reflectAccess) {\n const callExpr = node as {\n getArguments?: () => { getText: () => string }[];\n };\n const args = callExpr.getArguments?.() ?? [];\n if (args.length >= 2) {\n const firstArg = args[0]!.getText();\n const secondArgLiteral = stripStringLiteral(args[1]!.getText());\n const denyMethods = new Set([\n \"get\",\n \"has\",\n \"getOwnPropertyDescriptor\",\n \"getOwnPropertyDescriptors\",\n \"ownKeys\",\n \"getPrototypeOf\",\n ]);\n if (\n denyMethods.has(reflectAccess[2]!) &&\n (firstArg === \"globalThis\" || ALLOWED_GLOBALS.has(firstArg)) &&\n secondArgLiteral !== null &&\n (secondArgLiteral === \"constructor\" ||\n DENIED_GLOBALS.has(secondArgLiteral))\n ) {\n report(\n node,\n `\\`${reflectAccess[1]}.${reflectAccess[2]}(${firstArg}, \"${secondArgLiteral}\")\\` would reach a denied name`,\n );\n }\n }\n }\n }\n });\n}\n\nfunction checkGlobalIdentifiers(\n fileLabel: string,\n project: Project,\n errors: ValidationError[],\n): void {\n const sourceFile = project.getSourceFileOrThrow(fileLabel);\n // Walk every Identifier and check whether it resolves to a\n // top-level binding. We only care about the \"free\" identifiers —\n // ones the snippet didn't declare via import, var/let/const, or\n // function/class declaration. Those are globals.\n const localBindings = new Set<string>();\n for (const decl of sourceFile.getImportDeclarations()) {\n for (const n of decl.getNamedImports()) {\n localBindings.add(n.getName());\n }\n const def = decl.getDefaultImport();\n if (def) localBindings.add(def.getText());\n const ns = decl.getNamespaceImport();\n if (ns) localBindings.add(ns.getText());\n }\n for (const v of sourceFile.getVariableDeclarations()) {\n localBindings.add(v.getName());\n }\n for (const f of sourceFile.getFunctions()) {\n const name = f.getName();\n if (name) localBindings.add(name);\n }\n for (const c of sourceFile.getClasses()) {\n const name = c.getName();\n if (name) localBindings.add(name);\n }\n\n sourceFile.forEachDescendant((node) => {\n if (node.getKind() !== SyntaxKind.Identifier) {\n return;\n }\n const name = node.getText();\n if (localBindings.has(name)) {\n return;\n }\n if (ALLOWED_GLOBALS.has(name)) {\n return;\n }\n\n // Skip identifiers in non-reference positions where the name is\n // just a label, not a binding reference:\n //\n // - Property-assignment KEYS in object literals: `{ module: x }`\n // — the LHS `module` is a label, not a reference to Node's `module`.\n // - Property-access NAMES: `obj.foo` — `foo` is a property selector.\n // - Property-access in shorthand: `{ foo }` — `foo` IS a reference;\n // ts-morph's ShorthandPropertyAssignment parents that case, so we\n // only filter PropertyAssignment.name and PropertyAccessExpression.name.\n // - Type-annotation positions: `let x: foo` — `foo` is a type.\n // - Import/export specifier names.\n const parent = node.getParent();\n if (parent) {\n const parentKind = parent.getKind();\n // `{ module: x }` — name child of a PropertyAssignment.\n if (parentKind === SyntaxKind.PropertyAssignment) {\n const propertyName = (\n parent as { getNameNode?: () => unknown }\n ).getNameNode?.();\n if (propertyName === node) {\n return;\n }\n }\n // `obj.foo` — `foo` is the .name on the right of a dot.\n if (parentKind === SyntaxKind.PropertyAccessExpression) {\n const accessName = (\n parent as { getNameNode?: () => unknown }\n ).getNameNode?.();\n if (accessName === node) {\n return;\n }\n }\n // `{ foo: bar }` LHS for shorthand-style method declarations.\n if (parentKind === SyntaxKind.MethodDeclaration) {\n return;\n }\n // import { foo } / import { foo as bar } / export { foo }\n if (\n parentKind === SyntaxKind.ImportSpecifier ||\n parentKind === SyntaxKind.ExportSpecifier ||\n parentKind === SyntaxKind.NamespaceImport ||\n parentKind === SyntaxKind.ImportClause\n ) {\n return;\n }\n // Function/method parameter names.\n if (parentKind === SyntaxKind.Parameter) {\n return;\n }\n // Type references — `let x: foo` etc.\n if (\n parentKind === SyntaxKind.TypeReference ||\n parentKind === SyntaxKind.TypeQuery\n ) {\n return;\n }\n }\n\n if (DENIED_GLOBALS.has(name)) {\n const { line, column } = sourceFile.getLineAndColumnAtPos(\n node.getStart(),\n );\n errors.push({\n path: fileLabel,\n line,\n column,\n message: `identifier \"${name}\" is denied in the sandbox (FS/network/eval surface)`,\n });\n }\n // We don't reject unknown identifiers — they might be legitimate\n // members of an allowlist-imported binding (e.g. `system.events.foo`\n // — `foo` is an identifier in property-access position). The\n // import + denylist + dynamic-import check above already covers\n // the actual escape paths.\n });\n}\n\nexport function validateSandboxInput(\n files: PlaygroundFile[],\n): ValidationError[] {\n const errors: ValidationError[] = [];\n const project = new Project({\n useInMemoryFileSystem: true,\n compilerOptions: {\n target: 99, // ESNext\n module: 99, // ESNext\n allowJs: true,\n strict: false,\n },\n });\n\n for (const file of files) {\n project.createSourceFile(file.path, file.source, { overwrite: true });\n }\n\n for (const file of files) {\n checkImports(file.path, project, errors);\n checkDynamicImportsAndCalls(file.path, project, errors);\n checkPropertyAccessEscapes(file.path, project, errors);\n checkGlobalIdentifiers(file.path, project, errors);\n }\n\n return errors;\n}\n","/**\n * Strip absolute paths from a stack trace before it crosses the\n * worker→host→client boundary. Stack traces leak the sandbox host's\n * filesystem layout — `/Users/<name>/`, `/home/<user>/`, the mkdtemp\n * bundle directory, and on serverless surfaces `/var/task/...`. These\n * become a fingerprint of the host environment any sandbox client can\n * read. Sanitize once at the boundary so every error path benefits.\n *\n * Lives in its own module because `worker.ts` throws at top-level when\n * loaded outside a worker_threads context — extracting `sanitizeStack`\n * lets the unit test suite import the function directly.\n *\n * Terminator characters in the patterns below intentionally include\n * matched-delimiter punctuation (`)`, `]`, `>`, `\"`, `'`, `,`) so a\n * path inside an `at fn (…)` frame or a JSON-stringified `\"path\"`\n * field is bounded correctly. The character class is shared across\n * POSIX + Windows patterns to keep the boundary consistent.\n */\nconst PATH_TERMINATOR = \"[^\\\\s)\\\\]>\\\"',]\";\n\nexport function sanitizeStack(s: string | undefined): string {\n if (!s) return \"\";\n const TAIL = `${PATH_TERMINATOR}+`;\n return (\n s\n // file:// URLs\n .replace(new RegExp(`file:\\\\/\\\\/\\\\/${PATH_TERMINATOR}+`, \"g\"), \"<sandbox>\")\n // UNC paths — must run BEFORE the generic `\\\\` Windows pattern so\n // `\\\\server\\share\\file` becomes `<unc>`, not `<unc>\\share\\file`.\n .replace(new RegExp(`\\\\\\\\\\\\\\\\${TAIL}`, \"g\"), \"<unc>\")\n // Windows drive paths — `C:\\Users\\<user>\\...` and friends.\n // Captures any drive letter + `\\Users\\<name>` or `\\Documents and Settings\\<name>`.\n .replace(/[A-Za-z]:\\\\Users\\\\[^\\\\\\s)\\]>\"',]+/g, \"<home>\")\n .replace(\n /[A-Za-z]:\\\\Documents and Settings\\\\[^\\\\\\s)\\]>\"',]+/g,\n \"<home>\",\n )\n // Generic Windows absolute paths — `C:\\<anything-not-Users>` that\n // didn't match above. Catches CI runners (`D:\\a\\_work\\…`) and\n // tempdirs.\n .replace(/[A-Za-z]:\\\\[^\\\\\\s)\\]>\"',]+(?:\\\\[^\\\\\\s)\\]>\"',]+)+/g, \"<path>\")\n // POSIX dev-machine home directories — macOS + Linux\n .replace(new RegExp(`\\\\/Users\\\\/${TAIL}`, \"g\"), \"/<home>\")\n .replace(new RegExp(`\\\\/home\\\\/${TAIL}`, \"g\"), \"/<home>\")\n // macOS private-var prefix\n .replace(new RegExp(`\\\\/private\\\\/var\\\\/${TAIL}`, \"g\"), \"<tmp>\")\n // serverless lambda / vercel function task root\n .replace(new RegExp(`\\\\/var\\\\/task\\\\/${TAIL}`, \"g\"), \"<task>\")\n // generic tmp + lambda-runtime task paths\n .replace(new RegExp(`\\\\/tmp\\\\/${TAIL}`, \"g\"), \"<tmp>\")\n // /opt and /usr/local — common Linux distro prefixes that leak\n // package layout (e.g. `/opt/render/project/src/...`).\n .replace(new RegExp(`\\\\/opt\\\\/${TAIL}`, \"g\"), \"<opt>\")\n .replace(new RegExp(`\\\\/usr\\\\/local\\\\/${TAIL}`, \"g\"), \"<usr-local>\")\n // Common deploy roots — Heroku/Render/Docker/Codespaces/GitHub\n // Actions all root applications under one of these. Without\n // these, a worker error from a containerized host leaks the\n // deploy layout + package versions inside `/app/node_modules/`.\n .replace(new RegExp(`\\\\/app\\\\/${TAIL}`, \"g\"), \"<app>\")\n .replace(new RegExp(`\\\\/srv\\\\/${TAIL}`, \"g\"), \"<srv>\")\n .replace(new RegExp(`\\\\/workspace\\\\/${TAIL}`, \"g\"), \"<workspace>\")\n .replace(new RegExp(`\\\\/data\\\\/${TAIL}`, \"g\"), \"<data>\")\n // /etc and /root — high-sensitivity Linux paths (config files,\n // root home). A snippet that throws referencing one of these\n // should not echo the absolute path back to the caller.\n .replace(new RegExp(`\\\\/etc\\\\/${TAIL}`, \"g\"), \"<etc>\")\n .replace(new RegExp(`\\\\/root\\\\/${TAIL}`, \"g\"), \"<root>\")\n );\n}\n","/**\n * Public API for `@directive-run/sandbox`.\n *\n * Single entry point: `runInSandbox({files, timeoutMs})` validates the\n * payload against the AST allowlist, bundles the multi-file payload via\n * esbuild, and executes the result in a bounded worker_threads sandbox.\n * Returns a structured `SandboxResult` with captured logs, the final\n * facts snapshot, and any errors that occurred at any stage.\n *\n * Consumers:\n *\n * - `@directive-run/mcp` — the `run_in_sandbox` MCP tool returns the\n * result to AI clients alongside a `playground_link` URL.\n * - `directive-docs` — the `/api/run-sandbox` Next.js route wraps this\n * for the playground page's live DevTools transcript view.\n */\n\nimport { BundleError, bundleSandboxFiles } from \"./bundler.js\";\nimport { WorkerExecError, execInWorker } from \"./host.js\";\nimport { extractDerivationKeys } from \"./key-extractor.js\";\nimport { SandboxError } from \"./types.js\";\nimport type {\n PlaygroundFile,\n RunInSandboxInput,\n SandboxResult,\n} from \"./types.js\";\nimport { validateSandboxInput } from \"./validator.js\";\n\nexport {\n SandboxError,\n type PlaygroundFile,\n type RunInSandboxInput,\n type SandboxResult,\n} from \"./types.js\";\nexport type { ValidationError } from \"./validator.js\";\nexport { setMaxConcurrentWorkers } from \"./host.js\";\nexport { sanitizeStack } from \"./sanitize-stack.js\";\n\nconst MAX_PAYLOAD_BYTES = 200_000;\nconst MAX_FILES = 10;\nconst MAIN_PATH = \"src/main.ts\";\n\nfunction normalizeInput(input: RunInSandboxInput): PlaygroundFile[] {\n if (input.files && input.source) {\n throw new SandboxError(\n \"pass either `source` or `files`, not both\",\n \"input-invalid\",\n );\n }\n if (!input.files && !input.source) {\n throw new SandboxError(\n \"must pass either `source` or `files`\",\n \"input-invalid\",\n );\n }\n const files: PlaygroundFile[] = input.files\n ? input.files\n : [{ path: MAIN_PATH, source: input.source ?? \"\" }];\n\n if (files.length === 0) {\n throw new SandboxError(\"files array is empty\", \"input-invalid\");\n }\n if (files.length > MAX_FILES) {\n throw new SandboxError(\n `payload exceeds ${MAX_FILES} files`,\n \"input-invalid\",\n );\n }\n let totalBytes = 0;\n for (const file of files) {\n if (typeof file.path !== \"string\" || file.path.length === 0) {\n throw new SandboxError(\n \"every file must have a non-empty path\",\n \"input-invalid\",\n );\n }\n if (typeof file.source !== \"string\" || file.source.length === 0) {\n throw new SandboxError(\n `file \"${file.path}\" has empty source`,\n \"input-invalid\",\n );\n }\n totalBytes += Buffer.byteLength(file.source, \"utf8\");\n }\n if (totalBytes > MAX_PAYLOAD_BYTES) {\n throw new SandboxError(\n `total payload is ${totalBytes} bytes (max ${MAX_PAYLOAD_BYTES})`,\n \"input-invalid\",\n );\n }\n // Ensure src/main.ts is present — that's the runner / entry point.\n if (!files.some((f) => f.path === MAIN_PATH)) {\n throw new SandboxError(\n `payload must include \"${MAIN_PATH}\" — the runner entry point`,\n \"input-invalid\",\n );\n }\n return files;\n}\n\nexport async function runInSandbox(\n input: RunInSandboxInput,\n): Promise<SandboxResult> {\n let files: PlaygroundFile[];\n try {\n files = normalizeInput(input);\n } catch (err) {\n if (err instanceof SandboxError) {\n return {\n logs: [],\n facts: {},\n derived: {},\n errors: [err.message],\n durationMs: 0,\n timedOut: false,\n };\n }\n throw err;\n }\n\n const validationErrors = validateSandboxInput(files);\n if (validationErrors.length > 0) {\n return {\n logs: [],\n facts: {},\n derived: {},\n errors: validationErrors.map(\n (e) => `${e.path}:${e.line}:${e.column} — ${e.message}`,\n ),\n durationMs: 0,\n timedOut: false,\n };\n }\n\n // Pre-extract derivation key names from the source files so the\n // worker can iterate them after settle. The `system.derive` proxy\n // has no `ownKeys` trap.\n const derivationKeys = extractDerivationKeys(files);\n\n let bundled: { source: string; bytes: number };\n try {\n bundled = await bundleSandboxFiles(files);\n } catch (err) {\n if (err instanceof BundleError) {\n return {\n logs: [],\n facts: {},\n derived: {},\n errors: [err.message],\n durationMs: 0,\n timedOut: false,\n };\n }\n throw err;\n }\n\n try {\n const result = await execInWorker({\n bundledSource: bundled.source,\n derivationKeys,\n timeoutMs: input.timeoutMs,\n signal: input.signal,\n });\n return result;\n } catch (err) {\n if (err instanceof WorkerExecError) {\n return {\n logs: [],\n facts: {},\n derived: {},\n errors: [err.message],\n durationMs: 0,\n timedOut: err.code === \"timeout\",\n };\n }\n throw err;\n }\n}\n"],"mappings":"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","names":["createRequire","pathToFileURL","build","resolveDirectivePackageToFileUrl","specifier","resolved","ENTRY_PATH","VIRTUAL_NAMESPACE","BundleError","message","cause","injectEarlyCapture","bundled","re","match","decl","capture","bundleSandboxFiles","files","f","fileMap","file","rawSource","b","args","importerDir","stripped","candidates","p","candidate","contents","epilogue","source","err","existsSync","mkdtempSync","rmSync","writeFileSync","tmpdir","dirname","join","fileURLToPath","pathToFileURL","Worker","MIN_TIMEOUT_MS","MAX_TIMEOUT_MS","DEFAULT_TIMEOUT_MS","DEFAULT_MAX_WORKERS","maxConcurrentWorkers","activeWorkers","waiters","wakeNextWaiter","next","setMaxConcurrentWorkers","value","prev","acquireSlot","signal","resolve","reject","waiter","onAbort","originalResolve","originalReject","reason","releaseSlot","resolveWorkerPath","staticUrl","path","createRequire","WorkerExecError","message","code","clampTimeout","raw","getTempBundleDir","osTmp","workerPath","writeBundleToTemp","bundledSource","baseDir","dir","bundlePath","execInWorker","input","timeoutMs","cleanupTempDir","err","worker","timer","timedOut","aborted","abortListener","startMs","settled","msg","findMatchingClose","source","openBrace","depth","i","ch","collectTopLevelKeys","block","keys","segStart","braceDepth","parenDepth","bracketDepth","flushSegment","end","seg","m","j","extractTopLevelKeys","sectionName","headerRe","headerMatch","closeBrace","extractDerivationKeys","files","seen","out","file","k","SandboxError","message","code","Project","SyntaxKind","ALLOWED_DIRECTIVE_PACKAGES","DENIED_DIRECTIVE_PACKAGES","extractDirectivePackage","specifier","match","isAllowedImport","dpkg","ALLOWED_GLOBALS","DENIED_GLOBALS","importRejectionMessage","checkImports","fileLabel","project","errors","sourceFile","decl","moduleSpecifier","line","column","checkDynamicImportsAndCalls","node","parent","text","checkPropertyAccessEscapes","report","message","stripStringLiteral","trimmed","kind","name","expression","argument","receiverText","argText","literal","exprText","reflectAccess","args","firstArg","secondArgLiteral","checkGlobalIdentifiers","localBindings","n","def","ns","v","f","c","parentKind","validateSandboxInput","files","file","PATH_TERMINATOR","sanitizeStack","s","TAIL","MAX_PAYLOAD_BYTES","MAX_FILES","MAIN_PATH","normalizeInput","input","SandboxError","files","totalBytes","file","f","runInSandbox","err","validationErrors","validateSandboxInput","e","derivationKeys","extractDerivationKeys","bundled","bundleSandboxFiles","BundleError","execInWorker","WorkerExecError"]}
|
package/dist/worker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{performance as
|
|
2
|
-
`))}finally{n()}let s
|
|
3
|
-
${i.stack
|
|
1
|
+
import{performance as $}from"perf_hooks";import{parentPort as k}from"worker_threads";var y=new Set(["http:","https:"]);function g(t){let e=t.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);if(!e)return!1;let r=Number.parseInt(e[1],10),n=Number.parseInt(e[2],10),o=Number.parseInt(e[3],10),s=Number.parseInt(e[4],10);return r>255||n>255||o>255||s>255?!1:r===0||r===127||r===10||r===172&&n>=16&&n<=31||r===192&&n===168||r===169&&n===254||r===100&&n>=64&&n<=127||r>=224&&r<=239||r>=240}function w(t){let e=t.replace(/^\[|\]$/g,"").toLowerCase();if(e==="::1"||e==="::"||e.startsWith("fe80:")||e.startsWith("fe8")||e.startsWith("fc")||e.startsWith("fd"))return!0;let r=e.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/);if(r&&g(r[1]))return!0;let n=e.match(/::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);if(n){let o=Number.parseInt(n[1],16),s=Number.parseInt(n[2],16);if(Number.isFinite(o)&&Number.isFinite(s)){let c=o>>8&255,l=o&255,u=s>>8&255,i=s&255;if(g(`${c}.${l}.${u}.${i}`))return!0}}return!1}function v(t){let e=t.toLowerCase();return!!(e==="localhost"||e==="localhost.localdomain"||e.endsWith(".local")||e.endsWith(".internal"))}function R(t){let e;try{e=new URL(t)}catch{return{allow:!1,reason:`not a valid URL: "${t}"`}}if(!y.has(e.protocol))return{allow:!1,reason:`protocol "${e.protocol}" is not allowed (only http:, https:)`};let r=e.hostname;return r?v(r)?{allow:!1,reason:`hostname "${r}" resolves to localhost or internal-only`}:g(r)?{allow:!1,reason:`IPv4 ${r} is in a private / loopback / link-local range`}:w(r)?{allow:!1,reason:`IPv6 ${r} is in a private / loopback / link-local range`}:{allow:!0}:{allow:!1,reason:"URL has no hostname"}}async function x(t){if(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(t)||t.includes(":"))return null;try{let{lookup:e}=await import("dns/promises"),r=await e(t,{all:!0});for(let n of r){if(n.family===4&&g(n.address))return`hostname "${t}" resolves to private IPv4 ${n.address}`;if(n.family===6&&w(n.address))return`hostname "${t}" resolves to private IPv6 ${n.address}`}return null}catch{return null}}function m(){let t=globalThis.fetch??(async()=>{throw new Error("fetch is not available in this Node version")}),e=async(r,n)=>{let o=typeof r=="string"?r:r instanceof URL?r.toString():r.url,s=R(o);if(!s.allow)throw new Error(`sandbox: outbound fetch blocked \u2014 ${s.reason??"denied"}`);let c="";try{c=new URL(o).hostname.replace(/^\[|\]$/g,"")}catch{}if(c){let l=await x(c);if(l!==null)throw new Error(`sandbox: outbound fetch blocked \u2014 ${l}`)}return t(r,n)};globalThis.fetch=e}var b=`[^\\s)\\]>"',]`;function p(t){if(!t)return"";let e=`${b}+`;return t.replace(new RegExp(`file:\\/\\/\\/${b}+`,"g"),"<sandbox>").replace(new RegExp(`\\\\\\\\${e}`,"g"),"<unc>").replace(/[A-Za-z]:\\Users\\[^\\\s)\]>"',]+/g,"<home>").replace(/[A-Za-z]:\\Documents and Settings\\[^\\\s)\]>"',]+/g,"<home>").replace(/[A-Za-z]:\\[^\\\s)\]>"',]+(?:\\[^\\\s)\]>"',]+)+/g,"<path>").replace(new RegExp(`\\/Users\\/${e}`,"g"),"/<home>").replace(new RegExp(`\\/home\\/${e}`,"g"),"/<home>").replace(new RegExp(`\\/private\\/var\\/${e}`,"g"),"<tmp>").replace(new RegExp(`\\/var\\/task\\/${e}`,"g"),"<task>").replace(new RegExp(`\\/tmp\\/${e}`,"g"),"<tmp>").replace(new RegExp(`\\/opt\\/${e}`,"g"),"<opt>").replace(new RegExp(`\\/usr\\/local\\/${e}`,"g"),"<usr-local>").replace(new RegExp(`\\/app\\/${e}`,"g"),"<app>").replace(new RegExp(`\\/srv\\/${e}`,"g"),"<srv>").replace(new RegExp(`\\/workspace\\/${e}`,"g"),"<workspace>").replace(new RegExp(`\\/data\\/${e}`,"g"),"<data>").replace(new RegExp(`\\/etc\\/${e}`,"g"),"<etc>").replace(new RegExp(`\\/root\\/${e}`,"g"),"<root>")}if(!k)throw new Error("@directive-run/sandbox/worker must be loaded via worker_threads");m();var h=k;function E(t){if(typeof t=="string")return t;if(t==null)return String(t);if(typeof t=="object"){let e=t;if(e.$store&&typeof e.$store.toObject=="function")try{return JSON.stringify(e.$store.toObject())}catch{}if(typeof e.$snapshot=="function")try{return JSON.stringify(e.$snapshot())}catch{}}try{return JSON.stringify(t)}catch{return String(t)}}function S(t,e){let r={log:console.log,info:console.info,warn:console.warn,error:console.error,debug:console.debug},n=(o,s)=>{let c=s.map(l=>E(l)).join(" ");t.push(o?`${o} ${c}`:c)};return console.log=(...o)=>n(e.log,o),console.info=(...o)=>n(e.log,o),console.warn=(...o)=>n(e.warn,o),console.error=(...o)=>n(e.error,o),console.debug=(...o)=>n(e.log,o),()=>{console.log=r.log,console.info=r.info,console.warn=r.warn,console.error=r.error,console.debug=r.debug}}async function _(t){let e=[],r=[],n=S(e,{log:"[log]",warn:"[warn]",error:"[error]"});globalThis.__directiveSandbox_system__=null;let o=$.now();try{await import(t.bundlePath)}catch(i){let a=i,f=[a.message??String(i)];if(a.code&&f.push(`[code: ${a.code}]`),a.stack&&f.push(p(a.stack)),a.cause){let d=a.cause;f.push(`Cause: ${d.message??String(d)}`),d.stack&&f.push(p(d.stack))}r.push(f.join(`
|
|
2
|
+
`))}finally{n()}let s=$.now()-o,c={},l={},u=globalThis.__directiveSandbox_system__;if(u){try{c=u.facts.$store.toObject()}catch(i){r.push(`facts snapshot failed: ${i.message}
|
|
3
|
+
${p(i.stack)}`)}if(t.derivationKeys.length>0&&u.derive)for(let i of t.derivationKeys)try{let a=u.derive[i];a!==void 0&&(l[i]=a)}catch(a){r.push(`derivation "${i}" snapshot failed: ${a.message}`)}try{u.destroy()}catch{}globalThis.__directiveSandbox_system__=null}return{logs:e,facts:c,derived:l,errors:r,durationMs:s,timedOut:!1}}h.once("message",async t=>{try{let r={ok:!0,result:await _(t)};h.postMessage(r)}catch(e){let r={ok:!1,error:p(e.stack)||e.message||String(e)};h.postMessage(r)}});
|
|
4
4
|
//# sourceMappingURL=worker.js.map
|
package/dist/worker.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/worker.ts","../src/fetch-wrapper.ts"],"sourcesContent":["/**\n * Worker-thread entry. Receives a bundled ESM string + timeout, hooks\n * `console.*` to a capture buffer, evaluates the bundle via a data:\n * URL import (Node's native ESM loader handles top-level await for us),\n * then sends the SandboxResult back.\n *\n * The host (`./host.ts`) enforces the wall-clock budget by calling\n * `worker.terminate()` on overrun — there's no cooperative cancellation\n * from inside the worker. This keeps the worker code simple and means\n * even hostile snippets that try to outlast the budget get hard-killed.\n *\n * Why a data: URL import rather than `vm.runInContext`? `vm` doesn't\n * link ESM imports, so a snippet that imports `@directive-run/core`\n * fails at parse time. Data URL imports go through Node's real ESM\n * loader, which DOES link external imports — `@directive-run/core` is\n * already installed in the parent's node_modules, so the worker finds\n * it via standard resolution. The cost: data URL imports happen\n * exactly once per worker (which is what we want — one-shot execution).\n */\n\nimport { performance } from \"node:perf_hooks\";\nimport { parentPort } from \"node:worker_threads\";\nimport { installFetchWrapper } from \"./fetch-wrapper.js\";\nimport type {\n SandboxResult,\n WorkerInputMessage,\n WorkerOutputMessage,\n} from \"./types.js\";\n\nif (!parentPort) {\n throw new Error(\n \"@directive-run/sandbox/worker must be loaded via worker_threads\",\n );\n}\n\n// Phase A audit P0-S2: install the SSRF fetch wrapper BEFORE the\n// bundle is imported so @directive-run/query's / @directive-run/ai's\n// internal fetch calls (which the validator can't see because they\n// live in external module bodies) hit the loopback / private-IP /\n// IMDS blocklist.\ninstallFetchWrapper();\n\nconst port = parentPort;\n\n/**\n * Format a single console.log argument. Directive's `system.facts` is\n * a Proxy over a `FactsStore`; the audit (P0-DM1) found that\n * `JSON.stringify(system.facts)` produces `\"{}\"` (or store internals)\n * because the proxy has no `toJSON`. So `console.log(\"[start] facts:\",\n * system.facts)` rendered as `[start] facts: {}` in the transcript\n * while `result.facts` correctly showed the snapshot — two\n * contradictory views in the same response.\n *\n * Fix: detect Directive's facts/derive proxies via the `$store` or\n * `$snapshot` escape hatches the proxy DOES expose, snapshot via the\n * store's `toObject()` (the same call result.facts uses), and JSON-\n * stringify the snapshot. Falls back to JSON.stringify for any value\n * that doesn't look like a Directive proxy.\n */\nfunction serializeArg(arg: unknown): string {\n if (typeof arg === \"string\") {\n return arg;\n }\n if (arg === null || arg === undefined) {\n return String(arg);\n }\n // Directive facts/derive proxy fingerprints. `.$store` is the\n // internal FactsStore; `.$snapshot` is the alternate accessor.\n // Either returns a plain object when called.\n if (typeof arg === \"object\") {\n const obj = arg as {\n $store?: { toObject?: () => Record<string, unknown> };\n $snapshot?: () => Record<string, unknown>;\n };\n if (obj.$store && typeof obj.$store.toObject === \"function\") {\n try {\n return JSON.stringify(obj.$store.toObject());\n } catch {\n // fall through\n }\n }\n if (typeof obj.$snapshot === \"function\") {\n try {\n return JSON.stringify(obj.$snapshot());\n } catch {\n // fall through\n }\n }\n }\n try {\n return JSON.stringify(arg);\n } catch {\n return String(arg);\n }\n}\n\n/**\n * Replace console.{log,info,warn,error,debug} with capture functions\n * that push to a shared buffer. The original references are restored\n * in `finally` so this doesn't leak across hot-reloads in tests.\n */\nfunction captureConsole(\n buffer: string[],\n prefix: { log: string; warn: string; error: string },\n): () => void {\n const originals = {\n log: console.log,\n info: console.info,\n warn: console.warn,\n error: console.error,\n debug: console.debug,\n };\n const format = (label: string, args: unknown[]) => {\n const text = args.map((a) => serializeArg(a)).join(\" \");\n buffer.push(label ? `${label} ${text}` : text);\n };\n console.log = (...a: unknown[]) => format(prefix.log, a);\n console.info = (...a: unknown[]) => format(prefix.log, a);\n console.warn = (...a: unknown[]) => format(prefix.warn, a);\n console.error = (...a: unknown[]) => format(prefix.error, a);\n console.debug = (...a: unknown[]) => format(prefix.log, a);\n\n return () => {\n console.log = originals.log;\n console.info = originals.info;\n console.warn = originals.warn;\n console.error = originals.error;\n console.debug = originals.debug;\n };\n}\n\n/**\n * Look for a system on globalThis the snippet attached. The runner\n * scaffold emits `const system = createSystem({...}); system.start();`\n * at top level, but inside an ESM module those bindings are scoped to\n * the module's own scope — we can't read them from the worker.\n *\n * Workaround: the worker patches `createSystem` to also stash the\n * returned system on `globalThis.__directiveSandbox_system__`. After\n * the bundle finishes, we read facts from there.\n *\n * This monkey-patch happens via a tiny ESM module we prepend to the\n * bundle before evaluation (see `wrapForExecution`).\n */\ninterface SandboxSystem {\n facts: { $store: { toObject(): Record<string, unknown> } };\n derive: Record<string, unknown>;\n destroy(): void;\n}\n\ndeclare global {\n // eslint-disable-next-line no-var\n var __directiveSandbox_system__: SandboxSystem | null;\n}\n\nasync function runOne(message: WorkerInputMessage): Promise<SandboxResult> {\n const logs: string[] = [];\n const errors: string[] = [];\n const restoreConsole = captureConsole(logs, {\n log: \"[log]\",\n warn: \"[warn]\",\n error: \"[error]\",\n });\n globalThis.__directiveSandbox_system__ = null;\n const startMs = performance.now();\n try {\n // Import the bundle via file:// URL. Node's ESM loader resolves\n // `@directive-run/core` against node_modules relative to the\n // file path's anchor. The bundler appends a trailing assignment\n // to the entry module that lifts the runner's `system` binding\n // onto `globalThis.__directiveSandbox_system__` so we can read\n // it from here after execution.\n await import(message.bundlePath);\n } catch (err) {\n const e = err as Error & { code?: string; cause?: unknown };\n const parts = [e.message ?? String(err)];\n if (e.code) {\n parts.push(`[code: ${e.code}]`);\n }\n if (e.stack) {\n parts.push(e.stack);\n }\n if (e.cause) {\n const cause = e.cause as Error;\n parts.push(`Cause: ${cause.message ?? String(cause)}`);\n if (cause.stack) {\n parts.push(cause.stack);\n }\n }\n errors.push(parts.join(\"\\n\"));\n } finally {\n restoreConsole();\n }\n const durationMs = performance.now() - startMs;\n\n let facts: Record<string, unknown> = {};\n const derived: Record<string, unknown> = {};\n // Cast through unknown because TS narrows `globalThis.X` to `null`\n // after the earlier assignment, even though the bundle's epilogue\n // may have populated it before we read here.\n const system = globalThis.__directiveSandbox_system__ as SandboxSystem | null;\n if (system) {\n try {\n facts = system.facts.$store.toObject();\n } catch (err) {\n errors.push(\n `facts snapshot failed: ${(err as Error).message}\\n${(err as Error).stack ?? \"\"}`,\n );\n }\n // Phase A audit P0-DM2: snapshot derivations. The host pre-extracts\n // the key names from the source files (system.derive's proxy has no\n // ownKeys trap) and passes them via message.derivationKeys; we read\n // each via `system.derive[key]`, swallowing per-key errors so one\n // bad derivation doesn't kill the whole snapshot.\n if (message.derivationKeys.length > 0 && system.derive) {\n for (const key of message.derivationKeys) {\n try {\n const value = (system.derive as Record<string, unknown>)[key];\n if (value !== undefined) {\n derived[key] = value;\n }\n } catch (err) {\n errors.push(\n `derivation \"${key}\" snapshot failed: ${(err as Error).message}`,\n );\n }\n }\n }\n try {\n system.destroy();\n } catch {\n // best-effort\n }\n globalThis.__directiveSandbox_system__ = null;\n }\n\n return {\n logs,\n facts,\n derived,\n errors,\n durationMs,\n timedOut: false,\n };\n}\n\nport.once(\"message\", async (msg: WorkerInputMessage) => {\n try {\n const result = await runOne(msg);\n const out: WorkerOutputMessage = { ok: true, result };\n port.postMessage(out);\n } catch (err) {\n const out: WorkerOutputMessage = {\n ok: false,\n error: (err as Error).stack ?? (err as Error).message ?? String(err),\n };\n port.postMessage(out);\n }\n});\n","/**\n * Outbound fetch wrapper installed in the worker BEFORE the user's\n * bundle imports anything. The validator already blocks `fetch` as a\n * free identifier AND `globalThis.fetch` property access, so the\n * user's snippet can't call fetch directly. But `@directive-run/query`\n * and `@directive-run/ai` are in the allowlist and their package code\n * calls global `fetch` internally — the validator never sees those\n * calls because they live in external module bodies.\n *\n * Phase A audit P0-S2: a snippet that builds a `createBaseQuery({\n * baseUrl: \"http://169.254.169.254\" })` triggers an outbound HTTP\n * request from the worker to AWS IMDS. On the directive.run/api/run-\n * sandbox Vercel surface, that's SSRF from a privileged egress.\n *\n * This wrapper rejects:\n *\n * - Loopback (127.0.0.0/8, [::1], localhost)\n * - Link-local (169.254.0.0/16 — includes IMDS at .169.254)\n * - RFC-1918 private (10.x, 172.16-31.x, 192.168.x)\n * - Multicast / broadcast / reserved ranges\n * - Non-HTTP(S) protocols (file://, ftp://, gopher://, data://, blob://)\n * - URLs without a hostname (relative paths, javascript:, etc.)\n *\n * Allows:\n *\n * - Any public IPv4 / IPv6 address\n * - Any hostname that resolves to public (resolution happens at request\n * time; we don't pre-resolve because Node's fetch doesn't expose the\n * resolved IP before sending. The host header is parsed for the\n * surface-level check; DNS rebinding is theoretical here because\n * external attackers can't control internal DNS, but documented as\n * a known gap.)\n */\n\nconst ALLOWED_PROTOCOLS = new Set([\"http:\", \"https:\"]);\n\nfunction isPrivateIPv4(host: string): boolean {\n // Match IPv4 dotted-quad. We don't accept octal/hex forms — Node's\n // URL parser rejects those for hostnames anyway.\n const m = host.match(/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/);\n if (!m) return false;\n const a = Number.parseInt(m[1]!, 10);\n const b = Number.parseInt(m[2]!, 10);\n const c = Number.parseInt(m[3]!, 10);\n const d = Number.parseInt(m[4]!, 10);\n if (a > 255 || b > 255 || c > 255 || d > 255) {\n // Malformed — let it through; Node's fetch will reject the URL.\n return false;\n }\n // 0.0.0.0/8 — \"this network\", treated as unrouted/private here.\n if (a === 0) return true;\n // 127.0.0.0/8 — loopback.\n if (a === 127) return true;\n // 10.0.0.0/8 — RFC-1918 Class A.\n if (a === 10) return true;\n // 172.16.0.0/12 — RFC-1918 Class B.\n if (a === 172 && b >= 16 && b <= 31) return true;\n // 192.168.0.0/16 — RFC-1918 Class C.\n if (a === 192 && b === 168) return true;\n // 169.254.0.0/16 — link-local, includes AWS/GCP/Azure IMDS at\n // 169.254.169.254. THIS is the SSRF target we care most about.\n if (a === 169 && b === 254) return true;\n // 100.64.0.0/10 — carrier-grade NAT, not internet-routable.\n if (a === 100 && b >= 64 && b <= 127) return true;\n // 224.0.0.0/4 — multicast.\n if (a >= 224 && a <= 239) return true;\n // 240.0.0.0/4 — reserved / experimental.\n if (a >= 240) return true;\n return false;\n}\n\nfunction isPrivateIPv6(host: string): boolean {\n // Strip brackets the URL parser leaves in place.\n const stripped = host.replace(/^\\[|\\]$/g, \"\").toLowerCase();\n // Loopback.\n if (stripped === \"::1\") return true;\n // Unspecified / \"any\".\n if (stripped === \"::\") return true;\n // Link-local fe80::/10\n if (stripped.startsWith(\"fe80:\") || stripped.startsWith(\"fe8\")) return true;\n // Unique local fc00::/7 (fc and fd prefixes).\n if (stripped.startsWith(\"fc\") || stripped.startsWith(\"fd\")) return true;\n // IPv4-mapped IPv6 (::ffff:169.254.169.254) — extract the v4 tail\n // and re-check. Node's URL parser may normalize the literal-IPv4\n // form to its hex equivalent (`::ffff:a9fe:a9fe`), so handle both.\n const mappedV4 = stripped.match(/::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/);\n if (mappedV4 && isPrivateIPv4(mappedV4[1]!)) {\n return true;\n }\n const mappedHex = stripped.match(/::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);\n if (mappedHex) {\n const hi = Number.parseInt(mappedHex[1]!, 16);\n const lo = Number.parseInt(mappedHex[2]!, 16);\n if (Number.isFinite(hi) && Number.isFinite(lo)) {\n const a = (hi >> 8) & 0xff;\n const b = hi & 0xff;\n const c = (lo >> 8) & 0xff;\n const d = lo & 0xff;\n if (isPrivateIPv4(`${a}.${b}.${c}.${d}`)) {\n return true;\n }\n }\n }\n return false;\n}\n\nfunction isLocalHostname(host: string): boolean {\n const lower = host.toLowerCase();\n if (lower === \"localhost\") return true;\n if (lower === \"localhost.localdomain\") return true;\n if (lower.endsWith(\".local\")) return true;\n if (lower.endsWith(\".internal\")) return true;\n return false;\n}\n\ninterface UrlCheck {\n allow: boolean;\n reason?: string;\n}\n\nexport function checkSandboxFetchUrl(rawUrl: string): UrlCheck {\n let parsed: URL;\n try {\n parsed = new URL(rawUrl);\n } catch {\n return { allow: false, reason: `not a valid URL: \"${rawUrl}\"` };\n }\n if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) {\n return {\n allow: false,\n reason: `protocol \"${parsed.protocol}\" is not allowed (only http:, https:)`,\n };\n }\n const host = parsed.hostname;\n if (!host) {\n return { allow: false, reason: \"URL has no hostname\" };\n }\n if (isLocalHostname(host)) {\n return {\n allow: false,\n reason: `hostname \"${host}\" resolves to localhost or internal-only`,\n };\n }\n if (isPrivateIPv4(host)) {\n return {\n allow: false,\n reason: `IPv4 ${host} is in a private / loopback / link-local range`,\n };\n }\n if (isPrivateIPv6(host)) {\n return {\n allow: false,\n reason: `IPv6 ${host} is in a private / loopback / link-local range`,\n };\n }\n return { allow: true };\n}\n\n/**\n * Install the wrapper on `globalThis.fetch`. Called from the worker\n * BEFORE the bundle is imported so any `fetch` calls inside the user's\n * code or inside allowlisted packages (`@directive-run/query`,\n * `@directive-run/ai`) hit the wrapper.\n *\n * Safe to call when `fetch` is undefined (older Node); just installs\n * the rejection wrapper.\n */\nexport function installFetchWrapper(): void {\n const original =\n (globalThis as { fetch?: typeof fetch }).fetch ??\n ((async () => {\n throw new Error(\"fetch is not available in this Node version\");\n }) as typeof fetch);\n\n const wrapped: typeof fetch = async (\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> => {\n const rawUrl =\n typeof input === \"string\"\n ? input\n : input instanceof URL\n ? input.toString()\n : (input as Request).url;\n const decision = checkSandboxFetchUrl(rawUrl);\n if (!decision.allow) {\n throw new Error(\n `sandbox: outbound fetch blocked — ${decision.reason ?? \"denied\"}`,\n );\n }\n return original(input, init);\n };\n\n (globalThis as { fetch: typeof fetch }).fetch = wrapped;\n}\n"],"mappings":"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","names":["performance","parentPort","ALLOWED_PROTOCOLS","isPrivateIPv4","host","m","a","b","c","d","isPrivateIPv6","stripped","mappedV4","mappedHex","hi","lo","isLocalHostname","lower","checkSandboxFetchUrl","rawUrl","parsed","installFetchWrapper","original","wrapped","input","init","decision","parentPort","installFetchWrapper","port","serializeArg","arg","obj","captureConsole","buffer","prefix","originals","format","label","args","text","a","runOne","message","logs","errors","restoreConsole","startMs","performance","err","e","parts","cause","durationMs","facts","derived","system","key","value","msg","out"]}
|
|
1
|
+
{"version":3,"sources":["../src/worker.ts","../src/fetch-wrapper.ts","../src/sanitize-stack.ts"],"sourcesContent":["/**\n * Worker-thread entry. Receives a bundled ESM string + timeout, hooks\n * `console.*` to a capture buffer, evaluates the bundle via a data:\n * URL import (Node's native ESM loader handles top-level await for us),\n * then sends the SandboxResult back.\n *\n * The host (`./host.ts`) enforces the wall-clock budget by calling\n * `worker.terminate()` on overrun — there's no cooperative cancellation\n * from inside the worker. This keeps the worker code simple and means\n * even hostile snippets that try to outlast the budget get hard-killed.\n *\n * Why a data: URL import rather than `vm.runInContext`? `vm` doesn't\n * link ESM imports, so a snippet that imports `@directive-run/core`\n * fails at parse time. Data URL imports go through Node's real ESM\n * loader, which DOES link external imports — `@directive-run/core` is\n * already installed in the parent's node_modules, so the worker finds\n * it via standard resolution. The cost: data URL imports happen\n * exactly once per worker (which is what we want — one-shot execution).\n */\n\nimport { performance } from \"node:perf_hooks\";\nimport { parentPort } from \"node:worker_threads\";\nimport { installFetchWrapper } from \"./fetch-wrapper.js\";\nimport { sanitizeStack } from \"./sanitize-stack.js\";\nimport type {\n SandboxResult,\n WorkerInputMessage,\n WorkerOutputMessage,\n} from \"./types.js\";\n\nif (!parentPort) {\n throw new Error(\n \"@directive-run/sandbox/worker must be loaded via worker_threads\",\n );\n}\n\n// Install the SSRF fetch wrapper BEFORE the bundle is imported so\n// @directive-run/query's / @directive-run/ai's internal fetch calls\n// (which the validator can't see because they live in external module\n// bodies) hit the loopback / private-IP / IMDS blocklist.\ninstallFetchWrapper();\n\nconst port = parentPort;\n\n/**\n * Format a single console.log argument. Directive's `system.facts` is\n * a Proxy over a `FactsStore`; the audit found that\n * `JSON.stringify(system.facts)` produces `\"{}\"` (or store internals)\n * because the proxy has no `toJSON`. So `console.log(\"[start] facts:\",\n * system.facts)` rendered as `[start] facts: {}` in the transcript\n * while `result.facts` correctly showed the snapshot — two\n * contradictory views in the same response.\n *\n * Fix: detect Directive's facts/derive proxies via the `$store` or\n * `$snapshot` escape hatches the proxy DOES expose, snapshot via the\n * store's `toObject()` (the same call result.facts uses), and JSON-\n * stringify the snapshot. Falls back to JSON.stringify for any value\n * that doesn't look like a Directive proxy.\n */\nfunction serializeArg(arg: unknown): string {\n if (typeof arg === \"string\") {\n return arg;\n }\n if (arg === null || arg === undefined) {\n return String(arg);\n }\n // Directive facts/derive proxy fingerprints. `.$store` is the\n // internal FactsStore; `.$snapshot` is the alternate accessor.\n // Either returns a plain object when called.\n if (typeof arg === \"object\") {\n const obj = arg as {\n $store?: { toObject?: () => Record<string, unknown> };\n $snapshot?: () => Record<string, unknown>;\n };\n if (obj.$store && typeof obj.$store.toObject === \"function\") {\n try {\n return JSON.stringify(obj.$store.toObject());\n } catch {\n // fall through\n }\n }\n if (typeof obj.$snapshot === \"function\") {\n try {\n return JSON.stringify(obj.$snapshot());\n } catch {\n // fall through\n }\n }\n }\n try {\n return JSON.stringify(arg);\n } catch {\n return String(arg);\n }\n}\n\n/**\n * Replace console.{log,info,warn,error,debug} with capture functions\n * that push to a shared buffer. The original references are restored\n * in `finally` so this doesn't leak across hot-reloads in tests.\n */\nfunction captureConsole(\n buffer: string[],\n prefix: { log: string; warn: string; error: string },\n): () => void {\n const originals = {\n log: console.log,\n info: console.info,\n warn: console.warn,\n error: console.error,\n debug: console.debug,\n };\n const format = (label: string, args: unknown[]) => {\n const text = args.map((a) => serializeArg(a)).join(\" \");\n buffer.push(label ? `${label} ${text}` : text);\n };\n console.log = (...a: unknown[]) => format(prefix.log, a);\n console.info = (...a: unknown[]) => format(prefix.log, a);\n console.warn = (...a: unknown[]) => format(prefix.warn, a);\n console.error = (...a: unknown[]) => format(prefix.error, a);\n console.debug = (...a: unknown[]) => format(prefix.log, a);\n\n return () => {\n console.log = originals.log;\n console.info = originals.info;\n console.warn = originals.warn;\n console.error = originals.error;\n console.debug = originals.debug;\n };\n}\n\n/**\n * Look for a system on globalThis the snippet attached. The runner\n * scaffold emits `const system = createSystem({...}); system.start();`\n * at top level, but inside an ESM module those bindings are scoped to\n * the module's own scope — we can't read them from the worker.\n *\n * Workaround: the worker patches `createSystem` to also stash the\n * returned system on `globalThis.__directiveSandbox_system__`. After\n * the bundle finishes, we read facts from there.\n *\n * This monkey-patch happens via a tiny ESM module we prepend to the\n * bundle before evaluation (see `wrapForExecution`).\n */\ninterface SandboxSystem {\n facts: { $store: { toObject(): Record<string, unknown> } };\n derive: Record<string, unknown>;\n destroy(): void;\n}\n\ndeclare global {\n // eslint-disable-next-line no-var\n var __directiveSandbox_system__: SandboxSystem | null;\n}\n\nasync function runOne(message: WorkerInputMessage): Promise<SandboxResult> {\n const logs: string[] = [];\n const errors: string[] = [];\n const restoreConsole = captureConsole(logs, {\n log: \"[log]\",\n warn: \"[warn]\",\n error: \"[error]\",\n });\n globalThis.__directiveSandbox_system__ = null;\n const startMs = performance.now();\n try {\n // Import the bundle via file:// URL. Node's ESM loader resolves\n // `@directive-run/core` against node_modules relative to the\n // file path's anchor. The bundler appends a trailing assignment\n // to the entry module that lifts the runner's `system` binding\n // onto `globalThis.__directiveSandbox_system__` so we can read\n // it from here after execution.\n await import(message.bundlePath);\n } catch (err) {\n const e = err as Error & { code?: string; cause?: unknown };\n const parts = [e.message ?? String(err)];\n if (e.code) {\n parts.push(`[code: ${e.code}]`);\n }\n if (e.stack) {\n parts.push(sanitizeStack(e.stack));\n }\n if (e.cause) {\n const cause = e.cause as Error;\n parts.push(`Cause: ${cause.message ?? String(cause)}`);\n if (cause.stack) {\n parts.push(sanitizeStack(cause.stack));\n }\n }\n errors.push(parts.join(\"\\n\"));\n } finally {\n restoreConsole();\n }\n const durationMs = performance.now() - startMs;\n\n let facts: Record<string, unknown> = {};\n const derived: Record<string, unknown> = {};\n // Cast through unknown because TS narrows `globalThis.X` to `null`\n // after the earlier assignment, even though the bundle's epilogue\n // may have populated it before we read here.\n const system = globalThis.__directiveSandbox_system__ as SandboxSystem | null;\n if (system) {\n try {\n facts = system.facts.$store.toObject();\n } catch (err) {\n errors.push(\n `facts snapshot failed: ${(err as Error).message}\\n${sanitizeStack((err as Error).stack)}`,\n );\n }\n // Snapshot derivations. The host pre-extracts the key names from\n // the source files (system.derive's proxy has no ownKeys trap) and\n // passes them via message.derivationKeys; we read each via\n // `system.derive[key]`, swallowing per-key errors so one bad\n // derivation doesn't kill the whole snapshot.\n if (message.derivationKeys.length > 0 && system.derive) {\n for (const key of message.derivationKeys) {\n try {\n const value = (system.derive as Record<string, unknown>)[key];\n if (value !== undefined) {\n derived[key] = value;\n }\n } catch (err) {\n errors.push(\n `derivation \"${key}\" snapshot failed: ${(err as Error).message}`,\n );\n }\n }\n }\n try {\n system.destroy();\n } catch {\n // best-effort\n }\n globalThis.__directiveSandbox_system__ = null;\n }\n\n return {\n logs,\n facts,\n derived,\n errors,\n durationMs,\n timedOut: false,\n };\n}\n\nport.once(\"message\", async (msg: WorkerInputMessage) => {\n try {\n const result = await runOne(msg);\n const out: WorkerOutputMessage = { ok: true, result };\n port.postMessage(out);\n } catch (err) {\n const out: WorkerOutputMessage = {\n ok: false,\n error:\n sanitizeStack((err as Error).stack) ||\n (err as Error).message ||\n String(err),\n };\n port.postMessage(out);\n }\n});\n","/**\n * Outbound fetch wrapper installed in the worker BEFORE the user's\n * bundle imports anything. The validator already blocks `fetch` as a\n * free identifier AND `globalThis.fetch` property access, so the\n * user's snippet can't call fetch directly. But `@directive-run/query`\n * and `@directive-run/ai` are in the allowlist and their package code\n * calls global `fetch` internally — the validator never sees those\n * calls because they live in external module bodies.\n *\n * Threat closed: a snippet that builds a `createBaseQuery({\n * baseUrl: \"http://169.254.169.254\" })` triggers an outbound HTTP\n * request from the worker to AWS IMDS. On the directive.run/api/sandbox\n * Vercel surface, that's SSRF from a privileged egress.\n *\n * This wrapper rejects:\n *\n * - Loopback (127.0.0.0/8, [::1], localhost)\n * - Link-local (169.254.0.0/16 — includes IMDS at .169.254)\n * - RFC-1918 private (10.x, 172.16-31.x, 192.168.x)\n * - Multicast / broadcast / reserved ranges\n * - Non-HTTP(S) protocols (file://, ftp://, gopher://, data://, blob://)\n * - URLs without a hostname (relative paths, javascript:, etc.)\n *\n * Allows:\n *\n * - Any public IPv4 / IPv6 address\n * - Any hostname that resolves to public addresses only. `checkResolvedAddresses`\n * pre-resolves via `dns.lookup({ all: true })` and rejects when ANY\n * returned address is in a private range — this NARROWS the DNS-\n * rebinding window from the bare hostname check, but does NOT close\n * it. Node's fetch performs an INDEPENDENT DNS resolution at socket\n * connect time; an attacker controlling DNS with a 0-TTL record can\n * still rotate the address between our pre-check and the socket\n * connect. Fully closing the TOCTOU requires an `undici.Agent` with\n * a custom `connect.lookup` that pins the IP we vetted — tracked as\n * a follow-on (lz-bomb + DNS-pin land together).\n */\n\nconst ALLOWED_PROTOCOLS = new Set([\"http:\", \"https:\"]);\n\nfunction isPrivateIPv4(host: string): boolean {\n // Match IPv4 dotted-quad. We don't accept octal/hex forms — Node's\n // URL parser rejects those for hostnames anyway.\n const m = host.match(/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/);\n if (!m) return false;\n const a = Number.parseInt(m[1]!, 10);\n const b = Number.parseInt(m[2]!, 10);\n const c = Number.parseInt(m[3]!, 10);\n const d = Number.parseInt(m[4]!, 10);\n if (a > 255 || b > 255 || c > 255 || d > 255) {\n // Malformed — let it through; Node's fetch will reject the URL.\n return false;\n }\n // 0.0.0.0/8 — \"this network\", treated as unrouted/private here.\n if (a === 0) return true;\n // 127.0.0.0/8 — loopback.\n if (a === 127) return true;\n // 10.0.0.0/8 — RFC-1918 Class A.\n if (a === 10) return true;\n // 172.16.0.0/12 — RFC-1918 Class B.\n if (a === 172 && b >= 16 && b <= 31) return true;\n // 192.168.0.0/16 — RFC-1918 Class C.\n if (a === 192 && b === 168) return true;\n // 169.254.0.0/16 — link-local, includes AWS/GCP/Azure IMDS at\n // 169.254.169.254. THIS is the SSRF target we care most about.\n if (a === 169 && b === 254) return true;\n // 100.64.0.0/10 — carrier-grade NAT, not internet-routable.\n if (a === 100 && b >= 64 && b <= 127) return true;\n // 224.0.0.0/4 — multicast.\n if (a >= 224 && a <= 239) return true;\n // 240.0.0.0/4 — reserved / experimental.\n if (a >= 240) return true;\n return false;\n}\n\nfunction isPrivateIPv6(host: string): boolean {\n // Strip brackets the URL parser leaves in place.\n const stripped = host.replace(/^\\[|\\]$/g, \"\").toLowerCase();\n // Loopback.\n if (stripped === \"::1\") return true;\n // Unspecified / \"any\".\n if (stripped === \"::\") return true;\n // Link-local fe80::/10\n if (stripped.startsWith(\"fe80:\") || stripped.startsWith(\"fe8\")) return true;\n // Unique local fc00::/7 (fc and fd prefixes).\n if (stripped.startsWith(\"fc\") || stripped.startsWith(\"fd\")) return true;\n // IPv4-mapped IPv6 (::ffff:169.254.169.254) — extract the v4 tail\n // and re-check. Node's URL parser may normalize the literal-IPv4\n // form to its hex equivalent (`::ffff:a9fe:a9fe`), so handle both.\n const mappedV4 = stripped.match(/::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/);\n if (mappedV4 && isPrivateIPv4(mappedV4[1]!)) {\n return true;\n }\n const mappedHex = stripped.match(/::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);\n if (mappedHex) {\n const hi = Number.parseInt(mappedHex[1]!, 16);\n const lo = Number.parseInt(mappedHex[2]!, 16);\n if (Number.isFinite(hi) && Number.isFinite(lo)) {\n const a = (hi >> 8) & 0xff;\n const b = hi & 0xff;\n const c = (lo >> 8) & 0xff;\n const d = lo & 0xff;\n if (isPrivateIPv4(`${a}.${b}.${c}.${d}`)) {\n return true;\n }\n }\n }\n return false;\n}\n\nfunction isLocalHostname(host: string): boolean {\n const lower = host.toLowerCase();\n if (lower === \"localhost\") return true;\n if (lower === \"localhost.localdomain\") return true;\n if (lower.endsWith(\".local\")) return true;\n if (lower.endsWith(\".internal\")) return true;\n return false;\n}\n\ninterface UrlCheck {\n allow: boolean;\n reason?: string;\n}\n\nexport function checkSandboxFetchUrl(rawUrl: string): UrlCheck {\n let parsed: URL;\n try {\n parsed = new URL(rawUrl);\n } catch {\n return { allow: false, reason: `not a valid URL: \"${rawUrl}\"` };\n }\n if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) {\n return {\n allow: false,\n reason: `protocol \"${parsed.protocol}\" is not allowed (only http:, https:)`,\n };\n }\n const host = parsed.hostname;\n if (!host) {\n return { allow: false, reason: \"URL has no hostname\" };\n }\n if (isLocalHostname(host)) {\n return {\n allow: false,\n reason: `hostname \"${host}\" resolves to localhost or internal-only`,\n };\n }\n if (isPrivateIPv4(host)) {\n return {\n allow: false,\n reason: `IPv4 ${host} is in a private / loopback / link-local range`,\n };\n }\n if (isPrivateIPv6(host)) {\n return {\n allow: false,\n reason: `IPv6 ${host} is in a private / loopback / link-local range`,\n };\n }\n return { allow: true };\n}\n\n/**\n * Pre-resolve a hostname via `dns.lookup` and reject if ANY resolved\n * address lands in a private / loopback / link-local range. Closes\n * the DNS-rebinding gap the original wrapper acknowledged: a snippet\n * that fetches `http://attacker.example.com/` whose A record resolves\n * to 169.254.169.254 (or whose record rotates between checks) would\n * otherwise reach the IMDS surface. We resolve once and check the\n * actual IPs, not just the hostname spelling.\n *\n * Returns `null` if every resolved address is public; returns a\n * reason string for the first private hit.\n */\nexport async function checkResolvedAddresses(host: string): Promise<string | null> {\n // Skip DNS resolution for hostnames that are already IP literals —\n // the hostname-level check already covered them.\n if (/^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(host)) return null;\n if (host.includes(\":\")) return null; // IPv6 literal (URL parser leaves it bracketed but hostname strips them)\n try {\n const { lookup } = await import(\"node:dns/promises\");\n const records = await lookup(host, { all: true });\n for (const r of records) {\n if (r.family === 4 && isPrivateIPv4(r.address)) {\n return `hostname \"${host}\" resolves to private IPv4 ${r.address}`;\n }\n if (r.family === 6 && isPrivateIPv6(r.address)) {\n return `hostname \"${host}\" resolves to private IPv6 ${r.address}`;\n }\n }\n return null;\n } catch {\n // DNS failure: fall through and let the underlying fetch report\n // the resolution error to the caller. The wrapper isn't the place\n // to swallow DNS noise — we only block when we KNOW the address\n // is private.\n return null;\n }\n}\n\n/**\n * Install the wrapper on `globalThis.fetch`. Called from the worker\n * BEFORE the bundle is imported so any `fetch` calls inside the user's\n * code or inside allowlisted packages (`@directive-run/query`,\n * `@directive-run/ai`) hit the wrapper.\n *\n * Safe to call when `fetch` is undefined (older Node); just installs\n * the rejection wrapper.\n */\nexport function installFetchWrapper(): void {\n const original =\n (globalThis as { fetch?: typeof fetch }).fetch ??\n ((async () => {\n throw new Error(\"fetch is not available in this Node version\");\n }) as typeof fetch);\n\n const wrapped: typeof fetch = async (\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> => {\n const rawUrl =\n typeof input === \"string\"\n ? input\n : input instanceof URL\n ? input.toString()\n : (input as Request).url;\n const decision = checkSandboxFetchUrl(rawUrl);\n if (!decision.allow) {\n throw new Error(\n `sandbox: outbound fetch blocked — ${decision.reason ?? \"denied\"}`,\n );\n }\n // DNS rebinding defence — pre-resolve and reject before the\n // outbound socket opens. The original wrapper acknowledged this\n // gap in its preamble; closing it now that node:dns/promises is\n // a stable surface.\n let parsedHost = \"\";\n try {\n parsedHost = new URL(rawUrl).hostname.replace(/^\\[|\\]$/g, \"\");\n } catch {\n // already rejected above; defensive\n }\n if (parsedHost) {\n const dnsReason = await checkResolvedAddresses(parsedHost);\n if (dnsReason !== null) {\n throw new Error(`sandbox: outbound fetch blocked — ${dnsReason}`);\n }\n }\n return original(input, init);\n };\n\n (globalThis as { fetch: typeof fetch }).fetch = wrapped;\n}\n","/**\n * Strip absolute paths from a stack trace before it crosses the\n * worker→host→client boundary. Stack traces leak the sandbox host's\n * filesystem layout — `/Users/<name>/`, `/home/<user>/`, the mkdtemp\n * bundle directory, and on serverless surfaces `/var/task/...`. These\n * become a fingerprint of the host environment any sandbox client can\n * read. Sanitize once at the boundary so every error path benefits.\n *\n * Lives in its own module because `worker.ts` throws at top-level when\n * loaded outside a worker_threads context — extracting `sanitizeStack`\n * lets the unit test suite import the function directly.\n *\n * Terminator characters in the patterns below intentionally include\n * matched-delimiter punctuation (`)`, `]`, `>`, `\"`, `'`, `,`) so a\n * path inside an `at fn (…)` frame or a JSON-stringified `\"path\"`\n * field is bounded correctly. The character class is shared across\n * POSIX + Windows patterns to keep the boundary consistent.\n */\nconst PATH_TERMINATOR = \"[^\\\\s)\\\\]>\\\"',]\";\n\nexport function sanitizeStack(s: string | undefined): string {\n if (!s) return \"\";\n const TAIL = `${PATH_TERMINATOR}+`;\n return (\n s\n // file:// URLs\n .replace(new RegExp(`file:\\\\/\\\\/\\\\/${PATH_TERMINATOR}+`, \"g\"), \"<sandbox>\")\n // UNC paths — must run BEFORE the generic `\\\\` Windows pattern so\n // `\\\\server\\share\\file` becomes `<unc>`, not `<unc>\\share\\file`.\n .replace(new RegExp(`\\\\\\\\\\\\\\\\${TAIL}`, \"g\"), \"<unc>\")\n // Windows drive paths — `C:\\Users\\<user>\\...` and friends.\n // Captures any drive letter + `\\Users\\<name>` or `\\Documents and Settings\\<name>`.\n .replace(/[A-Za-z]:\\\\Users\\\\[^\\\\\\s)\\]>\"',]+/g, \"<home>\")\n .replace(\n /[A-Za-z]:\\\\Documents and Settings\\\\[^\\\\\\s)\\]>\"',]+/g,\n \"<home>\",\n )\n // Generic Windows absolute paths — `C:\\<anything-not-Users>` that\n // didn't match above. Catches CI runners (`D:\\a\\_work\\…`) and\n // tempdirs.\n .replace(/[A-Za-z]:\\\\[^\\\\\\s)\\]>\"',]+(?:\\\\[^\\\\\\s)\\]>\"',]+)+/g, \"<path>\")\n // POSIX dev-machine home directories — macOS + Linux\n .replace(new RegExp(`\\\\/Users\\\\/${TAIL}`, \"g\"), \"/<home>\")\n .replace(new RegExp(`\\\\/home\\\\/${TAIL}`, \"g\"), \"/<home>\")\n // macOS private-var prefix\n .replace(new RegExp(`\\\\/private\\\\/var\\\\/${TAIL}`, \"g\"), \"<tmp>\")\n // serverless lambda / vercel function task root\n .replace(new RegExp(`\\\\/var\\\\/task\\\\/${TAIL}`, \"g\"), \"<task>\")\n // generic tmp + lambda-runtime task paths\n .replace(new RegExp(`\\\\/tmp\\\\/${TAIL}`, \"g\"), \"<tmp>\")\n // /opt and /usr/local — common Linux distro prefixes that leak\n // package layout (e.g. `/opt/render/project/src/...`).\n .replace(new RegExp(`\\\\/opt\\\\/${TAIL}`, \"g\"), \"<opt>\")\n .replace(new RegExp(`\\\\/usr\\\\/local\\\\/${TAIL}`, \"g\"), \"<usr-local>\")\n // Common deploy roots — Heroku/Render/Docker/Codespaces/GitHub\n // Actions all root applications under one of these. Without\n // these, a worker error from a containerized host leaks the\n // deploy layout + package versions inside `/app/node_modules/`.\n .replace(new RegExp(`\\\\/app\\\\/${TAIL}`, \"g\"), \"<app>\")\n .replace(new RegExp(`\\\\/srv\\\\/${TAIL}`, \"g\"), \"<srv>\")\n .replace(new RegExp(`\\\\/workspace\\\\/${TAIL}`, \"g\"), \"<workspace>\")\n .replace(new RegExp(`\\\\/data\\\\/${TAIL}`, \"g\"), \"<data>\")\n // /etc and /root — high-sensitivity Linux paths (config files,\n // root home). A snippet that throws referencing one of these\n // should not echo the absolute path back to the caller.\n .replace(new RegExp(`\\\\/etc\\\\/${TAIL}`, \"g\"), \"<etc>\")\n .replace(new RegExp(`\\\\/root\\\\/${TAIL}`, \"g\"), \"<root>\")\n );\n}\n"],"mappings":"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","names":["performance","parentPort","ALLOWED_PROTOCOLS","isPrivateIPv4","host","m","a","b","c","d","isPrivateIPv6","stripped","mappedV4","mappedHex","hi","lo","isLocalHostname","lower","checkSandboxFetchUrl","rawUrl","parsed","checkResolvedAddresses","lookup","records","r","installFetchWrapper","original","wrapped","input","init","decision","parsedHost","dnsReason","PATH_TERMINATOR","sanitizeStack","s","TAIL","parentPort","installFetchWrapper","port","serializeArg","arg","obj","captureConsole","buffer","prefix","originals","format","label","args","text","a","runOne","message","logs","errors","restoreConsole","startMs","performance","err","e","parts","sanitizeStack","cause","durationMs","facts","derived","system","key","value","msg","out"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directive-run/sandbox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Execute Directive snippets server-side and return a structured transcript (logs + facts + errors). Consumed by @directive-run/mcp's run_in_sandbox tool and directive.run/playground's live DevTools panel. Uses worker_threads + esbuild bundling + an AST allowlist validator so user-supplied TypeScript runs with a bounded surface (allowlisted imports, allowlisted API calls, 5s wall clock, 32 MB heap).",
|
|
5
5
|
"license": "(MIT OR Apache-2.0)",
|
|
6
6
|
"author": "Jason Comes",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"dist"
|
|
48
48
|
],
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@directive-run/core": "1.
|
|
50
|
+
"@directive-run/core": "1.21.0"
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
53
|
"esbuild": "^0.24.0",
|