import { Effect, Option, Predicate, Schema } from "effect"; import type { SandboxToolInvoker } from "@executor-js/codemode-core"; import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs"; import { InputValidationError, OutputValidationError, ToolSandboxError, type CollectResult, type CollectRequest, type HandleBridge, type InvokeRequest, type InvokeResult, type ValidationIssue, 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/integration), // then executes the bundle so `module.exports.default` is the artifact. A // driver appended after the bundle either collects the descriptor (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`. // The old `db` handle is intentionally unavailable in v1; handlers that still // try it get a clear error before any host storage call exists. // 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` records its def so the driver can read // it back. Clients are built by `__mkHandle`. const runtimePrelude = ` var __modules = {}; var __defs = { tool: 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); } }); } function __unavailableStorage() { return new Proxy({}, { get: function() { return function() { throw new Error('storage is not available yet'); }; }, apply: function() { throw new Error('storage is not available yet'); } }); } var __executorApp = { integration: function(integration) { return { integration: integration }; }, defineTool: function(def) { __defs.tool = def; return def; }, }; function __require(id) { if (id === 'executor:app') return __executorApp; 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 defineTool). `require` is our shim; `defineTool` // also records 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); `; const decodeJsonMarker = Schema.decodeUnknownOption(Schema.UnknownFromJsonString); const isRecord = (value: unknown): value is Record => value !== null && typeof value === "object" && !Array.isArray(value); const parseMarker = (message: string): unknown => { const parse = (text: string): unknown => Option.getOrNull(decodeJsonMarker(text)); const direct = parse(message); if (direct) return direct; const firstBrace = message.indexOf("{"); const lastBrace = message.lastIndexOf("}"); if (firstBrace !== -1 && lastBrace > firstBrace) { return parse(message.slice(firstBrace, lastBrace + 1)); } return null; }; const isMarker = (value: unknown, tag: string): value is Record => { if (!isRecord(value)) return false; return Predicate.isTagged(tag)(value); }; const markerMessage = (marker: Record, fallback: string): string => typeof marker.message === "string" ? marker.message : fallback; const markerIssues = (marker: Record): readonly ValidationIssue[] => Array.isArray(marker.issues) ? marker.issues.filter(isValidationIssue) : []; const isValidationIssue = (value: unknown): value is ValidationIssue => isRecord(value) && typeof value.message === "string" && (value.path === undefined || Array.isArray(value.path)); // Collect driver: describe the artifact's integrations + input/output schema. // Deterministic JSON only — no handler execution. const collectDriver = (artifact: string): string => ` return await (async () => { var artifactName = ${JSON.stringify(artifact)}; var fail = function(tag, data) { var payload = data || {}; payload._tag = tag; throw JSON.stringify(payload); }; var sanitizeJson = function(value) { if (value === undefined) return undefined; if (value === null || typeof value !== 'object') return value; if (Array.isArray(value)) return value.map(sanitizeJson); var out = {}; for (var key in value) { if (key === '~standard') continue; if (typeof value[key] === 'function' || value[key] === undefined) continue; out[key] = sanitizeJson(value[key]); } return out; }; var schemaToJson = function(field, schema, direction) { if (schema === undefined) return undefined; if (schema && typeof schema === 'object' && schema['~standard']) { var standard = schema['~standard']; var vendor = standard && standard.vendor ? String(standard.vendor) : 'unknown'; var jsonSchema = standard && standard.jsonSchema; if (!jsonSchema || typeof jsonSchema[direction] !== 'function') { fail('SchemaExportError', { tool: artifactName, field: field, vendor: vendor, message: 'tool "' + artifactName + '" ' + field + ' schema from "' + vendor + '" is not supported for schema export', }); } return sanitizeJson(jsonSchema[direction]({ target: 'draft-07' })); } if (schema && typeof schema === 'object' && !Array.isArray(schema)) { return sanitizeJson(schema); } fail('SchemaExportError', { tool: artifactName, field: field, vendor: 'raw', message: 'tool "' + artifactName + '" ' + field + ' schema must be a Standard Schema or raw JSON Schema object', }); }; var def = __defs.tool || (globalThis.__artifact && (globalThis.__artifact.default || globalThis.__artifact)); if (def && typeof def.handler === 'function') { var handlerSource = Function.prototype.toString.call(def.handler); if (/\\bdb\\b/.test(handlerSource)) { fail('StorageUnavailableError', { tool: artifactName, field: 'handler', message: 'tool "' + artifactName + '" uses storage, but storage is not available yet', }); } } var integrations = {}; if (def && def.integrations) { for (var k in def.integrations) { var c = def.integrations[k]; integrations[k] = { integration: c && c.integration ? String(c.integration) : '' }; } } return { artifact: artifactName, kind: 'tool', description: def && def.description, integrations: integrations, annotations: def && def.annotations, hasHandler: !!(def && def.handler), inputJsonSchema: def ? schemaToJson('input', def.input, 'input') : undefined, outputJsonSchema: def && def.output !== undefined ? schemaToJson('output', def.output, 'output') : undefined, }; })() `; const buildCollectCode = (bundle: string, request?: CollectRequest): string => runtimePrelude + wrapBundle(bundle) + collectDriver(request?.artifact ?? "default"); // Invoke driver: build injected clients from the request roots, validate // Standard Schema input/output inside the sandbox, then call the artifact's // handler with (input, injected). 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 failValidation = function(tag, field, issues) { throw JSON.stringify({ _tag: tag, tool: '${request.artifact}', field: field, message: 'tool "${request.artifact}" ' + field + ' validation failed', issues: JSON.parse(JSON.stringify(issues || [])), }); }; var validateWithStandardSchema = async function(field, schema, value) { if (!schema || typeof schema !== 'object' || !schema['~standard']) return value; var standard = schema['~standard']; if (!standard || typeof standard.validate !== 'function') return value; var result = await standard.validate(value); if (result && result.issues) { failValidation(field === 'input' ? 'InputValidationError' : 'OutputValidationError', field, result.issues); } return result && Object.prototype.hasOwnProperty.call(result, 'value') ? result.value : value; }; var roots = ${rootsLiteral}; var injected = {}; for (var name in roots) { var spec = roots[name]; if (spec.kind !== 'single') throw new Error('unsupported handle root kind: ' + spec.kind); injected[name] = __mkHandle(name, []); } injected.db = __unavailableStorage(); var input = await validateWithStandardSchema('input', def.input, ${inputLiteral}); var out = await def.handler(input, injected); out = await validateWithStandardSchema('output', def.output, out); 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" }), ), }; 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 = (code: string): Effect.Effect => collectExecutor.execute(code, collectInvoker).pipe( Effect.mapError( (cause) => new ToolSandboxError({ kind: "collect", message: "collect run failed", cause }), ), Effect.flatMap((result) => { if (result.error) { const marker = parseMarker(result.error); if ( isMarker(marker, "SchemaExportError") || isMarker(marker, "StorageUnavailableError") ) { return Effect.fail( new ToolSandboxError({ kind: "collect", message: markerMessage(marker, result.error), cause: marker, }), ); } return Effect.fail(new ToolSandboxError({ kind: "collect", message: result.error })); } return Effect.succeed(result.result); }), ); return { collect: (bundle: string, request?: CollectRequest) => 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(buildCollectCode(bundle, request)); const second = yield* runCollect(buildCollectCode(bundle, request)); 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 { artifact?: string }; const result: CollectResult = { artifacts: { [String(descriptor.artifact ?? "default")]: { kind: "tool", 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}`, }), ); } 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" }), ); } 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) { const marker = parseMarker(result.error); if (isMarker(marker, "InputValidationError")) { return yield* new InputValidationError({ message: markerMessage(marker, "input validation failed"), issues: markerIssues(marker), }); } if (isMarker(marker, "OutputValidationError")) { return yield* new OutputValidationError({ message: markerMessage(marker, "output validation failed"), issues: markerIssues(marker), }); } return yield* new ToolSandboxError({ kind: "invoke", message: result.error }); } return { output: result.result, logs: result.logs ?? [] } satisfies InvokeResult; }), }; };