@directive-run/sandbox 0.1.0 → 0.2.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 ADDED
@@ -0,0 +1,58 @@
1
+ # @directive-run/sandbox
2
+
3
+ Execute Directive snippets server-side and return a structured transcript (logs, facts, errors). Used by [`@directive-run/mcp`](../mcp)'s `run_in_sandbox` tool and by `directive.run/playground`'s live DevTools panel.
4
+
5
+ ## What it does
6
+
7
+ Takes a TypeScript snippet (single source string OR a paired library + runner `files: [{path, source}]` array — same shape `playground_link` accepts), validates it against an AST allowlist, bundles the multi-file payload via esbuild, and executes the result in a bounded `worker_threads` sandbox. Returns:
8
+
9
+ ```ts
10
+ interface SandboxResult {
11
+ logs: string[]; // captured console.log/warn/error lines
12
+ facts: Record<string, unknown>; // system.facts.$store.toObject() snapshot
13
+ errors: string[]; // structured error messages
14
+ durationMs: number;
15
+ timedOut: boolean;
16
+ }
17
+ ```
18
+
19
+ The bundler injects an early-capture immediately after `createSystem(...)`, so the post-mortem snapshot survives mid-runner errors — a validation throw inside `await system.settle()` still hands you the init-state facts.
20
+
21
+ ## Sandbox boundary
22
+
23
+ Three layers:
24
+
25
+ 1. **AST allowlist validator** (`ts-morph`). Imports are restricted to a curated `@directive-run/*` set + relative `./*.js`:
26
+ - **Allowed:** `core`, `ai`, `query`, `react`, `vue`, `svelte`, `solid`, `lit`, `el`, `optimistic`, `timeline`, `mutator`, `knowledge`, `scaffold`, `claude-plugin`, `lint` (16 packages — anything an end-user demo realistically composes from).
27
+ - **Denied:** `cli`, `mcp`, `sandbox`, `vite-plugin-api-proxy` (build / CLI / sandbox-meta tooling — no legitimate use inside a sandboxed demo).
28
+ - Everything else (`node:fs`, `express`, `@sizls/*`, etc.) is rejected.
29
+ - **Identifier references** to FS / network / eval surfaces (`process`, `require`, `fetch`, `fs`, `child_process`, `eval`, `new Function`, `setTimeout`, `Buffer`, etc.) are rejected as free identifiers.
30
+ - **Property-access bypass chains** are rejected (v0.3.0): `globalThis.process`, `globalThis.fetch`, `globalThis["X"]` bracket access with string literal, `.constructor` access on any value, `Function(...)` call without `new`, `Reflect.get/has/getOwnPropertyDescriptor(globalThis, "X")` smuggle chains. The AE security audit in `docs/AE-AUDIT-SANDBOX.md` traces these PoCs and how v0.3.0 closes them.
31
+ 2. **esbuild bundler**. Virtualizes the multi-file payload into a single ESM string with `@directive-run/*` externalized. Throws on imports that can't be resolved against the in-memory file map.
32
+ 3. **`worker_threads.Worker`** with `resourceLimits` (32 MB heap, 16 MB code) and a clamped wall-clock budget (`[100ms, 10s]`, default 5s). The worker is hard-terminated on timeout — no cooperative cancellation needed.
33
+
34
+ Resource limits are heap-only — workers share the parent process's FS / network access. The allowlist validator is what actually prevents sandbox escape; the worker layer adds OOM / runaway-CPU bounding on top.
35
+
36
+ ## API
37
+
38
+ ```ts
39
+ import { runInSandbox } from "@directive-run/sandbox";
40
+
41
+ const result = await runInSandbox({
42
+ files: [
43
+ { path: "src/counter.ts", source: moduleSource },
44
+ { path: "src/main.ts", source: runnerSource },
45
+ ],
46
+ timeoutMs: 5000,
47
+ });
48
+
49
+ console.log(result.logs); // ["[start] count= 0", "[settled] count= 2"]
50
+ console.log(result.facts); // { count: 2 }
51
+ ```
52
+
53
+ The single-source shortcut `runInSandbox({ source: "..." })` maps onto `src/main.ts` internally — convenient for already-runnable snippets from `get_example` / `fix_code`.
54
+
55
+ ## See also
56
+
57
+ - [`@directive-run/mcp`](../mcp) — wraps `runInSandbox` as the `run_in_sandbox` MCP tool, returns the transcript to the AI client alongside a `playgroundUrl`.
58
+ - [`@directive-run/scaffold`](../scaffold) — `generateRunner` produces the canonical runner shape this sandbox expects to execute (`createSystem` → `start()` → dispatch → `settle()` → log).
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
- "use strict";var D=Object.create;var x=Object.defineProperty;var j=Object.getOwnPropertyDescriptor;var L=Object.getOwnPropertyNames;var W=Object.getPrototypeOf,B=Object.prototype.hasOwnProperty;var C=(e,r)=>{for(var o in r)x(e,o,{get:r[o],enumerable:!0})},I=(e,r,o,t)=>{if(r&&typeof r=="object"||typeof r=="function")for(let n of L(r))!B.call(e,n)&&n!==o&&x(e,n,{get:()=>r[n],enumerable:!(t=j(r,n))||t.enumerable});return e};var V=(e,r,o)=>(o=e!=null?D(W(e)):{},I(r||!e||!e.__esModule?x(o,"default",{value:e,enumerable:!0}):o,e)),q=e=>I(x({},"__esModule",{value:!0}),e);var se={};C(se,{SandboxError:()=>d,runInSandbox:()=>oe});module.exports=q(se);var P=require("esbuild"),S="src/main.ts",g="directive-sandbox-vfs",p=class extends Error{constructor(o,t){super(o);this.cause=t;this.name="BundleError"}};function K(e){let r=/(var\s+(system\d*)\s*=\s*createSystem\s*\([\s\S]*?\)\s*;)/,o=e.match(r);if(!o)return e;let t=o[1],s=`
1
+ "use strict";var B=Object.create;var E=Object.defineProperty;var K=Object.getOwnPropertyDescriptor;var V=Object.getOwnPropertyNames;var q=Object.getPrototypeOf,G=Object.prototype.hasOwnProperty;var U=(e,t)=>{for(var o in t)E(e,o,{get:t[o],enumerable:!0})},M=(e,t,o,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of V(t))!G.call(e,n)&&n!==o&&E(e,n,{get:()=>t[n],enumerable:!(r=K(t,n))||r.enumerable});return e};var H=(e,t,o)=>(o=e!=null?B(q(e)):{},M(t||!e||!e.__esModule?E(o,"default",{value:e,enumerable:!0}):o,e)),X=e=>M(E({},"__esModule",{value:!0}),e);var le={};U(le,{SandboxError:()=>p,runInSandbox:()=>ue});module.exports=X(le);var T=require("esbuild"),v="src/main.ts",y="directive-sandbox-vfs",h=class extends Error{constructor(o,r){super(o);this.cause=r;this.name="BundleError"}};function z(e){let t=/(var\s+(system\d*)\s*=\s*createSystem\s*\([\s\S]*?\)\s*;)/,o=e.match(t);if(!o)return e;let r=o[1],a=`
2
2
  (globalThis).__directiveSandbox_system__ = ${o[2]};
3
- `;return e.replace(t,t+s)}async function E(e){if(!e.find(t=>t.path===S))throw new p(`payload must include "${S}" \u2014 that's the entry point the runner targets`);let o=new Map;for(let t of e)o.set(t.path,t.source);try{let n=(await(0,P.build)({entryPoints:[`${g}:${S}`],bundle:!0,format:"esm",target:"node20",platform:"node",write:!1,logLevel:"silent",external:["@directive-run/core","@directive-run/ai","@directive-run/query"],supported:{"top-level-await":!0},plugins:[{name:"directive-sandbox-vfs",setup(u){u.onResolve({filter:/.*/},i=>{if(i.kind==="entry-point")return{path:i.path.replace(`${g}:`,""),namespace:g};if(i.namespace!==g)return null;if(i.path.startsWith("@directive-run/"))return{external:!0,path:i.path};if(i.path.startsWith("./")||i.path.startsWith("../")){let a=i.importer.replace(/\/[^/]+$/,""),c=i.path.replace(/^\.\//,"").replace(/\.js$/,""),b=[`${a}/${c}.ts`,`${a}/${c}.js`,`${a}/${c}`,`${c}.ts`,`${c}.js`].map(m=>m.replace(/\/+/g,"/"));for(let m of b)if(o.has(m))return{path:m,namespace:g};throw new p(`cannot resolve "${i.path}" from "${i.importer}" \u2014 tried ${b.join(", ")}`)}throw new p(`unexpected import "${i.path}" from "${i.importer}" \u2014 only relative or @directive-run/* allowed`)}),u.onLoad({filter:/.*/,namespace:g},i=>{let a=o.get(i.path);if(a===void 0)throw new p(`virtual file missing: ${i.path}`);if(i.path===S){let c=["","// directive-sandbox: lift the runner's `system` binding","// onto a side-channel global so the worker can read","// system.facts.$store.toObject() after execution.","if (typeof system !== 'undefined') {"," (globalThis).__directiveSandbox_system__ = system;","}"].join(`
4
- `);return{contents:a+c,loader:"ts"}}return{contents:a,loader:"ts"}})}}]})).outputFiles[0]?.text;if(!n)throw new p("esbuild produced no output");let s=K(n);return{source:s,bytes:Buffer.byteLength(s,"utf8")}}catch(t){throw t instanceof p?t:new p(`bundle failed: ${t.message??String(t)}`,t)}}var w=require("fs"),y=require("path"),$=require("url"),T=require("worker_threads"),Y={},U=100,G=1e4,k=5e3;async function N(){let{createRequire:e}=await import("module");return e(Y.url).resolve("@directive-run/sandbox/worker")}var h=class extends Error{constructor(o,t){super(o);this.code=t;this.name="WorkerExecError"}};function H(e){let r=e??k;return Number.isFinite(r)?Math.min(G,Math.max(U,Math.floor(r))):k}async function X(){let e=await N();return(0,y.dirname)((0,y.dirname)(e))}async function z(e){let r=await X(),o=(0,w.mkdtempSync)((0,y.join)(r,".sandbox-tmp-")),t=(0,y.join)(o,"bundle.mjs");return(0,w.writeFileSync)(t,e,"utf8"),{bundlePath:t,cleanup:()=>{try{(0,w.rmSync)(o,{recursive:!0,force:!0})}catch{}}}}async function _(e){let r=H(e.timeoutMs),o=await N(),{bundlePath:t,cleanup:n}=await z(e.bundledSource),s=new T.Worker(o,{resourceLimits:{maxOldGenerationSizeMb:32,maxYoungGenerationSizeMb:8,codeRangeSizeMb:16},stderr:!1}),u=null,i=!1,a=Date.now();try{return await new Promise((b,m)=>{let v=!1;s.once("message",f=>{v=!0,f.ok?b(f.result):m(new h(f.error,"worker-error"))}),s.once("error",f=>{v=!0,m(new h(f.message,"worker-error"))}),s.once("exit",f=>{!v&&f!==0&&f!==null&&m(new h(`worker exited with code ${f} before responding`,"worker-error"))}),u=setTimeout(()=>{i=!0,s.terminate(),m(new h(`wall-clock budget of ${r}ms elapsed`,"timeout"))},r);let O={bundlePath:(0,$.pathToFileURL)(t).href,timeoutMs:r};s.postMessage(O)})}catch(c){if(c instanceof h&&c.code==="timeout")return{logs:[],facts:{},errors:[c.message],durationMs:Date.now()-a,timedOut:!0};throw c}finally{u&&clearTimeout(u),await s.terminate().catch(()=>{}),n()}}var d=class extends Error{constructor(o,t){super(o);this.code=t;this.name="SandboxError"}};var l=require("ts-morph"),J=[/^@directive-run\/(core|ai|query)(\/.+)?$/,/^\.\/.+\.js$/,/^\.\.\/.+\.js$/],Q=new Set(["createSystem","system","console","Math","JSON","Object","Array","Number","String","Boolean","Symbol","Promise","Error","Date","Map","Set","WeakMap","WeakSet","Reflect","globalThis","undefined","null","NaN","Infinity"]),Z=new Set(["process","require","module","__dirname","__filename","fetch","XMLHttpRequest","WebSocket","eval","Function","Buffer","setImmediate","queueMicrotask","setTimeout","setInterval","clearTimeout","clearInterval"]);function ee(e,r,o){let t=r.getSourceFileOrThrow(e);for(let n of t.getImportDeclarations()){let s=n.getModuleSpecifierValue();if(!J.some(i=>i.test(s))){let{line:i,column:a}=t.getLineAndColumnAtPos(n.getStart());o.push({path:e,line:i,column:a,message:`import "${s}" is not allowed in the sandbox. Allowed: @directive-run/{core,ai,query} or relative "./X.js" paths.`})}}}function te(e,r,o){let t=r.getSourceFileOrThrow(e);t.forEachDescendant(n=>{if(n.getKind()===l.SyntaxKind.ImportKeyword){let s=n.getParent();if(s&&s.getKind()===l.SyntaxKind.CallExpression){let{line:u,column:i}=t.getLineAndColumnAtPos(n.getStart());o.push({path:e,line:u,column:i,message:"dynamic import() is not allowed in the sandbox"})}}if(n.getKind()===l.SyntaxKind.NewExpression){let s=n.getText();if(/^new\s+Function\s*\(/.test(s)){let{line:u,column:i}=t.getLineAndColumnAtPos(n.getStart());o.push({path:e,line:u,column:i,message:"new Function(...) is not allowed in the sandbox"})}}})}function re(e,r,o){let t=r.getSourceFileOrThrow(e),n=new Set;for(let s of t.getImportDeclarations()){for(let a of s.getNamedImports())n.add(a.getName());let u=s.getDefaultImport();u&&n.add(u.getText());let i=s.getNamespaceImport();i&&n.add(i.getText())}for(let s of t.getVariableDeclarations())n.add(s.getName());for(let s of t.getFunctions()){let u=s.getName();u&&n.add(u)}for(let s of t.getClasses()){let u=s.getName();u&&n.add(u)}t.forEachDescendant(s=>{if(s.getKind()!==l.SyntaxKind.Identifier)return;let u=s.getText();if(n.has(u)||Q.has(u))return;let i=s.getParent();if(i){let a=i.getKind();if(a===l.SyntaxKind.PropertyAssignment&&i.getNameNode?.()===s||a===l.SyntaxKind.PropertyAccessExpression&&i.getNameNode?.()===s||a===l.SyntaxKind.MethodDeclaration||a===l.SyntaxKind.ImportSpecifier||a===l.SyntaxKind.ExportSpecifier||a===l.SyntaxKind.NamespaceImport||a===l.SyntaxKind.ImportClause||a===l.SyntaxKind.Parameter||a===l.SyntaxKind.TypeReference||a===l.SyntaxKind.TypeQuery)return}if(Z.has(u)){let{line:a,column:c}=t.getLineAndColumnAtPos(s.getStart());o.push({path:e,line:a,column:c,message:`identifier "${u}" is denied in the sandbox (FS/network/eval surface)`})}})}function F(e){let r=[],o=new l.Project({useInMemoryFileSystem:!0,compilerOptions:{target:99,module:99,allowJs:!0,strict:!1}});for(let t of e)o.createSourceFile(t.path,t.source,{overwrite:!0});for(let t of e)ee(t.path,o,r),te(t.path,o,r),re(t.path,o,r);return r}var A=2e5,R=10,M="src/main.ts";function ne(e){if(e.files&&e.source)throw new d("pass either `source` or `files`, not both","input-invalid");if(!e.files&&!e.source)throw new d("must pass either `source` or `files`","input-invalid");let r=e.files?e.files:[{path:M,source:e.source??""}];if(r.length===0)throw new d("files array is empty","input-invalid");if(r.length>R)throw new d(`payload exceeds ${R} files`,"input-invalid");let o=0;for(let t of r){if(typeof t.path!="string"||t.path.length===0)throw new d("every file must have a non-empty path","input-invalid");if(typeof t.source!="string"||t.source.length===0)throw new d(`file "${t.path}" has empty source`,"input-invalid");o+=Buffer.byteLength(t.source,"utf8")}if(o>A)throw new d(`total payload is ${o} bytes (max ${A})`,"input-invalid");if(!r.some(t=>t.path===M))throw new d(`payload must include "${M}" \u2014 the runner entry point`,"input-invalid");return r}async function oe(e){let r;try{r=ne(e)}catch(n){if(n instanceof d)return{logs:[],facts:{},errors:[n.message],durationMs:0,timedOut:!1};throw n}let o=F(r);if(o.length>0)return{logs:[],facts:{},errors:o.map(n=>`${n.path}:${n.line}:${n.column} \u2014 ${n.message}`),durationMs:0,timedOut:!1};let t;try{t=await E(r)}catch(n){if(n instanceof p)return{logs:[],facts:{},errors:[n.message],durationMs:0,timedOut:!1};throw n}try{return await _({bundledSource:t.source,timeoutMs:e.timeoutMs})}catch(n){if(n instanceof h)return{logs:[],facts:{},errors:[n.message],durationMs:0,timedOut:n.code==="timeout"};throw n}}0&&(module.exports={SandboxError,runInSandbox});
3
+ `;return e.replace(r,r+a)}async function k(e){if(!e.find(r=>r.path===v))throw new h(`payload must include "${v}" \u2014 that's the entry point the runner targets`);let o=new Map;for(let r of e)o.set(r.path,r.source);try{let n=(await(0,T.build)({entryPoints:[`${y}:${v}`],bundle:!0,format:"esm",target:"node20",platform:"node",write:!1,logLevel:"silent",external:["@directive-run/*"],supported:{"top-level-await":!0},plugins:[{name:"directive-sandbox-vfs",setup(i){i.onResolve({filter:/.*/},s=>{if(s.kind==="entry-point")return{path:s.path.replace(`${y}:`,""),namespace:y};if(s.namespace!==y)return null;if(s.path.startsWith("@directive-run/"))return{external:!0,path:s.path};if(s.path.startsWith("./")||s.path.startsWith("../")){let c=s.importer.replace(/\/[^/]+$/,""),u=s.path.replace(/^\.\//,"").replace(/\.js$/,""),g=[`${c}/${u}.ts`,`${c}/${u}.js`,`${c}/${u}`,`${u}.ts`,`${u}.js`].map(f=>f.replace(/\/+/g,"/"));for(let f of g)if(o.has(f))return{path:f,namespace:y};throw new h(`cannot resolve "${s.path}" from "${s.importer}" \u2014 tried ${g.join(", ")}`)}throw new h(`unexpected import "${s.path}" from "${s.importer}" \u2014 only relative or @directive-run/* allowed`)}),i.onLoad({filter:/.*/,namespace:y},s=>{let c=o.get(s.path);if(c===void 0)throw new h(`virtual file missing: ${s.path}`);if(s.path===v){let u=["","// directive-sandbox: lift the runner's `system` binding","// onto a side-channel global so the worker can read","// system.facts.$store.toObject() after execution.","if (typeof system !== 'undefined') {"," (globalThis).__directiveSandbox_system__ = system;","}"].join(`
4
+ `);return{contents:c+u,loader:"ts"}}return{contents:c,loader:"ts"}})}}]})).outputFiles[0]?.text;if(!n)throw new h("esbuild produced no output");let a=z(n);return{source:a,bytes:Buffer.byteLength(a,"utf8")}}catch(r){throw r instanceof h?r:new h(`bundle failed: ${r.message??String(r)}`,r)}}var b=require("fs"),w=require("path"),$=require("url"),F=require("worker_threads"),te={},Y=100,J=1e4,A=5e3;async function N(){let{createRequire:e}=await import("module");return e(te.url).resolve("@directive-run/sandbox/worker")}var x=class extends Error{constructor(o,r){super(o);this.code=r;this.name="WorkerExecError"}};function Q(e){let t=e??A;return Number.isFinite(t)?Math.min(J,Math.max(Y,Math.floor(t))):A}async function Z(){let e=await N();return(0,w.dirname)((0,w.dirname)(e))}async function ee(e){let t=await Z(),o=(0,b.mkdtempSync)((0,w.join)(t,".sandbox-tmp-")),r=(0,w.join)(o,"bundle.mjs");return(0,b.writeFileSync)(r,e,"utf8"),{bundlePath:r,cleanup:()=>{try{(0,b.rmSync)(o,{recursive:!0,force:!0})}catch{}}}}async function _(e){let t=Q(e.timeoutMs),o=await N(),{bundlePath:r,cleanup:n}=await ee(e.bundledSource),a=new F.Worker(o,{resourceLimits:{maxOldGenerationSizeMb:32,maxYoungGenerationSizeMb:8,codeRangeSizeMb:16},stderr:!1}),i=null,s=!1,c=Date.now();try{return await new Promise((g,f)=>{let m=!1;a.once("message",d=>{m=!0,d.ok?g(d.result):f(new x(d.error,"worker-error"))}),a.once("error",d=>{m=!0,f(new x(d.message,"worker-error"))}),a.once("exit",d=>{!m&&d!==0&&d!==null&&f(new x(`worker exited with code ${d} before responding`,"worker-error"))}),i=setTimeout(()=>{s=!0,a.terminate(),f(new x(`wall-clock budget of ${t}ms elapsed`,"timeout"))},t);let S={bundlePath:(0,$.pathToFileURL)(r).href,timeoutMs:t};a.postMessage(S)})}catch(u){if(u instanceof x&&u.code==="timeout")return{logs:[],facts:{},errors:[u.message],durationMs:Date.now()-c,timedOut:!0};throw u}finally{i&&clearTimeout(i),await a.terminate().catch(()=>{}),n()}}var p=class extends Error{constructor(o,r){super(o);this.code=r;this.name="SandboxError"}};var l=require("ts-morph"),O=new Set(["core","ai","query","el","react","vue","svelte","solid","lit","optimistic","timeline","mutator","knowledge","scaffold","claude-plugin","lint"]),R=new Set(["cli","mcp","sandbox","vite-plugin-api-proxy"]);function D(e){let t=e.match(/^@directive-run\/([^/]+)/);return t?t[1]:null}function re(e){if(/^\.{1,2}\/.+\.js$/.test(e))return!0;let t=D(e);return t===null||R.has(t)?!1:O.has(t)}var W=new Set(["createSystem","system","console","Math","JSON","Object","Array","Number","String","Boolean","Symbol","Promise","Error","Date","Map","Set","WeakMap","WeakSet","Reflect","globalThis","undefined","null","NaN","Infinity"]),P=new Set(["process","require","module","__dirname","__filename","fetch","XMLHttpRequest","WebSocket","eval","Function","Buffer","setImmediate","queueMicrotask","setTimeout","setInterval","clearTimeout","clearInterval"]);function ne(e){let t=D(e);return t&&R.has(t)?`import "${e}" is denied \u2014 @directive-run/${t} is a build/CLI/sandbox tool, not for use inside a sandboxed demo`:`import "${e}" is not allowed in the sandbox. Allowed: relative "./X.js" paths or any of @directive-run/{${Array.from(O).sort().join(",")}}.`}function oe(e,t,o){let r=t.getSourceFileOrThrow(e);for(let n of r.getImportDeclarations()){let a=n.getModuleSpecifierValue();if(!re(a)){let{line:i,column:s}=r.getLineAndColumnAtPos(n.getStart());o.push({path:e,line:i,column:s,message:ne(a)})}}}function se(e,t,o){let r=t.getSourceFileOrThrow(e);r.forEachDescendant(n=>{if(n.getKind()===l.SyntaxKind.ImportKeyword){let a=n.getParent();if(a&&a.getKind()===l.SyntaxKind.CallExpression){let{line:i,column:s}=r.getLineAndColumnAtPos(n.getStart());o.push({path:e,line:i,column:s,message:"dynamic import() is not allowed in the sandbox"})}}if(n.getKind()===l.SyntaxKind.NewExpression){let a=n.getText();if(/^new\s+Function\s*\(/.test(a)){let{line:i,column:s}=r.getLineAndColumnAtPos(n.getStart());o.push({path:e,line:i,column:s,message:"new Function(...) is not allowed in the sandbox"})}}})}function ie(e,t,o){let r=t.getSourceFileOrThrow(e),n=(i,s)=>{let{line:c,column:u}=r.getLineAndColumnAtPos(i.getStart());o.push({path:e,line:c,column:u,message:s})},a=i=>{let s=i.trim();return s.startsWith('"')&&s.endsWith('"')||s.startsWith("'")&&s.endsWith("'")||s.startsWith("`")&&s.endsWith("`")?s.slice(1,-1):null};r.forEachDescendant(i=>{let s=i.getKind();if(s===l.SyntaxKind.PropertyAccessExpression){let c=i.getName?.();if(!c)return;if(c==="constructor"){n(i,"`.constructor` access is denied in the sandbox (Function-constructor smuggle vector)");return}if(P.has(c)){n(i,`\`.${c}\` access is denied in the sandbox (FS/network/eval surface) \u2014 accessing a denied global via property syntax was the property-access bypass closed in v0.3.0`);return}}if(s===l.SyntaxKind.ElementAccessExpression){let c=i.getExpression?.(),u=i.getArgumentExpression?.(),g=c?.getText()??"",f=u?.getText()??"",m=a(f);if(g==="globalThis"&&m!==null){n(i,"bracket-access on `globalThis` with a string literal is denied in the sandbox (use direct identifier reference for allowlisted names)");return}if(m!==null&&(m==="constructor"||P.has(m))){n(i,`bracket-access \`["${m}"]\` is denied in the sandbox (would reach a denied name)`);return}}if(s===l.SyntaxKind.CallExpression){let u=i.getExpression?.()?.getText()??"";if(u==="Function"){n(i,"`Function(...)` call is denied in the sandbox");return}let g=u.match(/^(Reflect|Object)\.(\w+)$/);if(g){let m=i.getArguments?.()??[];if(m.length>=2){let S=m[0].getText(),d=a(m[1].getText());new Set(["get","has","getOwnPropertyDescriptor","getOwnPropertyDescriptors","ownKeys","getPrototypeOf"]).has(g[2])&&(S==="globalThis"||W.has(S))&&d!==null&&(d==="constructor"||P.has(d))&&n(i,`\`${g[1]}.${g[2]}(${S}, "${d}")\` would reach a denied name`)}}}})}function ae(e,t,o){let r=t.getSourceFileOrThrow(e),n=new Set;for(let a of r.getImportDeclarations()){for(let c of a.getNamedImports())n.add(c.getName());let i=a.getDefaultImport();i&&n.add(i.getText());let s=a.getNamespaceImport();s&&n.add(s.getText())}for(let a of r.getVariableDeclarations())n.add(a.getName());for(let a of r.getFunctions()){let i=a.getName();i&&n.add(i)}for(let a of r.getClasses()){let i=a.getName();i&&n.add(i)}r.forEachDescendant(a=>{if(a.getKind()!==l.SyntaxKind.Identifier)return;let i=a.getText();if(n.has(i)||W.has(i))return;let s=a.getParent();if(s){let c=s.getKind();if(c===l.SyntaxKind.PropertyAssignment&&s.getNameNode?.()===a||c===l.SyntaxKind.PropertyAccessExpression&&s.getNameNode?.()===a||c===l.SyntaxKind.MethodDeclaration||c===l.SyntaxKind.ImportSpecifier||c===l.SyntaxKind.ExportSpecifier||c===l.SyntaxKind.NamespaceImport||c===l.SyntaxKind.ImportClause||c===l.SyntaxKind.Parameter||c===l.SyntaxKind.TypeReference||c===l.SyntaxKind.TypeQuery)return}if(P.has(i)){let{line:c,column:u}=r.getLineAndColumnAtPos(a.getStart());o.push({path:e,line:c,column:u,message:`identifier "${i}" is denied in the sandbox (FS/network/eval surface)`})}})}function j(e){let t=[],o=new l.Project({useInMemoryFileSystem:!0,compilerOptions:{target:99,module:99,allowJs:!0,strict:!1}});for(let r of e)o.createSourceFile(r.path,r.source,{overwrite:!0});for(let r of e)oe(r.path,o,t),se(r.path,o,t),ie(r.path,o,t),ae(r.path,o,t);return t}var L=2e5,C=10,I="src/main.ts";function ce(e){if(e.files&&e.source)throw new p("pass either `source` or `files`, not both","input-invalid");if(!e.files&&!e.source)throw new p("must pass either `source` or `files`","input-invalid");let t=e.files?e.files:[{path:I,source:e.source??""}];if(t.length===0)throw new p("files array is empty","input-invalid");if(t.length>C)throw new p(`payload exceeds ${C} files`,"input-invalid");let o=0;for(let r of t){if(typeof r.path!="string"||r.path.length===0)throw new p("every file must have a non-empty path","input-invalid");if(typeof r.source!="string"||r.source.length===0)throw new p(`file "${r.path}" has empty source`,"input-invalid");o+=Buffer.byteLength(r.source,"utf8")}if(o>L)throw new p(`total payload is ${o} bytes (max ${L})`,"input-invalid");if(!t.some(r=>r.path===I))throw new p(`payload must include "${I}" \u2014 the runner entry point`,"input-invalid");return t}async function ue(e){let t;try{t=ce(e)}catch(n){if(n instanceof p)return{logs:[],facts:{},errors:[n.message],durationMs:0,timedOut:!1};throw n}let o=j(t);if(o.length>0)return{logs:[],facts:{},errors:o.map(n=>`${n.path}:${n.line}:${n.column} \u2014 ${n.message}`),durationMs:0,timedOut:!1};let r;try{r=await k(t)}catch(n){if(n instanceof h)return{logs:[],facts:{},errors:[n.message],durationMs:0,timedOut:!1};throw n}try{return await _({bundledSource:r.source,timeoutMs:e.timeoutMs})}catch(n){if(n instanceof x)return{logs:[],facts:{},errors:[n.message],durationMs:0,timedOut:n.code==="timeout"};throw n}}0&&(module.exports={SandboxError,runInSandbox});
5
5
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/bundler.ts","../src/host.ts","../src/types.ts","../src/validator.ts"],"sourcesContent":["/**\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 { 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 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 errors: validationErrors.map(\n (e) => `${e.path}:${e.line}:${e.column} — ${e.message}`,\n ),\n durationMs: 0,\n timedOut: false,\n };\n }\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 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 timeoutMs: input.timeoutMs,\n });\n return result;\n } catch (err) {\n if (err instanceof WorkerExecError) {\n return {\n logs: [],\n facts: {},\n errors: [err.message],\n durationMs: 0,\n timedOut: err.code === \"timeout\",\n };\n }\n throw err;\n }\n}\n","/**\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 { build } from \"esbuild\";\nimport type { PlaygroundFile } from \"./types.js\";\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 external: [\n \"@directive-run/core\",\n \"@directive-run/ai\",\n \"@directive-run/query\",\n ],\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 // External — let esbuild's external list handle it.\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 { mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { 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 // Use createRequire instead of `import.meta.resolve` — Vitest's SSR\n // shim of import.meta exposes a `resolve` that isn't callable, and\n // `createRequire` works in both real Node ESM and Vitest dev mode.\n // The worker subpath export points at dist/worker.js; CJS resolution\n // picks up the \"default\" condition we set in package.json.\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 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 * Resolve the directory the sandbox package's own dist sits in. The\n * temp bundle has to live somewhere Node's ESM resolver can walk UP\n * from to find `@directive-run/core` in node_modules; the sandbox\n * package's dist/ has that path because sandbox declares core as a\n * dependency. Writing the bundle next to dist/worker.js inherits the\n * same resolution chain.\n */\nasync function getSandboxPackageDir(): Promise<string> {\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. Bare specifiers like `@directive-run/core`\n * don't resolve from `data:` URLs (no hierarchical base for\n * node_modules lookup) AND don't resolve from /tmp (no node_modules\n * to walk up to). Writing inside the sandbox package directory\n * inherits its node_modules chain.\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 sandboxDir = await getSandboxPackageDir();\n const dir = mkdtempSync(join(sandboxDir, \".sandbox-tmp-\"));\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 };\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 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 * 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 * 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\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\nconst ALLOWED_IMPORT_PATTERNS: RegExp[] = [\n /^@directive-run\\/(core|ai|query)(\\/.+)?$/,\n /^\\.\\/.+\\.js$/,\n /^\\.\\.\\/.+\\.js$/,\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 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 const allowed = ALLOWED_IMPORT_PATTERNS.some((re) =>\n re.test(moduleSpecifier),\n );\n if (!allowed) {\n const { line, column } = sourceFile.getLineAndColumnAtPos(\n decl.getStart(),\n );\n errors.push({\n path: fileLabel,\n line,\n column,\n message: `import \"${moduleSpecifier}\" is not allowed in the sandbox. Allowed: @directive-run/{core,ai,query} or relative \"./X.js\" paths.`,\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\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 checkGlobalIdentifiers(file.path, project, errors);\n }\n\n return errors;\n}\n"],"mappings":"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","names":["src_exports","__export","SandboxError","runInSandbox","__toCommonJS","import_esbuild","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","import_node_fs","import_node_path","import_node_url","import_node_worker_threads","import_meta","MIN_TIMEOUT_MS","MAX_TIMEOUT_MS","DEFAULT_TIMEOUT_MS","resolveWorkerPath","createRequire","WorkerExecError","message","code","clampTimeout","value","raw","getSandboxPackageDir","workerPath","writeBundleToTemp","bundledSource","sandboxDir","dir","bundlePath","execInWorker","input","timeoutMs","cleanupTempDir","worker","timer","timedOut","startMs","resolve","reject","settled","msg","err","SandboxError","message","code","import_ts_morph","ALLOWED_IMPORT_PATTERNS","ALLOWED_GLOBALS","DENIED_GLOBALS","checkImports","fileLabel","project","errors","sourceFile","decl","moduleSpecifier","re","line","column","checkDynamicImportsAndCalls","node","parent","text","checkGlobalIdentifiers","localBindings","n","def","ns","v","f","name","c","parentKind","validateSandboxInput","files","file","MAX_PAYLOAD_BYTES","MAX_FILES","MAIN_PATH","normalizeInput","input","SandboxError","files","totalBytes","file","f","runInSandbox","err","validationErrors","validateSandboxInput","e","bundled","bundleSandboxFiles","BundleError","execInWorker","WorkerExecError"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/bundler.ts","../src/host.ts","../src/types.ts","../src/validator.ts"],"sourcesContent":["/**\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 { 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 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 errors: validationErrors.map(\n (e) => `${e.path}:${e.line}:${e.column} — ${e.message}`,\n ),\n durationMs: 0,\n timedOut: false,\n };\n }\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 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 timeoutMs: input.timeoutMs,\n });\n return result;\n } catch (err) {\n if (err instanceof WorkerExecError) {\n return {\n logs: [],\n facts: {},\n errors: [err.message],\n durationMs: 0,\n timedOut: err.code === \"timeout\",\n };\n }\n throw err;\n }\n}\n","/**\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 { build } from \"esbuild\";\nimport type { PlaygroundFile } from \"./types.js\";\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 // Wildcard pattern lets ANY @directive-run/* import the validator\n // permits resolve at worker runtime against the worker's\n // node_modules. The validator gates which packages can be\n // imported in the first place; the bundler just doesn't try to\n // inline them.\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 // External — let esbuild's external list handle it.\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 { mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { 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 // Use createRequire instead of `import.meta.resolve` — Vitest's SSR\n // shim of import.meta exposes a `resolve` that isn't callable, and\n // `createRequire` works in both real Node ESM and Vitest dev mode.\n // The worker subpath export points at dist/worker.js; CJS resolution\n // picks up the \"default\" condition we set in package.json.\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 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 * Resolve the directory the sandbox package's own dist sits in. The\n * temp bundle has to live somewhere Node's ESM resolver can walk UP\n * from to find `@directive-run/core` in node_modules; the sandbox\n * package's dist/ has that path because sandbox declares core as a\n * dependency. Writing the bundle next to dist/worker.js inherits the\n * same resolution chain.\n */\nasync function getSandboxPackageDir(): Promise<string> {\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. Bare specifiers like `@directive-run/core`\n * don't resolve from `data:` URLs (no hierarchical base for\n * node_modules lookup) AND don't resolve from /tmp (no node_modules\n * to walk up to). Writing inside the sandbox package directory\n * inherits its node_modules chain.\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 sandboxDir = await getSandboxPackageDir();\n const dir = mkdtempSync(join(sandboxDir, \".sandbox-tmp-\"));\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 };\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 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 * 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 * 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\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]);\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"],"mappings":"0jBAAA,IAAAA,GAAA,GAAAC,EAAAD,GAAA,kBAAAE,EAAA,iBAAAC,KAAA,eAAAC,EAAAJ,ICgBA,IAAAK,EAAsB,mBAGhBC,EAAa,cACbC,EAAoB,wBASbC,EAAN,cAA0B,KAAM,CACrC,YACEC,EACgBC,EAChB,CACA,MAAMD,CAAO,EAFG,WAAAC,EAGhB,KAAK,KAAO,aACd,CACF,EAoBA,SAASC,EAAmBC,EAAyB,CACnD,IAAMC,EAAK,4DACLC,EAAQF,EAAQ,MAAMC,CAAE,EAC9B,GAAI,CAACC,EACH,OAAOF,EAET,IAAMG,EAAOD,EAAM,CAAC,EAEdE,EAAU;AAAA,6CADEF,EAAM,CAAC,CACgD;AAAA,EACzE,OAAOF,EAAQ,QAAQG,EAAMA,EAAOC,CAAO,CAC7C,CAEA,eAAsBC,EACpBC,EACuB,CAEvB,GAAI,CADUA,EAAM,KAAMC,GAAMA,EAAE,OAASb,CAAU,EAEnD,MAAM,IAAIE,EACR,yBAAyBF,CAAU,oDACrC,EAGF,IAAMc,EAAU,IAAI,IACpB,QAAWC,KAAQH,EACjBE,EAAQ,IAAIC,EAAK,KAAMA,EAAK,MAAM,EAGpC,GAAI,CAiGF,IAAMC,GAhGS,QAAM,SAAM,CACzB,YAAa,CAAC,GAAGf,CAAiB,IAAID,CAAU,EAAE,EAClD,OAAQ,GACR,OAAQ,MACR,OAAQ,SACR,SAAU,OACV,MAAO,GACP,SAAU,SAMV,SAAU,CAAC,kBAAkB,EAG7B,UAAW,CAAE,kBAAmB,EAAK,EACrC,QAAS,CACP,CACE,KAAM,wBACN,MAAMiB,EAAG,CACPA,EAAE,UAAU,CAAE,OAAQ,IAAK,EAAIC,GAAS,CACtC,GAAIA,EAAK,OAAS,cAEhB,MAAO,CAAE,KADIA,EAAK,KAAK,QAAQ,GAAGjB,CAAiB,IAAK,EAAE,EAC3C,UAAWA,CAAkB,EAE9C,GAAIiB,EAAK,YAAcjB,EACrB,OAAO,KAET,GAAIiB,EAAK,KAAK,WAAW,iBAAiB,EAExC,MAAO,CAAE,SAAU,GAAM,KAAMA,EAAK,IAAK,EAE3C,GAAIA,EAAK,KAAK,WAAW,IAAI,GAAKA,EAAK,KAAK,WAAW,KAAK,EAAG,CAE7D,IAAMC,EAAcD,EAAK,SAAS,QAAQ,WAAY,EAAE,EAIlDE,EAAWF,EAAK,KACnB,QAAQ,QAAS,EAAE,EACnB,QAAQ,QAAS,EAAE,EAChBG,EAAa,CACjB,GAAGF,CAAW,IAAIC,CAAQ,MAC1B,GAAGD,CAAW,IAAIC,CAAQ,MAC1B,GAAGD,CAAW,IAAIC,CAAQ,GAG1B,GAAGA,CAAQ,MACX,GAAGA,CAAQ,KACb,EAAE,IAAKE,GAAMA,EAAE,QAAQ,OAAQ,GAAG,CAAC,EAEnC,QAAWC,KAAaF,EACtB,GAAIP,EAAQ,IAAIS,CAAS,EACvB,MAAO,CAAE,KAAMA,EAAW,UAAWtB,CAAkB,EAG3D,MAAM,IAAIC,EACR,mBAAmBgB,EAAK,IAAI,WAAWA,EAAK,QAAQ,kBAAaG,EAAW,KAAK,IAAI,CAAC,EACxF,CACF,CACA,MAAM,IAAInB,EACR,sBAAsBgB,EAAK,IAAI,WAAWA,EAAK,QAAQ,oDACzD,CACF,CAAC,EACDD,EAAE,OAAO,CAAE,OAAQ,KAAM,UAAWhB,CAAkB,EAAIiB,GAAS,CACjE,IAAMM,EAAWV,EAAQ,IAAII,EAAK,IAAI,EACtC,GAAIM,IAAa,OACf,MAAM,IAAItB,EAAY,yBAAyBgB,EAAK,IAAI,EAAE,EAU5D,GAAIA,EAAK,OAASlB,EAAY,CAC5B,IAAMyB,EAAW,CACf,GACA,2DACA,uDACA,qDACA,uCACA,uDACA,GACF,EAAE,KAAK;AAAA,CAAI,EACX,MAAO,CAAE,SAAUD,EAAWC,EAAU,OAAQ,IAAK,CACvD,CACA,MAAO,CAAE,SAAAD,EAAU,OAAQ,IAAK,CAClC,CAAC,CACH,CACF,CACF,CACF,CAAC,GACwB,YAAY,CAAC,GAAG,KACzC,GAAI,CAACR,EACH,MAAM,IAAId,EAAY,4BAA4B,EAQpD,IAAMwB,EAASrB,EAAmBW,CAAS,EAC3C,MAAO,CAAE,OAAAU,EAAQ,MAAO,OAAO,WAAWA,EAAQ,MAAM,CAAE,CAC5D,OAASC,EAAK,CACZ,MAAIA,aAAezB,EACXyB,EAEF,IAAIzB,EACR,kBAAmByB,EAAc,SAAW,OAAOA,CAAG,CAAC,GACvDA,CACF,CACF,CACF,CC9LA,IAAAC,EAAmD,cACnDC,EAA8B,gBAC9BC,EAA8B,eAC9BC,EAAuB,0BAfvBC,GAAA,GAsBMC,EAAiB,IACjBC,EAAiB,IACjBC,EAAqB,IAE3B,eAAeC,GAAqC,CAMlD,GAAM,CAAE,cAAAC,CAAc,EAAI,KAAM,QAAO,QAAa,EAEpD,OADgBA,EAAcL,GAAY,GAAG,EAC9B,QAAQ,+BAA+B,CACxD,CAOO,IAAMM,EAAN,cAA8B,KAAM,CACzC,YACEC,EACgBC,EAChB,CACA,MAAMD,CAAO,EAFG,UAAAC,EAGhB,KAAK,KAAO,iBACd,CACF,EAEA,SAASC,EAAaC,EAAmC,CACvD,IAAMC,EAAMD,GAASP,EACrB,OAAK,OAAO,SAASQ,CAAG,EAGjB,KAAK,IAAIT,EAAgB,KAAK,IAAID,EAAgB,KAAK,MAAMU,CAAG,CAAC,CAAC,EAFhER,CAGX,CAUA,eAAeS,GAAwC,CAGrD,IAAMC,EAAa,MAAMT,EAAkB,EAE3C,SAAO,cAAQ,WAAQS,CAAU,CAAC,CACpC,CAYA,eAAeC,GAAkBC,EAG9B,CACD,IAAMC,EAAa,MAAMJ,EAAqB,EACxCK,KAAM,kBAAY,QAAKD,EAAY,eAAe,CAAC,EACnDE,KAAa,QAAKD,EAAK,YAAY,EACzC,0BAAcC,EAAYH,EAAe,MAAM,EACxC,CACL,WAAAG,EACA,QAAS,IAAM,CACb,GAAI,IACF,UAAOD,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,CAC9C,MAAQ,CAER,CACF,CACF,CACF,CAEA,eAAsBE,EACpBC,EACwB,CACxB,IAAMC,EAAYZ,EAAaW,EAAM,SAAS,EACxCP,EAAa,MAAMT,EAAkB,EACrC,CAAE,WAAAc,EAAY,QAASI,CAAe,EAAI,MAAMR,GACpDM,EAAM,aACR,EACMG,EAAS,IAAI,SAAOV,EAAY,CAGpC,eAAgB,CACd,uBAAwB,GACxB,yBAA0B,EAC1B,gBAAiB,EACnB,EAGA,OAAQ,EACV,CAAC,EAEGW,EAA+B,KAC/BC,EAAW,GACTC,EAAU,KAAK,IAAI,EAEzB,GAAI,CA+CF,OA9Ce,MAAM,IAAI,QAAuB,CAACC,EAASC,IAAW,CACnE,IAAIC,EAAU,GAEdN,EAAO,KAAK,UAAYO,GAA6B,CACnDD,EAAU,GACNC,EAAI,GACNH,EAAQG,EAAI,MAAM,EAElBF,EAAO,IAAItB,EAAgBwB,EAAI,MAAO,cAAc,CAAC,CAEzD,CAAC,EAEDP,EAAO,KAAK,QAAUQ,GAAe,CACnCF,EAAU,GACVD,EAAO,IAAItB,EAAgByB,EAAI,QAAS,cAAc,CAAC,CACzD,CAAC,EAEDR,EAAO,KAAK,OAASf,GAAS,CACxB,CAACqB,GAAWrB,IAAS,GAAKA,IAAS,MACrCoB,EACE,IAAItB,EACF,2BAA2BE,CAAI,qBAC/B,cACF,CACF,CAEJ,CAAC,EAEDgB,EAAQ,WAAW,IAAM,CACvBC,EAAW,GACXF,EAAO,UAAU,EACjBK,EACE,IAAItB,EACF,wBAAwBe,CAAS,aACjC,SACF,CACF,CACF,EAAGA,CAAS,EAEZ,IAAMd,EAA8B,CAClC,cAAY,iBAAcW,CAAU,EAAE,KACtC,UAAAG,CACF,EACAE,EAAO,YAAYhB,CAAO,CAC5B,CAAC,CAGH,OAASwB,EAAK,CACZ,GAAIA,aAAezB,GAAmByB,EAAI,OAAS,UAGjD,MAAO,CACL,KAAM,CAAC,EACP,MAAO,CAAC,EACR,OAAQ,CAACA,EAAI,OAAO,EACpB,WAAY,KAAK,IAAI,EAAIL,EACzB,SAAU,EACZ,EAEF,MAAMK,CACR,QAAE,CACIP,GACF,aAAaA,CAAK,EAEpB,MAAMD,EAAO,UAAU,EAAE,MAAM,IAAG,EAAY,EAC9CD,EAAe,CAEjB,CACF,CC5IO,IAAMU,EAAN,cAA2B,KAAM,CACtC,YACEC,EACgBC,EAChB,CACA,MAAMD,CAAO,EAFG,UAAAC,EAGhB,KAAK,KAAO,cACd,CACF,EC3CA,IAAAC,EAAoC,oBAuB9BC,EAA6B,IAAI,IAAY,CACjD,OACA,KACA,QACA,KACA,QACA,MACA,SACA,QACA,MACA,aACA,WACA,UACA,YACA,WACA,gBACA,MACF,CAAC,EAEKC,EAA4B,IAAI,IAAY,CAChD,MACA,MACA,UACA,uBACF,CAAC,EAWD,SAASC,EAAwBC,EAAkC,CACjE,IAAMC,EAAQD,EAAU,MAAM,0BAA0B,EACxD,OAAOC,EAAQA,EAAM,CAAC,EAAK,IAC7B,CAEA,SAASC,GAAgBF,EAA4B,CACnD,GAAI,oBAAoB,KAAKA,CAAS,EACpC,MAAO,GAET,IAAMG,EAAOJ,EAAwBC,CAAS,EAI9C,OAHIG,IAAS,MAGTL,EAA0B,IAAIK,CAAI,EAC7B,GAEFN,EAA2B,IAAIM,CAAI,CAC5C,CAQA,IAAMC,EAAkB,IAAI,IAAY,CAEtC,eAEA,SAEA,UACA,OACA,OACA,SACA,QACA,SACA,SACA,UACA,SACA,UACA,QACA,OACA,MACA,MACA,UACA,UACA,UACA,aAEA,YACA,OACA,MACA,UACF,CAAC,EAOKC,EAAiB,IAAI,IAAY,CACrC,UACA,UACA,SACA,YACA,aACA,QACA,iBACA,YACA,OACA,WAGA,SACA,eACA,iBACA,aACA,cACA,eACA,eACF,CAAC,EASD,SAASC,GAAuBN,EAA2B,CACzD,IAAMG,EAAOJ,EAAwBC,CAAS,EAC9C,OAAIG,GAAQL,EAA0B,IAAIK,CAAI,EACrC,WAAWH,CAAS,qCAAgCG,CAAI,oEAE1D,WAAWH,CAAS,+FAA+F,MAAM,KAC9HH,CACF,EACG,KAAK,EACL,KAAK,GAAG,CAAC,IACd,CAEA,SAASU,GACPC,EACAC,EACAC,EACM,CACN,IAAMC,EAAaF,EAAQ,qBAAqBD,CAAS,EACzD,QAAWI,KAAQD,EAAW,sBAAsB,EAAG,CACrD,IAAME,EAAkBD,EAAK,wBAAwB,EACrD,GAAI,CAACV,GAAgBW,CAAe,EAAG,CACrC,GAAM,CAAE,KAAAC,EAAM,OAAAC,CAAO,EAAIJ,EAAW,sBAClCC,EAAK,SAAS,CAChB,EACAF,EAAO,KAAK,CACV,KAAMF,EACN,KAAAM,EACA,OAAAC,EACA,QAAST,GAAuBO,CAAe,CACjD,CAAC,CACH,CACF,CACF,CAEA,SAASG,GACPR,EACAC,EACAC,EACM,CACN,IAAMC,EAAaF,EAAQ,qBAAqBD,CAAS,EAGzDG,EAAW,kBAAmBM,GAAS,CACrC,GAAIA,EAAK,QAAQ,IAAM,aAAW,cAAe,CAC/C,IAAMC,EAASD,EAAK,UAAU,EAC9B,GAAIC,GAAUA,EAAO,QAAQ,IAAM,aAAW,eAAgB,CAC5D,GAAM,CAAE,KAAAJ,EAAM,OAAAC,CAAO,EAAIJ,EAAW,sBAClCM,EAAK,SAAS,CAChB,EACAP,EAAO,KAAK,CACV,KAAMF,EACN,KAAAM,EACA,OAAAC,EACA,QAAS,gDACX,CAAC,CACH,CACF,CACA,GAAIE,EAAK,QAAQ,IAAM,aAAW,cAAe,CAC/C,IAAME,EAAOF,EAAK,QAAQ,EAC1B,GAAI,uBAAuB,KAAKE,CAAI,EAAG,CACrC,GAAM,CAAE,KAAAL,EAAM,OAAAC,CAAO,EAAIJ,EAAW,sBAClCM,EAAK,SAAS,CAChB,EACAP,EAAO,KAAK,CACV,KAAMF,EACN,KAAAM,EACA,OAAAC,EACA,QAAS,iDACX,CAAC,CACH,CACF,CACF,CAAC,CACH,CAuCA,SAASK,GACPZ,EACAC,EACAC,EACM,CACN,IAAMC,EAAaF,EAAQ,qBAAqBD,CAAS,EAEnDa,EAAS,CAACJ,EAA+BK,IAAoB,CACjE,GAAM,CAAE,KAAAR,EAAM,OAAAC,CAAO,EAAIJ,EAAW,sBAAsBM,EAAK,SAAS,CAAC,EACzEP,EAAO,KAAK,CAAE,KAAMF,EAAW,KAAAM,EAAM,OAAAC,EAAQ,QAAAO,CAAQ,CAAC,CACxD,EAEMC,EAAsBJ,GAAgC,CAC1D,IAAMK,EAAUL,EAAK,KAAK,EAC1B,OACGK,EAAQ,WAAW,GAAG,GAAKA,EAAQ,SAAS,GAAG,GAC/CA,EAAQ,WAAW,GAAG,GAAKA,EAAQ,SAAS,GAAG,GAC/CA,EAAQ,WAAW,GAAG,GAAKA,EAAQ,SAAS,GAAG,EAEzCA,EAAQ,MAAM,EAAG,EAAE,EAErB,IACT,EAEAb,EAAW,kBAAmBM,GAAS,CACrC,IAAMQ,EAAOR,EAAK,QAAQ,EAG1B,GAAIQ,IAAS,aAAW,yBAA0B,CAChD,IAAMC,EAAQT,EAAoC,UAAU,EAC5D,GAAI,CAACS,EACH,OAEF,GAAIA,IAAS,cAAe,CAC1BL,EACEJ,EACA,sFACF,EACA,MACF,CACA,GAAIZ,EAAe,IAAIqB,CAAI,EAAG,CAC5BL,EACEJ,EACA,MAAMS,CAAI,mKACZ,EACA,MACF,CACF,CAGA,GAAID,IAAS,aAAW,wBAAyB,CAC/C,IAAME,EACJV,EACA,gBAAgB,EACZW,EACJX,EAGA,wBAAwB,EACpBY,EAAeF,GAAY,QAAQ,GAAK,GACxCG,EAAUF,GAAU,QAAQ,GAAK,GACjCG,EAAUR,EAAmBO,CAAO,EAG1C,GAAID,IAAiB,cAAgBE,IAAY,KAAM,CACrDV,EACEJ,EACA,uIACF,EACA,MACF,CAGA,GAAIc,IAAY,OACVA,IAAY,eAAiB1B,EAAe,IAAI0B,CAAO,GAAG,CAC5DV,EACEJ,EACA,sBAAsBc,CAAO,2DAC/B,EACA,MACF,CAEJ,CAGA,GAAIN,IAAS,aAAW,eAAgB,CAStC,IAAMO,EAPJf,EAMA,gBAAgB,GACW,QAAQ,GAAK,GAC1C,GAAIe,IAAa,WAAY,CAC3BX,EAAOJ,EAAM,+CAA+C,EAC5D,MACF,CAGA,IAAMgB,EAAgBD,EAAS,MAAM,2BAA2B,EAChE,GAAIC,EAAe,CAIjB,IAAMC,EAHWjB,EAGK,eAAe,GAAK,CAAC,EAC3C,GAAIiB,EAAK,QAAU,EAAG,CACpB,IAAMC,EAAWD,EAAK,CAAC,EAAG,QAAQ,EAC5BE,EAAmBb,EAAmBW,EAAK,CAAC,EAAG,QAAQ,CAAC,EAC1C,IAAI,IAAI,CAC1B,MACA,MACA,2BACA,4BACA,UACA,gBACF,CAAC,EAEa,IAAID,EAAc,CAAC,CAAE,IAChCE,IAAa,cAAgB/B,EAAgB,IAAI+B,CAAQ,IAC1DC,IAAqB,OACpBA,IAAqB,eACpB/B,EAAe,IAAI+B,CAAgB,IAErCf,EACEJ,EACA,KAAKgB,EAAc,CAAC,CAAC,IAAIA,EAAc,CAAC,CAAC,IAAIE,CAAQ,MAAMC,CAAgB,gCAC7E,CAEJ,CACF,CACF,CACF,CAAC,CACH,CAEA,SAASC,GACP7B,EACAC,EACAC,EACM,CACN,IAAMC,EAAaF,EAAQ,qBAAqBD,CAAS,EAKnD8B,EAAgB,IAAI,IAC1B,QAAW1B,KAAQD,EAAW,sBAAsB,EAAG,CACrD,QAAW4B,KAAK3B,EAAK,gBAAgB,EACnC0B,EAAc,IAAIC,EAAE,QAAQ,CAAC,EAE/B,IAAMC,EAAM5B,EAAK,iBAAiB,EAC9B4B,GAAKF,EAAc,IAAIE,EAAI,QAAQ,CAAC,EACxC,IAAMC,EAAK7B,EAAK,mBAAmB,EAC/B6B,GAAIH,EAAc,IAAIG,EAAG,QAAQ,CAAC,CACxC,CACA,QAAWC,KAAK/B,EAAW,wBAAwB,EACjD2B,EAAc,IAAII,EAAE,QAAQ,CAAC,EAE/B,QAAWC,KAAKhC,EAAW,aAAa,EAAG,CACzC,IAAMe,EAAOiB,EAAE,QAAQ,EACnBjB,GAAMY,EAAc,IAAIZ,CAAI,CAClC,CACA,QAAWkB,KAAKjC,EAAW,WAAW,EAAG,CACvC,IAAMe,EAAOkB,EAAE,QAAQ,EACnBlB,GAAMY,EAAc,IAAIZ,CAAI,CAClC,CAEAf,EAAW,kBAAmBM,GAAS,CACrC,GAAIA,EAAK,QAAQ,IAAM,aAAW,WAChC,OAEF,IAAMS,EAAOT,EAAK,QAAQ,EAI1B,GAHIqB,EAAc,IAAIZ,CAAI,GAGtBtB,EAAgB,IAAIsB,CAAI,EAC1B,OAcF,IAAMR,EAASD,EAAK,UAAU,EAC9B,GAAIC,EAAQ,CACV,IAAM2B,EAAa3B,EAAO,QAAQ,EAqClC,GAnCI2B,IAAe,aAAW,oBAE1B3B,EACA,cAAc,IACKD,GAKnB4B,IAAe,aAAW,0BAE1B3B,EACA,cAAc,IACGD,GAKjB4B,IAAe,aAAW,mBAK5BA,IAAe,aAAW,iBAC1BA,IAAe,aAAW,iBAC1BA,IAAe,aAAW,iBAC1BA,IAAe,aAAW,cAKxBA,IAAe,aAAW,WAK5BA,IAAe,aAAW,eAC1BA,IAAe,aAAW,UAE1B,MAEJ,CAEA,GAAIxC,EAAe,IAAIqB,CAAI,EAAG,CAC5B,GAAM,CAAE,KAAAZ,EAAM,OAAAC,CAAO,EAAIJ,EAAW,sBAClCM,EAAK,SAAS,CAChB,EACAP,EAAO,KAAK,CACV,KAAMF,EACN,KAAAM,EACA,OAAAC,EACA,QAAS,eAAeW,CAAI,sDAC9B,CAAC,CACH,CAMF,CAAC,CACH,CAEO,SAASoB,EACdC,EACmB,CACnB,IAAMrC,EAA4B,CAAC,EAC7BD,EAAU,IAAI,UAAQ,CAC1B,sBAAuB,GACvB,gBAAiB,CACf,OAAQ,GACR,OAAQ,GACR,QAAS,GACT,OAAQ,EACV,CACF,CAAC,EAED,QAAWuC,KAAQD,EACjBtC,EAAQ,iBAAiBuC,EAAK,KAAMA,EAAK,OAAQ,CAAE,UAAW,EAAK,CAAC,EAGtE,QAAWA,KAAQD,EACjBxC,GAAayC,EAAK,KAAMvC,EAASC,CAAM,EACvCM,GAA4BgC,EAAK,KAAMvC,EAASC,CAAM,EACtDU,GAA2B4B,EAAK,KAAMvC,EAASC,CAAM,EACrD2B,GAAuBW,EAAK,KAAMvC,EAASC,CAAM,EAGnD,OAAOA,CACT,CJphBA,IAAMuC,EAAoB,IACpBC,EAAY,GACZC,EAAY,cAElB,SAASC,GAAeC,EAA4C,CAClE,GAAIA,EAAM,OAASA,EAAM,OACvB,MAAM,IAAIC,EACR,4CACA,eACF,EAEF,GAAI,CAACD,EAAM,OAAS,CAACA,EAAM,OACzB,MAAM,IAAIC,EACR,uCACA,eACF,EAEF,IAAMC,EAA0BF,EAAM,MAClCA,EAAM,MACN,CAAC,CAAE,KAAMF,EAAW,OAAQE,EAAM,QAAU,EAAG,CAAC,EAEpD,GAAIE,EAAM,SAAW,EACnB,MAAM,IAAID,EAAa,uBAAwB,eAAe,EAEhE,GAAIC,EAAM,OAASL,EACjB,MAAM,IAAII,EACR,mBAAmBJ,CAAS,SAC5B,eACF,EAEF,IAAIM,EAAa,EACjB,QAAWC,KAAQF,EAAO,CACxB,GAAI,OAAOE,EAAK,MAAS,UAAYA,EAAK,KAAK,SAAW,EACxD,MAAM,IAAIH,EACR,wCACA,eACF,EAEF,GAAI,OAAOG,EAAK,QAAW,UAAYA,EAAK,OAAO,SAAW,EAC5D,MAAM,IAAIH,EACR,SAASG,EAAK,IAAI,qBAClB,eACF,EAEFD,GAAc,OAAO,WAAWC,EAAK,OAAQ,MAAM,CACrD,CACA,GAAID,EAAaP,EACf,MAAM,IAAIK,EACR,oBAAoBE,CAAU,eAAeP,CAAiB,IAC9D,eACF,EAGF,GAAI,CAACM,EAAM,KAAMG,GAAMA,EAAE,OAASP,CAAS,EACzC,MAAM,IAAIG,EACR,yBAAyBH,CAAS,kCAClC,eACF,EAEF,OAAOI,CACT,CAEA,eAAsBI,GACpBN,EACwB,CACxB,IAAIE,EACJ,GAAI,CACFA,EAAQH,GAAeC,CAAK,CAC9B,OAASO,EAAK,CACZ,GAAIA,aAAeN,EACjB,MAAO,CACL,KAAM,CAAC,EACP,MAAO,CAAC,EACR,OAAQ,CAACM,EAAI,OAAO,EACpB,WAAY,EACZ,SAAU,EACZ,EAEF,MAAMA,CACR,CAEA,IAAMC,EAAmBC,EAAqBP,CAAK,EACnD,GAAIM,EAAiB,OAAS,EAC5B,MAAO,CACL,KAAM,CAAC,EACP,MAAO,CAAC,EACR,OAAQA,EAAiB,IACtBE,GAAM,GAAGA,EAAE,IAAI,IAAIA,EAAE,IAAI,IAAIA,EAAE,MAAM,WAAMA,EAAE,OAAO,EACvD,EACA,WAAY,EACZ,SAAU,EACZ,EAGF,IAAIC,EACJ,GAAI,CACFA,EAAU,MAAMC,EAAmBV,CAAK,CAC1C,OAASK,EAAK,CACZ,GAAIA,aAAeM,EACjB,MAAO,CACL,KAAM,CAAC,EACP,MAAO,CAAC,EACR,OAAQ,CAACN,EAAI,OAAO,EACpB,WAAY,EACZ,SAAU,EACZ,EAEF,MAAMA,CACR,CAEA,GAAI,CAKF,OAJe,MAAMO,EAAa,CAChC,cAAeH,EAAQ,OACvB,UAAWX,EAAM,SACnB,CAAC,CAEH,OAASO,EAAK,CACZ,GAAIA,aAAeQ,EACjB,MAAO,CACL,KAAM,CAAC,EACP,MAAO,CAAC,EACR,OAAQ,CAACR,EAAI,OAAO,EACpB,WAAY,EACZ,SAAUA,EAAI,OAAS,SACzB,EAEF,MAAMA,CACR,CACF","names":["src_exports","__export","SandboxError","runInSandbox","__toCommonJS","import_esbuild","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","import_node_fs","import_node_path","import_node_url","import_node_worker_threads","import_meta","MIN_TIMEOUT_MS","MAX_TIMEOUT_MS","DEFAULT_TIMEOUT_MS","resolveWorkerPath","createRequire","WorkerExecError","message","code","clampTimeout","value","raw","getSandboxPackageDir","workerPath","writeBundleToTemp","bundledSource","sandboxDir","dir","bundlePath","execInWorker","input","timeoutMs","cleanupTempDir","worker","timer","timedOut","startMs","resolve","reject","settled","msg","err","SandboxError","message","code","import_ts_morph","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","bundled","bundleSandboxFiles","BundleError","execInWorker","WorkerExecError"]}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import{build as _}from"esbuild";var w="src/main.ts",g="directive-sandbox-vfs",p=class extends Error{constructor(n,e){super(n);this.cause=e;this.name="BundleError"}};function F(t){let o=/(var\s+(system\d*)\s*=\s*createSystem\s*\([\s\S]*?\)\s*;)/,n=t.match(o);if(!n)return t;let e=n[1],s=`
2
- (globalThis).__directiveSandbox_system__ = ${n[2]};
3
- `;return t.replace(e,e+s)}async function S(t){if(!t.find(e=>e.path===w))throw new p(`payload must include "${w}" \u2014 that's the entry point the runner targets`);let n=new Map;for(let e of t)n.set(e.path,e.source);try{let r=(await _({entryPoints:[`${g}:${w}`],bundle:!0,format:"esm",target:"node20",platform:"node",write:!1,logLevel:"silent",external:["@directive-run/core","@directive-run/ai","@directive-run/query"],supported:{"top-level-await":!0},plugins:[{name:"directive-sandbox-vfs",setup(u){u.onResolve({filter:/.*/},i=>{if(i.kind==="entry-point")return{path:i.path.replace(`${g}:`,""),namespace:g};if(i.namespace!==g)return null;if(i.path.startsWith("@directive-run/"))return{external:!0,path:i.path};if(i.path.startsWith("./")||i.path.startsWith("../")){let a=i.importer.replace(/\/[^/]+$/,""),c=i.path.replace(/^\.\//,"").replace(/\.js$/,""),y=[`${a}/${c}.ts`,`${a}/${c}.js`,`${a}/${c}`,`${c}.ts`,`${c}.js`].map(m=>m.replace(/\/+/g,"/"));for(let m of y)if(n.has(m))return{path:m,namespace:g};throw new p(`cannot resolve "${i.path}" from "${i.importer}" \u2014 tried ${y.join(", ")}`)}throw new p(`unexpected import "${i.path}" from "${i.importer}" \u2014 only relative or @directive-run/* allowed`)}),u.onLoad({filter:/.*/,namespace:g},i=>{let a=n.get(i.path);if(a===void 0)throw new p(`virtual file missing: ${i.path}`);if(i.path===w){let c=["","// directive-sandbox: lift the runner's `system` binding","// onto a side-channel global so the worker can read","// system.facts.$store.toObject() after execution.","if (typeof system !== 'undefined') {"," (globalThis).__directiveSandbox_system__ = system;","}"].join(`
4
- `);return{contents:a+c,loader:"ts"}}return{contents:a,loader:"ts"}})}}]})).outputFiles[0]?.text;if(!r)throw new p("esbuild produced no output");let s=F(r);return{source:s,bytes:Buffer.byteLength(s,"utf8")}}catch(e){throw e instanceof p?e:new p(`bundle failed: ${e.message??String(e)}`,e)}}import{mkdtempSync as A,rmSync as R,writeFileSync as O}from"fs";import{dirname as v,join as M}from"path";import{pathToFileURL as D}from"url";import{Worker as j}from"worker_threads";var L=100,W=1e4,I=5e3;async function P(){let{createRequire:t}=await import("module");return t(import.meta.url).resolve("@directive-run/sandbox/worker")}var h=class extends Error{constructor(n,e){super(n);this.code=e;this.name="WorkerExecError"}};function B(t){let o=t??I;return Number.isFinite(o)?Math.min(W,Math.max(L,Math.floor(o))):I}async function C(){let t=await P();return v(v(t))}async function V(t){let o=await C(),n=A(M(o,".sandbox-tmp-")),e=M(n,"bundle.mjs");return O(e,t,"utf8"),{bundlePath:e,cleanup:()=>{try{R(n,{recursive:!0,force:!0})}catch{}}}}async function E(t){let o=B(t.timeoutMs),n=await P(),{bundlePath:e,cleanup:r}=await V(t.bundledSource),s=new j(n,{resourceLimits:{maxOldGenerationSizeMb:32,maxYoungGenerationSizeMb:8,codeRangeSizeMb:16},stderr:!1}),u=null,i=!1,a=Date.now();try{return await new Promise((y,m)=>{let b=!1;s.once("message",f=>{b=!0,f.ok?y(f.result):m(new h(f.error,"worker-error"))}),s.once("error",f=>{b=!0,m(new h(f.message,"worker-error"))}),s.once("exit",f=>{!b&&f!==0&&f!==null&&m(new h(`worker exited with code ${f} before responding`,"worker-error"))}),u=setTimeout(()=>{i=!0,s.terminate(),m(new h(`wall-clock budget of ${o}ms elapsed`,"timeout"))},o);let N={bundlePath:D(e).href,timeoutMs:o};s.postMessage(N)})}catch(c){if(c instanceof h&&c.code==="timeout")return{logs:[],facts:{},errors:[c.message],durationMs:Date.now()-a,timedOut:!0};throw c}finally{u&&clearTimeout(u),await s.terminate().catch(()=>{}),r()}}var d=class extends Error{constructor(n,e){super(n);this.code=e;this.name="SandboxError"}};import{Project as q,SyntaxKind as l}from"ts-morph";var K=[/^@directive-run\/(core|ai|query)(\/.+)?$/,/^\.\/.+\.js$/,/^\.\.\/.+\.js$/],U=new Set(["createSystem","system","console","Math","JSON","Object","Array","Number","String","Boolean","Symbol","Promise","Error","Date","Map","Set","WeakMap","WeakSet","Reflect","globalThis","undefined","null","NaN","Infinity"]),G=new Set(["process","require","module","__dirname","__filename","fetch","XMLHttpRequest","WebSocket","eval","Function","Buffer","setImmediate","queueMicrotask","setTimeout","setInterval","clearTimeout","clearInterval"]);function H(t,o,n){let e=o.getSourceFileOrThrow(t);for(let r of e.getImportDeclarations()){let s=r.getModuleSpecifierValue();if(!K.some(i=>i.test(s))){let{line:i,column:a}=e.getLineAndColumnAtPos(r.getStart());n.push({path:t,line:i,column:a,message:`import "${s}" is not allowed in the sandbox. Allowed: @directive-run/{core,ai,query} or relative "./X.js" paths.`})}}}function X(t,o,n){let e=o.getSourceFileOrThrow(t);e.forEachDescendant(r=>{if(r.getKind()===l.ImportKeyword){let s=r.getParent();if(s&&s.getKind()===l.CallExpression){let{line:u,column:i}=e.getLineAndColumnAtPos(r.getStart());n.push({path:t,line:u,column:i,message:"dynamic import() is not allowed in the sandbox"})}}if(r.getKind()===l.NewExpression){let s=r.getText();if(/^new\s+Function\s*\(/.test(s)){let{line:u,column:i}=e.getLineAndColumnAtPos(r.getStart());n.push({path:t,line:u,column:i,message:"new Function(...) is not allowed in the sandbox"})}}})}function z(t,o,n){let e=o.getSourceFileOrThrow(t),r=new Set;for(let s of e.getImportDeclarations()){for(let a of s.getNamedImports())r.add(a.getName());let u=s.getDefaultImport();u&&r.add(u.getText());let i=s.getNamespaceImport();i&&r.add(i.getText())}for(let s of e.getVariableDeclarations())r.add(s.getName());for(let s of e.getFunctions()){let u=s.getName();u&&r.add(u)}for(let s of e.getClasses()){let u=s.getName();u&&r.add(u)}e.forEachDescendant(s=>{if(s.getKind()!==l.Identifier)return;let u=s.getText();if(r.has(u)||U.has(u))return;let i=s.getParent();if(i){let a=i.getKind();if(a===l.PropertyAssignment&&i.getNameNode?.()===s||a===l.PropertyAccessExpression&&i.getNameNode?.()===s||a===l.MethodDeclaration||a===l.ImportSpecifier||a===l.ExportSpecifier||a===l.NamespaceImport||a===l.ImportClause||a===l.Parameter||a===l.TypeReference||a===l.TypeQuery)return}if(G.has(u)){let{line:a,column:c}=e.getLineAndColumnAtPos(s.getStart());n.push({path:t,line:a,column:c,message:`identifier "${u}" is denied in the sandbox (FS/network/eval surface)`})}})}function k(t){let o=[],n=new q({useInMemoryFileSystem:!0,compilerOptions:{target:99,module:99,allowJs:!0,strict:!1}});for(let e of t)n.createSourceFile(e.path,e.source,{overwrite:!0});for(let e of t)H(e.path,n,o),X(e.path,n,o),z(e.path,n,o);return o}var $=2e5,T=10,x="src/main.ts";function Y(t){if(t.files&&t.source)throw new d("pass either `source` or `files`, not both","input-invalid");if(!t.files&&!t.source)throw new d("must pass either `source` or `files`","input-invalid");let o=t.files?t.files:[{path:x,source:t.source??""}];if(o.length===0)throw new d("files array is empty","input-invalid");if(o.length>T)throw new d(`payload exceeds ${T} files`,"input-invalid");let n=0;for(let e of o){if(typeof e.path!="string"||e.path.length===0)throw new d("every file must have a non-empty path","input-invalid");if(typeof e.source!="string"||e.source.length===0)throw new d(`file "${e.path}" has empty source`,"input-invalid");n+=Buffer.byteLength(e.source,"utf8")}if(n>$)throw new d(`total payload is ${n} bytes (max ${$})`,"input-invalid");if(!o.some(e=>e.path===x))throw new d(`payload must include "${x}" \u2014 the runner entry point`,"input-invalid");return o}async function de(t){let o;try{o=Y(t)}catch(r){if(r instanceof d)return{logs:[],facts:{},errors:[r.message],durationMs:0,timedOut:!1};throw r}let n=k(o);if(n.length>0)return{logs:[],facts:{},errors:n.map(r=>`${r.path}:${r.line}:${r.column} \u2014 ${r.message}`),durationMs:0,timedOut:!1};let e;try{e=await S(o)}catch(r){if(r instanceof p)return{logs:[],facts:{},errors:[r.message],durationMs:0,timedOut:!1};throw r}try{return await E({bundledSource:e.source,timeoutMs:t.timeoutMs})}catch(r){if(r instanceof h)return{logs:[],facts:{},errors:[r.message],durationMs:0,timedOut:r.code==="timeout"};throw r}}export{d as SandboxError,de as runInSandbox};
1
+ import{build as D}from"esbuild";var b="src/main.ts",y="directive-sandbox-vfs",h=class extends Error{constructor(i,e){super(i);this.cause=e;this.name="BundleError"}};function W(t){let r=/(var\s+(system\d*)\s*=\s*createSystem\s*\([\s\S]*?\)\s*;)/,i=t.match(r);if(!i)return t;let e=i[1],a=`
2
+ (globalThis).__directiveSandbox_system__ = ${i[2]};
3
+ `;return t.replace(e,e+a)}async function v(t){if(!t.find(e=>e.path===b))throw new h(`payload must include "${b}" \u2014 that's the entry point the runner targets`);let i=new Map;for(let e of t)i.set(e.path,e.source);try{let n=(await D({entryPoints:[`${y}:${b}`],bundle:!0,format:"esm",target:"node20",platform:"node",write:!1,logLevel:"silent",external:["@directive-run/*"],supported:{"top-level-await":!0},plugins:[{name:"directive-sandbox-vfs",setup(s){s.onResolve({filter:/.*/},o=>{if(o.kind==="entry-point")return{path:o.path.replace(`${y}:`,""),namespace:y};if(o.namespace!==y)return null;if(o.path.startsWith("@directive-run/"))return{external:!0,path:o.path};if(o.path.startsWith("./")||o.path.startsWith("../")){let c=o.importer.replace(/\/[^/]+$/,""),u=o.path.replace(/^\.\//,"").replace(/\.js$/,""),g=[`${c}/${u}.ts`,`${c}/${u}.js`,`${c}/${u}`,`${u}.ts`,`${u}.js`].map(f=>f.replace(/\/+/g,"/"));for(let f of g)if(i.has(f))return{path:f,namespace:y};throw new h(`cannot resolve "${o.path}" from "${o.importer}" \u2014 tried ${g.join(", ")}`)}throw new h(`unexpected import "${o.path}" from "${o.importer}" \u2014 only relative or @directive-run/* allowed`)}),s.onLoad({filter:/.*/,namespace:y},o=>{let c=i.get(o.path);if(c===void 0)throw new h(`virtual file missing: ${o.path}`);if(o.path===b){let u=["","// directive-sandbox: lift the runner's `system` binding","// onto a side-channel global so the worker can read","// system.facts.$store.toObject() after execution.","if (typeof system !== 'undefined') {"," (globalThis).__directiveSandbox_system__ = system;","}"].join(`
4
+ `);return{contents:c+u,loader:"ts"}}return{contents:c,loader:"ts"}})}}]})).outputFiles[0]?.text;if(!n)throw new h("esbuild produced no output");let a=W(n);return{source:a,bytes:Buffer.byteLength(a,"utf8")}}catch(e){throw e instanceof h?e:new h(`bundle failed: ${e.message??String(e)}`,e)}}import{mkdtempSync as j,rmSync as L,writeFileSync as C}from"fs";import{dirname as P,join as I}from"path";import{pathToFileURL as B}from"url";import{Worker as K}from"worker_threads";var V=100,q=1e4,M=5e3;async function T(){let{createRequire:t}=await import("module");return t(import.meta.url).resolve("@directive-run/sandbox/worker")}var x=class extends Error{constructor(i,e){super(i);this.code=e;this.name="WorkerExecError"}};function G(t){let r=t??M;return Number.isFinite(r)?Math.min(q,Math.max(V,Math.floor(r))):M}async function U(){let t=await T();return P(P(t))}async function H(t){let r=await U(),i=j(I(r,".sandbox-tmp-")),e=I(i,"bundle.mjs");return C(e,t,"utf8"),{bundlePath:e,cleanup:()=>{try{L(i,{recursive:!0,force:!0})}catch{}}}}async function k(t){let r=G(t.timeoutMs),i=await T(),{bundlePath:e,cleanup:n}=await H(t.bundledSource),a=new K(i,{resourceLimits:{maxOldGenerationSizeMb:32,maxYoungGenerationSizeMb:8,codeRangeSizeMb:16},stderr:!1}),s=null,o=!1,c=Date.now();try{return await new Promise((g,f)=>{let p=!1;a.once("message",d=>{p=!0,d.ok?g(d.result):f(new x(d.error,"worker-error"))}),a.once("error",d=>{p=!0,f(new x(d.message,"worker-error"))}),a.once("exit",d=>{!p&&d!==0&&d!==null&&f(new x(`worker exited with code ${d} before responding`,"worker-error"))}),s=setTimeout(()=>{o=!0,a.terminate(),f(new x(`wall-clock budget of ${r}ms elapsed`,"timeout"))},r);let w={bundlePath:B(e).href,timeoutMs:r};a.postMessage(w)})}catch(u){if(u instanceof x&&u.code==="timeout")return{logs:[],facts:{},errors:[u.message],durationMs:Date.now()-c,timedOut:!0};throw u}finally{s&&clearTimeout(s),await a.terminate().catch(()=>{}),n()}}var m=class extends Error{constructor(i,e){super(i);this.code=e;this.name="SandboxError"}};import{Project as X,SyntaxKind as l}from"ts-morph";var A=new Set(["core","ai","query","el","react","vue","svelte","solid","lit","optimistic","timeline","mutator","knowledge","scaffold","claude-plugin","lint"]),$=new Set(["cli","mcp","sandbox","vite-plugin-api-proxy"]);function F(t){let r=t.match(/^@directive-run\/([^/]+)/);return r?r[1]:null}function z(t){if(/^\.{1,2}\/.+\.js$/.test(t))return!0;let r=F(t);return r===null||$.has(r)?!1:A.has(r)}var N=new Set(["createSystem","system","console","Math","JSON","Object","Array","Number","String","Boolean","Symbol","Promise","Error","Date","Map","Set","WeakMap","WeakSet","Reflect","globalThis","undefined","null","NaN","Infinity"]),S=new Set(["process","require","module","__dirname","__filename","fetch","XMLHttpRequest","WebSocket","eval","Function","Buffer","setImmediate","queueMicrotask","setTimeout","setInterval","clearTimeout","clearInterval"]);function Y(t){let r=F(t);return r&&$.has(r)?`import "${t}" is denied \u2014 @directive-run/${r} is a build/CLI/sandbox tool, not for use inside a sandboxed demo`:`import "${t}" is not allowed in the sandbox. Allowed: relative "./X.js" paths or any of @directive-run/{${Array.from(A).sort().join(",")}}.`}function J(t,r,i){let e=r.getSourceFileOrThrow(t);for(let n of e.getImportDeclarations()){let a=n.getModuleSpecifierValue();if(!z(a)){let{line:s,column:o}=e.getLineAndColumnAtPos(n.getStart());i.push({path:t,line:s,column:o,message:Y(a)})}}}function Q(t,r,i){let e=r.getSourceFileOrThrow(t);e.forEachDescendant(n=>{if(n.getKind()===l.ImportKeyword){let a=n.getParent();if(a&&a.getKind()===l.CallExpression){let{line:s,column:o}=e.getLineAndColumnAtPos(n.getStart());i.push({path:t,line:s,column:o,message:"dynamic import() is not allowed in the sandbox"})}}if(n.getKind()===l.NewExpression){let a=n.getText();if(/^new\s+Function\s*\(/.test(a)){let{line:s,column:o}=e.getLineAndColumnAtPos(n.getStart());i.push({path:t,line:s,column:o,message:"new Function(...) is not allowed in the sandbox"})}}})}function Z(t,r,i){let e=r.getSourceFileOrThrow(t),n=(s,o)=>{let{line:c,column:u}=e.getLineAndColumnAtPos(s.getStart());i.push({path:t,line:c,column:u,message:o})},a=s=>{let o=s.trim();return o.startsWith('"')&&o.endsWith('"')||o.startsWith("'")&&o.endsWith("'")||o.startsWith("`")&&o.endsWith("`")?o.slice(1,-1):null};e.forEachDescendant(s=>{let o=s.getKind();if(o===l.PropertyAccessExpression){let c=s.getName?.();if(!c)return;if(c==="constructor"){n(s,"`.constructor` access is denied in the sandbox (Function-constructor smuggle vector)");return}if(S.has(c)){n(s,`\`.${c}\` access is denied in the sandbox (FS/network/eval surface) \u2014 accessing a denied global via property syntax was the property-access bypass closed in v0.3.0`);return}}if(o===l.ElementAccessExpression){let c=s.getExpression?.(),u=s.getArgumentExpression?.(),g=c?.getText()??"",f=u?.getText()??"",p=a(f);if(g==="globalThis"&&p!==null){n(s,"bracket-access on `globalThis` with a string literal is denied in the sandbox (use direct identifier reference for allowlisted names)");return}if(p!==null&&(p==="constructor"||S.has(p))){n(s,`bracket-access \`["${p}"]\` is denied in the sandbox (would reach a denied name)`);return}}if(o===l.CallExpression){let u=s.getExpression?.()?.getText()??"";if(u==="Function"){n(s,"`Function(...)` call is denied in the sandbox");return}let g=u.match(/^(Reflect|Object)\.(\w+)$/);if(g){let p=s.getArguments?.()??[];if(p.length>=2){let w=p[0].getText(),d=a(p[1].getText());new Set(["get","has","getOwnPropertyDescriptor","getOwnPropertyDescriptors","ownKeys","getPrototypeOf"]).has(g[2])&&(w==="globalThis"||N.has(w))&&d!==null&&(d==="constructor"||S.has(d))&&n(s,`\`${g[1]}.${g[2]}(${w}, "${d}")\` would reach a denied name`)}}}})}function ee(t,r,i){let e=r.getSourceFileOrThrow(t),n=new Set;for(let a of e.getImportDeclarations()){for(let c of a.getNamedImports())n.add(c.getName());let s=a.getDefaultImport();s&&n.add(s.getText());let o=a.getNamespaceImport();o&&n.add(o.getText())}for(let a of e.getVariableDeclarations())n.add(a.getName());for(let a of e.getFunctions()){let s=a.getName();s&&n.add(s)}for(let a of e.getClasses()){let s=a.getName();s&&n.add(s)}e.forEachDescendant(a=>{if(a.getKind()!==l.Identifier)return;let s=a.getText();if(n.has(s)||N.has(s))return;let o=a.getParent();if(o){let c=o.getKind();if(c===l.PropertyAssignment&&o.getNameNode?.()===a||c===l.PropertyAccessExpression&&o.getNameNode?.()===a||c===l.MethodDeclaration||c===l.ImportSpecifier||c===l.ExportSpecifier||c===l.NamespaceImport||c===l.ImportClause||c===l.Parameter||c===l.TypeReference||c===l.TypeQuery)return}if(S.has(s)){let{line:c,column:u}=e.getLineAndColumnAtPos(a.getStart());i.push({path:t,line:c,column:u,message:`identifier "${s}" is denied in the sandbox (FS/network/eval surface)`})}})}function _(t){let r=[],i=new X({useInMemoryFileSystem:!0,compilerOptions:{target:99,module:99,allowJs:!0,strict:!1}});for(let e of t)i.createSourceFile(e.path,e.source,{overwrite:!0});for(let e of t)J(e.path,i,r),Q(e.path,i,r),Z(e.path,i,r),ee(e.path,i,r);return r}var O=2e5,R=10,E="src/main.ts";function te(t){if(t.files&&t.source)throw new m("pass either `source` or `files`, not both","input-invalid");if(!t.files&&!t.source)throw new m("must pass either `source` or `files`","input-invalid");let r=t.files?t.files:[{path:E,source:t.source??""}];if(r.length===0)throw new m("files array is empty","input-invalid");if(r.length>R)throw new m(`payload exceeds ${R} files`,"input-invalid");let i=0;for(let e of r){if(typeof e.path!="string"||e.path.length===0)throw new m("every file must have a non-empty path","input-invalid");if(typeof e.source!="string"||e.source.length===0)throw new m(`file "${e.path}" has empty source`,"input-invalid");i+=Buffer.byteLength(e.source,"utf8")}if(i>O)throw new m(`total payload is ${i} bytes (max ${O})`,"input-invalid");if(!r.some(e=>e.path===E))throw new m(`payload must include "${E}" \u2014 the runner entry point`,"input-invalid");return r}async function xe(t){let r;try{r=te(t)}catch(n){if(n instanceof m)return{logs:[],facts:{},errors:[n.message],durationMs:0,timedOut:!1};throw n}let i=_(r);if(i.length>0)return{logs:[],facts:{},errors:i.map(n=>`${n.path}:${n.line}:${n.column} \u2014 ${n.message}`),durationMs:0,timedOut:!1};let e;try{e=await v(r)}catch(n){if(n instanceof h)return{logs:[],facts:{},errors:[n.message],durationMs:0,timedOut:!1};throw n}try{return await k({bundledSource:e.source,timeoutMs:t.timeoutMs})}catch(n){if(n instanceof x)return{logs:[],facts:{},errors:[n.message],durationMs:0,timedOut:n.code==="timeout"};throw n}}export{m as SandboxError,xe as runInSandbox};
5
5
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/bundler.ts","../src/host.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 { build } from \"esbuild\";\nimport type { PlaygroundFile } from \"./types.js\";\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 external: [\n \"@directive-run/core\",\n \"@directive-run/ai\",\n \"@directive-run/query\",\n ],\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 // External — let esbuild's external list handle it.\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 { mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { 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 // Use createRequire instead of `import.meta.resolve` — Vitest's SSR\n // shim of import.meta exposes a `resolve` that isn't callable, and\n // `createRequire` works in both real Node ESM and Vitest dev mode.\n // The worker subpath export points at dist/worker.js; CJS resolution\n // picks up the \"default\" condition we set in package.json.\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 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 * Resolve the directory the sandbox package's own dist sits in. The\n * temp bundle has to live somewhere Node's ESM resolver can walk UP\n * from to find `@directive-run/core` in node_modules; the sandbox\n * package's dist/ has that path because sandbox declares core as a\n * dependency. Writing the bundle next to dist/worker.js inherits the\n * same resolution chain.\n */\nasync function getSandboxPackageDir(): Promise<string> {\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. Bare specifiers like `@directive-run/core`\n * don't resolve from `data:` URLs (no hierarchical base for\n * node_modules lookup) AND don't resolve from /tmp (no node_modules\n * to walk up to). Writing inside the sandbox package directory\n * inherits its node_modules chain.\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 sandboxDir = await getSandboxPackageDir();\n const dir = mkdtempSync(join(sandboxDir, \".sandbox-tmp-\"));\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 };\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 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 * 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 * 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\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\nconst ALLOWED_IMPORT_PATTERNS: RegExp[] = [\n /^@directive-run\\/(core|ai|query)(\\/.+)?$/,\n /^\\.\\/.+\\.js$/,\n /^\\.\\.\\/.+\\.js$/,\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 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 const allowed = ALLOWED_IMPORT_PATTERNS.some((re) =>\n re.test(moduleSpecifier),\n );\n if (!allowed) {\n const { line, column } = sourceFile.getLineAndColumnAtPos(\n decl.getStart(),\n );\n errors.push({\n path: fileLabel,\n line,\n column,\n message: `import \"${moduleSpecifier}\" is not allowed in the sandbox. Allowed: @directive-run/{core,ai,query} or relative \"./X.js\" paths.`,\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\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 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 { 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 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 errors: validationErrors.map(\n (e) => `${e.path}:${e.line}:${e.column} — ${e.message}`,\n ),\n durationMs: 0,\n timedOut: false,\n };\n }\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 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 timeoutMs: input.timeoutMs,\n });\n return result;\n } catch (err) {\n if (err instanceof WorkerExecError) {\n return {\n logs: [],\n facts: {},\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":["build","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","mkdtempSync","rmSync","writeFileSync","dirname","join","pathToFileURL","Worker","MIN_TIMEOUT_MS","MAX_TIMEOUT_MS","DEFAULT_TIMEOUT_MS","resolveWorkerPath","createRequire","WorkerExecError","message","code","clampTimeout","value","raw","getSandboxPackageDir","workerPath","writeBundleToTemp","bundledSource","sandboxDir","dir","bundlePath","execInWorker","input","timeoutMs","cleanupTempDir","worker","timer","timedOut","startMs","resolve","reject","settled","msg","err","SandboxError","message","code","Project","SyntaxKind","ALLOWED_IMPORT_PATTERNS","ALLOWED_GLOBALS","DENIED_GLOBALS","checkImports","fileLabel","project","errors","sourceFile","decl","moduleSpecifier","re","line","column","checkDynamicImportsAndCalls","node","parent","text","checkGlobalIdentifiers","localBindings","n","def","ns","v","f","name","c","parentKind","validateSandboxInput","files","file","MAX_PAYLOAD_BYTES","MAX_FILES","MAIN_PATH","normalizeInput","input","SandboxError","files","totalBytes","file","f","runInSandbox","err","validationErrors","validateSandboxInput","e","bundled","bundleSandboxFiles","BundleError","execInWorker","WorkerExecError"]}
1
+ {"version":3,"sources":["../src/bundler.ts","../src/host.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 { build } from \"esbuild\";\nimport type { PlaygroundFile } from \"./types.js\";\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 // Wildcard pattern lets ANY @directive-run/* import the validator\n // permits resolve at worker runtime against the worker's\n // node_modules. The validator gates which packages can be\n // imported in the first place; the bundler just doesn't try to\n // inline them.\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 // External — let esbuild's external list handle it.\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 { mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { 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 // Use createRequire instead of `import.meta.resolve` — Vitest's SSR\n // shim of import.meta exposes a `resolve` that isn't callable, and\n // `createRequire` works in both real Node ESM and Vitest dev mode.\n // The worker subpath export points at dist/worker.js; CJS resolution\n // picks up the \"default\" condition we set in package.json.\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 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 * Resolve the directory the sandbox package's own dist sits in. The\n * temp bundle has to live somewhere Node's ESM resolver can walk UP\n * from to find `@directive-run/core` in node_modules; the sandbox\n * package's dist/ has that path because sandbox declares core as a\n * dependency. Writing the bundle next to dist/worker.js inherits the\n * same resolution chain.\n */\nasync function getSandboxPackageDir(): Promise<string> {\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. Bare specifiers like `@directive-run/core`\n * don't resolve from `data:` URLs (no hierarchical base for\n * node_modules lookup) AND don't resolve from /tmp (no node_modules\n * to walk up to). Writing inside the sandbox package directory\n * inherits its node_modules chain.\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 sandboxDir = await getSandboxPackageDir();\n const dir = mkdtempSync(join(sandboxDir, \".sandbox-tmp-\"));\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 };\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 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 * 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 * 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\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]);\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 { 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 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 errors: validationErrors.map(\n (e) => `${e.path}:${e.line}:${e.column} — ${e.message}`,\n ),\n durationMs: 0,\n timedOut: false,\n };\n }\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 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 timeoutMs: input.timeoutMs,\n });\n return result;\n } catch (err) {\n if (err instanceof WorkerExecError) {\n return {\n logs: [],\n facts: {},\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":["build","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","mkdtempSync","rmSync","writeFileSync","dirname","join","pathToFileURL","Worker","MIN_TIMEOUT_MS","MAX_TIMEOUT_MS","DEFAULT_TIMEOUT_MS","resolveWorkerPath","createRequire","WorkerExecError","message","code","clampTimeout","value","raw","getSandboxPackageDir","workerPath","writeBundleToTemp","bundledSource","sandboxDir","dir","bundlePath","execInWorker","input","timeoutMs","cleanupTempDir","worker","timer","timedOut","startMs","resolve","reject","settled","msg","err","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","bundled","bundleSandboxFiles","BundleError","execInWorker","WorkerExecError"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directive-run/sandbox",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",