import { Effect } from "effect"; import type { SandboxToolInvoker } from "@executor-js/codemode-core"; import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs"; import { ToolSandboxError, type CollectResult, type HandleBridge, type InvokeRequest, type InvokeResult, type ToolSandbox, } from "../seams/tool-sandbox"; import { stableStringify } from "../pipeline/descriptor"; // --------------------------------------------------------------------------- // QuickJS-backed ToolSandbox (self-hosted). // // The published bundle is a CJS string. The sandbox body prepends a `require` // shim providing `executor:app` (defineTool/defineWorkflow/connection/ // connections/catalog) and `executor:ui` stubs, then executes the bundle so // `module.exports.default` is the artifact. A driver appended after the bundle // either collects descriptors (nothing bound) or runs one handler with injected // clients. // // Injected clients are Proxies that turn `github.issues.listForRepo(args)` into // `await __invokeTool("__handle__", { root, path, args })` — the ONE bridge the // QuickJS runtime already provides (`tools`/`__invokeTool`). Our // `SandboxToolInvoker` decodes that and forwards to the host `HandleBridge`. // Everything crossing is JSON (the cloud version is RPC), so the interface // stays honest. // // Determinism: `collect` runs the collection body twice and byte-compares the // descriptor JSON. Effectful top-levels (Math.random, Date.now) diverge and are // rejected. QuickJS denies `fetch` and enforces a deadline + memory cap. // --------------------------------------------------------------------------- const COLLECT_TIMEOUT_MS = 10_000; const INVOKE_TIMEOUT_MS = 30_000; // The shim + module system injected before the bundle. Kept as a plain string // (QuickJS evals a string). `defineTool`/`defineWorkflow` record their def so // the driver can read it back. Clients are built by `__mkHandle`. const runtimePrelude = ` var __modules = {}; var __defs = { tool: null, workflow: null, ui: null }; function __recordDefault(mod) { return mod; } var __handleBridge = function(root, path, args) { // Route every injected-client method call through the single tool bridge. return tools.__handle__({ root: root, path: path, args: args }); }; function __mkHandle(root, prefix) { var target = function(){}; return new Proxy(target, { get: function(_t, prop) { if (prop === 'then' || typeof prop === 'symbol') return undefined; return __mkHandle(root, prefix.concat([String(prop)])); }, apply: function(_t, _this, callArgs) { return __handleBridge(root, prefix, callArgs); } }); } var __executorApp = { connection: function(integration, opts) { return { __decl: 'single', integration: integration, description: opts && opts.description }; }, connections: function(integration, opts) { return { __decl: 'array', integration: integration, description: opts && opts.description }; }, catalog: function() { return { __decl: 'catalog' }; }, defineTool: function(def) { __defs.tool = def; return def; }, defineWorkflow: function(def) { __defs.workflow = def; return def; }, }; var __executorUi = { config: function(){ return undefined; }, useQuery: function(){ return { data: [], isLoading: false, refetch: function(){} }; }, useTool: function(){ return { run: function(){ return Promise.resolve(); }, isRunning: false }; }, }; function __require(id) { if (id === 'executor:app') return __executorApp; if (id === 'executor:ui') return __executorUi; if (id === 'executor:ui/components') return {}; throw new Error('module not available in sandbox: ' + id); } `; // Run the CJS bundle. The bundle's virtual entry sets `globalThis.__artifact` // (the def object returned by define*), `globalThis.__zodToJson` and // `__zodToJsonOutput` (bound to the author's inlined zod). `require` is our // shim; `defineTool`/`defineWorkflow` also record into `__defs` as a fallback. const wrapBundle = (bundle: string): string => ` var module = { exports: {} }; var exports = module.exports; var require = __require; (function(module, exports, require){ ${bundle} })(module, exports, require); `; // Collect driver: describe the artifact's connections + input/output schema. // Deterministic JSON only — no handler execution. const collectDriver = ` return await (async () => { var def = __defs.tool || __defs.workflow || (globalThis.__artifact && (globalThis.__artifact.default || globalThis.__artifact)); var kind = __defs.workflow ? 'workflow' : 'tool'; var conns = {}; if (def && def.connections) { for (var k in def.connections) { var c = def.connections[k]; conns[k] = { decl: c && c.__decl ? c.__decl : 'single', integration: c && c.integration, description: c && c.description }; } } var toJson = (typeof globalThis.__zodToJson === 'function') ? globalThis.__zodToJson : function(){ return undefined; }; var toJsonOut = (typeof globalThis.__zodToJsonOutput === 'function') ? globalThis.__zodToJsonOutput : function(){ return undefined; }; return { kind: kind, description: def && def.description, connections: conns, annotations: def && def.annotations, schedule: def && def.schedule, hasHandler: !!(def && (def.handler || def.run)), inputJsonSchema: def ? toJson(def.input) : undefined, outputJsonSchema: def ? toJsonOut(def.output) : undefined, }; })() `; const buildCollectCode = (bundle: string): string => runtimePrelude + wrapBundle(bundle) + collectDriver; // Invoke driver: build injected clients from the request roots, then call the // artifact's handler with (input, injected). Fan-out arrays become arrays of // element handles. const buildInvokeDriver = (request: InvokeRequest): string => { const rootsLiteral = JSON.stringify(request.roots); const inputLiteral = JSON.stringify(request.input ?? {}); return ` return await (async () => { var def = __defs.tool || (globalThis.__artifact && (globalThis.__artifact.default || globalThis.__artifact)); if (!def || typeof def.handler !== 'function') throw new Error('artifact has no handler: ${request.artifact}'); var roots = ${rootsLiteral}; var injected = {}; for (var name in roots) { var spec = roots[name]; if (spec.kind === 'array') { var arr = []; for (var i = 0; i < spec.count; i++) arr.push(__mkHandle(name + '#' + i, [])); injected[name] = arr; } else { injected[name] = __mkHandle(name, []); } } var out = await def.handler(${inputLiteral}, injected); return out; })() `; }; const buildInvokeCode = (bundle: string, request: InvokeRequest): string => runtimePrelude + wrapBundle(bundle) + buildInvokeDriver(request); // A no-op invoker for collect: no handle calls should happen; if they do // (misbehaving describe path), fail loudly. const collectInvoker: SandboxToolInvoker = { invoke: () => Effect.fail( new ToolSandboxError({ kind: "collect", message: "collect must not make handle calls" }), ) as never, }; export interface QuickjsToolSandboxOptions { readonly collectTimeoutMs?: number; readonly invokeTimeoutMs?: number; } export const makeQuickjsToolSandbox = (options: QuickjsToolSandboxOptions = {}): ToolSandbox => { const collectExecutor = makeQuickJsExecutor({ timeoutMs: options.collectTimeoutMs ?? COLLECT_TIMEOUT_MS, }); const invokeExecutor = makeQuickJsExecutor({ timeoutMs: options.invokeTimeoutMs ?? INVOKE_TIMEOUT_MS, }); const runCollect = (bundle: string): Effect.Effect => collectExecutor.execute(buildCollectCode(bundle), collectInvoker).pipe( Effect.mapError( (cause) => new ToolSandboxError({ kind: "collect", message: "collect run failed", cause }), ), Effect.flatMap((result) => { if (result.error) { return Effect.fail(new ToolSandboxError({ kind: "collect", message: result.error })); } return Effect.succeed(result.result); }), ); return { collect: (bundle: string) => Effect.gen(function* () { // Run twice, byte-compare (determinism gate). Key-sorted stringify so a // false mismatch never comes from property-order luck — a real // divergence (Math.random / Date.now) still fails. const first = yield* runCollect(bundle); const second = yield* runCollect(bundle); const a = stableStringify(first); const b = stableStringify(second); if (a !== b) { return yield* new ToolSandboxError({ kind: "nondeterministic", message: "descriptor collection is non-deterministic (an artifact read Math.random/Date.now or otherwise diverged between runs)", }); } const descriptor = first as { kind?: string }; const kind = descriptor?.kind === "workflow" ? "workflow" : "tool"; const result: CollectResult = { artifacts: { [String((descriptor as { artifact?: string }).artifact ?? "default")]: { kind, descriptor: first, }, }, }; return result; }), invoke: (bundle: string, request: InvokeRequest, bridge: HandleBridge) => Effect.gen(function* () { // The invoker decodes the routed handle call and forwards to the host // bridge. Path 0 is `__handle__`; the single arg is {root, path, args}. const invoker: SandboxToolInvoker = { invoke: (input: { path: string; args: unknown }) => { // Strictness (grafted from A): the ONLY reserved bridge path the // invoke phase accepts is `__handle__`. Anything else is a hard // error, never silently ignored — a handler must not reach the host // through an unexpected channel. if (input.path !== "__handle__") { return Effect.fail( new ToolSandboxError({ kind: "invoke", message: `unexpected sandbox bridge path: ${input.path}`, }), ) as never; } const call = input.args as { root: string; path: readonly string[]; args: readonly unknown[]; }; if (!call || typeof call.root !== "string" || !Array.isArray(call.path)) { return Effect.fail( new ToolSandboxError({ kind: "invoke", message: "malformed sandbox bridge call" }), ) as never; } return bridge.call({ root: call.root, path: call.path, args: call.args }) as never; }, }; const result = yield* invokeExecutor .execute(buildInvokeCode(bundle, request), invoker) .pipe( Effect.mapError( (cause) => new ToolSandboxError({ kind: "invoke", message: "invoke run failed", cause }), ), ); if (result.error) { return yield* new ToolSandboxError({ kind: "invoke", message: result.error }); } return { output: result.result, logs: result.logs ?? [] } satisfies InvokeResult; }), }; };