@workerdeck/sandbox 0.22.0 → 1.0.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/build/index.d.mts +5 -37
- package/build/index.mjs +4 -14
- package/build/index.mjs.map +1 -1
- package/package.json +1 -1
package/build/index.d.mts
CHANGED
|
@@ -1,38 +1,19 @@
|
|
|
1
1
|
import { QuickJSAsyncVariant, QuickJSAsyncWASMModule } from "quickjs-emscripten-core";
|
|
2
2
|
|
|
3
3
|
//#region src/vfs.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* Per-call in-memory scratch filesystem. Seeded from the task's documents,
|
|
6
|
-
* discarded after the call — never backed by host paths. The guest only ever
|
|
7
|
-
* touches it through the by-value host bridge (vfs_read/vfs_write/vfs_list).
|
|
8
|
-
*
|
|
9
|
-
* A plain path→content map on purpose: this package must run unpolyfilled in
|
|
10
|
-
* the browser (the tab-side tool host seeds a VFS per bridged call), and a
|
|
11
|
-
* node-flavored fs emulation drags `node:buffer` in with it.
|
|
12
|
-
*/
|
|
13
4
|
type SandboxVfs = {
|
|
14
5
|
read(path: string): string | undefined;
|
|
15
|
-
write(path: string, content: string): void;
|
|
16
|
-
list(dir?: string): string[];
|
|
6
|
+
write(path: string, content: string): void;
|
|
7
|
+
list(dir?: string): string[];
|
|
17
8
|
snapshot(): Record<string, string>;
|
|
18
9
|
};
|
|
19
|
-
/** Collapse '.', '..' and empty segments into a rooted absolute path — the VFS
|
|
20
|
-
* has no host backing to escape into, this is pure path hygiene. */
|
|
21
10
|
declare function normalizeVfsPath(path: string): string;
|
|
22
11
|
declare function createVfs(seed?: Record<string, string>): SandboxVfs;
|
|
23
12
|
//#endregion
|
|
24
13
|
//#region src/run-script.d.ts
|
|
25
|
-
/**
|
|
26
|
-
* The guest engine, loaded from an injected WASM variant so server (Node
|
|
27
|
-
* asyncify) and browser (singlefile asyncify) share this package unchanged.
|
|
28
|
-
* Load once and reuse — the module is stateless; runtimes/contexts are per call.
|
|
29
|
-
*/
|
|
30
14
|
type SandboxEngine = {
|
|
31
15
|
module: QuickJSAsyncWASMModule;
|
|
32
16
|
};
|
|
33
|
-
/** Accepts the variant itself, a `{ default }` module namespace, or a promise of
|
|
34
|
-
* either — so callers can pass `import('...')` or a plain default import without
|
|
35
|
-
* caring how their bundler/runtime resolved the interop. */
|
|
36
17
|
type SandboxVariantInput = QuickJSAsyncVariant | {
|
|
37
18
|
default: QuickJSAsyncVariant;
|
|
38
19
|
} | Promise<QuickJSAsyncVariant | {
|
|
@@ -44,19 +25,11 @@ type SandboxLog = {
|
|
|
44
25
|
text: string;
|
|
45
26
|
};
|
|
46
27
|
type RunScriptOptions = {
|
|
47
|
-
|
|
28
|
+
script: string;
|
|
48
29
|
vfs?: SandboxVfs;
|
|
49
|
-
|
|
50
|
-
* Host-gated network capability, exposed to the guest as `fetchText(url)`.
|
|
51
|
-
* The CALLER owns the allowlist, credential injection, and a per-call
|
|
52
|
-
* timeout — the guest never holds a credential and this module never
|
|
53
|
-
* touches the network itself. Unset = `fetchText` throws in the guest.
|
|
54
|
-
*/
|
|
55
|
-
fetchText?: (url: string) => Promise<string>; /** QuickJS allocator cap. Guest OOM is a failed result, not a host crash. Default 64 MiB. */
|
|
30
|
+
fetchText?: (url: string) => Promise<string>;
|
|
56
31
|
memoryLimitBytes?: number;
|
|
57
|
-
|
|
58
|
-
* Does NOT cover time inside host functions — bound those caller-side. Default 5000. */
|
|
59
|
-
timeoutMs?: number; /** Guest stack cap. Default 1 MiB. */
|
|
32
|
+
timeoutMs?: number;
|
|
60
33
|
maxStackSizeBytes?: number;
|
|
61
34
|
signal?: AbortSignal;
|
|
62
35
|
};
|
|
@@ -70,11 +43,6 @@ type RunScriptResult = {
|
|
|
70
43
|
error: string;
|
|
71
44
|
logs: SandboxLog[];
|
|
72
45
|
};
|
|
73
|
-
/**
|
|
74
|
-
* Evaluate one untrusted script in a fresh QuickJS context: deny-by-default
|
|
75
|
-
* (no ambient fs/network/host access — only the granted bridge), interpreter-
|
|
76
|
-
* enforced memory and time limits, everything disposed afterwards.
|
|
77
|
-
*/
|
|
78
46
|
declare function runScript(engine: SandboxEngine, options: RunScriptOptions): Promise<RunScriptResult>;
|
|
79
47
|
//#endregion
|
|
80
48
|
export { type RunScriptOptions, type RunScriptResult, type SandboxEngine, type SandboxLog, type SandboxVariantInput, type SandboxVfs, createVfs, loadEngine, normalizeVfsPath, runScript };
|
package/build/index.mjs
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { newQuickJSAsyncWASMModuleFromVariant } from "quickjs-emscripten-core";
|
|
2
2
|
//#region src/vfs.ts
|
|
3
|
-
/** Collapse '.', '..' and empty segments into a rooted absolute path — the VFS
|
|
4
|
-
* has no host backing to escape into, this is pure path hygiene. */
|
|
5
3
|
function normalizeVfsPath(path) {
|
|
6
4
|
const out = [];
|
|
7
5
|
for (const part of path.split("/")) {
|
|
@@ -36,13 +34,6 @@ function createVfs(seed) {
|
|
|
36
34
|
}
|
|
37
35
|
//#endregion
|
|
38
36
|
//#region src/run-script.ts
|
|
39
|
-
async function loadEngine(variant) {
|
|
40
|
-
const resolved = await variant;
|
|
41
|
-
return { module: await newQuickJSAsyncWASMModuleFromVariant("default" in resolved ? resolved.default : resolved) };
|
|
42
|
-
}
|
|
43
|
-
/** Trusted prelude, evaluated before the untrusted script: ergonomic frozen
|
|
44
|
-
* wrappers over the raw host functions. Everything crossing the boundary is a
|
|
45
|
-
* string (by-value marshalling — never a host object reference). */
|
|
46
37
|
const PRELUDE = `
|
|
47
38
|
"use strict";
|
|
48
39
|
(() => {
|
|
@@ -64,11 +55,10 @@ const PRELUDE = `
|
|
|
64
55
|
globalThis.fetchText = (u) => __host_fetch_text(String(u))
|
|
65
56
|
})();
|
|
66
57
|
`;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
*/
|
|
58
|
+
async function loadEngine(variant) {
|
|
59
|
+
const resolved = await variant;
|
|
60
|
+
return { module: await newQuickJSAsyncWASMModuleFromVariant("default" in resolved ? resolved.default : resolved) };
|
|
61
|
+
}
|
|
72
62
|
async function runScript(engine, options) {
|
|
73
63
|
const logs = [];
|
|
74
64
|
const deadline = Date.now() + (options.timeoutMs ?? 5e3);
|
package/build/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/vfs.ts","../src/run-script.ts"],"sourcesContent":["/**\n * Per-call in-memory scratch filesystem. Seeded from the task's documents,\n * discarded after the call — never backed by host paths. The guest only ever\n * touches it through the by-value host bridge (vfs_read/vfs_write/vfs_list).\n *\n * A plain path→content map on purpose: this package must run unpolyfilled in\n * the browser (the tab-side tool host seeds a VFS per bridged call), and a\n * node-flavored fs emulation drags `node:buffer` in with it.\n */\nexport type SandboxVfs = {\n read(path: string): string | undefined\n write(path: string, content: string): void\n /** File paths under `dir` (recursive), sorted. */\n list(dir?: string): string[]\n /** Full path → content map (e.g. to collect results after a run). */\n snapshot(): Record<string, string>\n}\n\n/** Collapse '.', '..' and empty segments into a rooted absolute path — the VFS\n * has no host backing to escape into, this is pure path hygiene. */\nexport function normalizeVfsPath(path: string): string {\n const out: string[] = []\n for (const part of path.split('/')) {\n if (part === '' || part === '.') continue\n if (part === '..') {\n out.pop()\n continue\n }\n out.push(part)\n }\n return '/' + out.join('/')\n}\n\nexport function createVfs(seed?: Record<string, string>): SandboxVfs {\n const files = new Map<string, string>()\n const write = (path: string, content: string): void => {\n files.set(normalizeVfsPath(path), content)\n }\n for (const [path, content] of Object.entries(seed ?? {})) write(path, content)\n return {\n read(path) {\n return files.get(normalizeVfsPath(path))\n },\n write,\n list(dir = '/') {\n const prefix = normalizeVfsPath(dir)\n return [...files.keys()]\n .filter((file) => prefix === '/' || file === prefix || file.startsWith(prefix + '/'))\n .sort()\n },\n snapshot() {\n return Object.fromEntries(files)\n },\n }\n}\n","import {\n newQuickJSAsyncWASMModuleFromVariant,\n type QuickJSAsyncVariant,\n type QuickJSAsyncWASMModule,\n type QuickJSHandle,\n} from 'quickjs-emscripten-core'\nimport type { SandboxVfs } from './vfs.ts'\n\n/**\n * The guest engine, loaded from an injected WASM variant so server (Node\n * asyncify) and browser (singlefile asyncify) share this package unchanged.\n * Load once and reuse — the module is stateless; runtimes/contexts are per call.\n */\nexport type SandboxEngine = { module: QuickJSAsyncWASMModule }\n\n/** Accepts the variant itself, a `{ default }` module namespace, or a promise of\n * either — so callers can pass `import('...')` or a plain default import without\n * caring how their bundler/runtime resolved the interop. */\nexport type SandboxVariantInput =\n | QuickJSAsyncVariant\n | { default: QuickJSAsyncVariant }\n | Promise<QuickJSAsyncVariant | { default: QuickJSAsyncVariant }>\n\nexport async function loadEngine(variant: SandboxVariantInput): Promise<SandboxEngine> {\n const resolved = await variant\n const unwrapped = 'default' in resolved ? resolved.default : resolved\n return { module: await newQuickJSAsyncWASMModuleFromVariant(unwrapped) }\n}\n\nexport type SandboxLog = { level: 'log' | 'warn' | 'error'; text: string }\n\nexport type RunScriptOptions = {\n /** Untrusted, typically LLM-generated source. Evaluated as global code. */\n script: string\n /** Scratch filesystem exposed to the guest as `vfs.read/write/list`. */\n vfs?: SandboxVfs\n /**\n * Host-gated network capability, exposed to the guest as `fetchText(url)`.\n * The CALLER owns the allowlist, credential injection, and a per-call\n * timeout — the guest never holds a credential and this module never\n * touches the network itself. Unset = `fetchText` throws in the guest.\n */\n fetchText?: (url: string) => Promise<string>\n /** QuickJS allocator cap. Guest OOM is a failed result, not a host crash. Default 64 MiB. */\n memoryLimitBytes?: number\n /** Wall-clock deadline enforced by the interrupt handler between bytecode ops.\n * Does NOT cover time inside host functions — bound those caller-side. Default 5000. */\n timeoutMs?: number\n /** Guest stack cap. Default 1 MiB. */\n maxStackSizeBytes?: number\n signal?: AbortSignal\n}\n\nexport type RunScriptResult =\n | { ok: true; value: unknown; logs: SandboxLog[] }\n | {\n ok: false\n reason: 'exception' | 'timeout' | 'oom' | 'aborted'\n error: string\n logs: SandboxLog[]\n }\n\n/** Trusted prelude, evaluated before the untrusted script: ergonomic frozen\n * wrappers over the raw host functions. Everything crossing the boundary is a\n * string (by-value marshalling — never a host object reference). */\nconst PRELUDE = `\n\"use strict\";\n(() => {\n const fmt = (v) => {\n if (typeof v === 'string') return v\n if (v === undefined) return 'undefined'\n try { return JSON.stringify(v) } catch { return String(v) }\n }\n globalThis.console = Object.freeze({\n log: (...a) => __host_log('log', a.map(fmt).join(' ')),\n warn: (...a) => __host_log('warn', a.map(fmt).join(' ')),\n error: (...a) => __host_log('error', a.map(fmt).join(' ')),\n })\n globalThis.vfs = Object.freeze({\n read: (p) => __host_vfs_read(String(p)),\n write: (p, c) => { __host_vfs_write(String(p), String(c)) },\n list: (d) => JSON.parse(__host_vfs_list(d === undefined ? '/' : String(d))),\n })\n globalThis.fetchText = (u) => __host_fetch_text(String(u))\n})();\n`\n\n/**\n * Evaluate one untrusted script in a fresh QuickJS context: deny-by-default\n * (no ambient fs/network/host access — only the granted bridge), interpreter-\n * enforced memory and time limits, everything disposed afterwards.\n */\nexport async function runScript(engine: SandboxEngine, options: RunScriptOptions): Promise<RunScriptResult> {\n const logs: SandboxLog[] = []\n const deadline = Date.now() + (options.timeoutMs ?? 5000)\n let interruptedBy: 'timeout' | 'aborted' | undefined\n\n const runtime = engine.module.newRuntime()\n runtime.setMemoryLimit(options.memoryLimitBytes ?? 64 * 1024 * 1024)\n runtime.setMaxStackSize(options.maxStackSizeBytes ?? 1024 * 1024)\n runtime.setInterruptHandler(() => {\n if (options.signal?.aborted) {\n interruptedBy = 'aborted'\n return true\n }\n if (Date.now() > deadline) {\n interruptedBy = 'timeout'\n return true\n }\n return false\n })\n const context = runtime.newContext()\n\n const defineHostFn = (name: string, fn: (...args: QuickJSHandle[]) => QuickJSHandle | undefined): void => {\n const handle = context.newFunction(name, (...args) => fn(...args) ?? context.undefined)\n context.setProp(context.global, name, handle)\n handle.dispose()\n }\n\n try {\n defineHostFn('__host_log', (levelHandle, textHandle) => {\n const level = context.getString(levelHandle)\n logs.push({\n level: level === 'warn' || level === 'error' ? level : 'log',\n text: context.getString(textHandle),\n })\n return undefined\n })\n defineHostFn('__host_vfs_read', (pathHandle) => {\n const content = options.vfs?.read(context.getString(pathHandle))\n return content === undefined ? undefined : context.newString(content)\n })\n defineHostFn('__host_vfs_write', (pathHandle, contentHandle) => {\n if (!options.vfs) throw new Error('vfs is not enabled for this execution')\n options.vfs.write(context.getString(pathHandle), context.getString(contentHandle))\n return undefined\n })\n defineHostFn('__host_vfs_list', (dirHandle) => {\n const files = options.vfs?.list(context.getString(dirHandle)) ?? []\n return context.newString(JSON.stringify(files))\n })\n {\n const fetchText = options.fetchText\n const handle = context.newAsyncifiedFunction('__host_fetch_text', async (urlHandle) => {\n const url = context.getString(urlHandle)\n if (!fetchText) throw new Error('network access is not enabled for this execution')\n return context.newString(await fetchText(url))\n })\n context.setProp(context.global, '__host_fetch_text', handle)\n handle.dispose()\n }\n\n const prelude = await context.evalCodeAsync(PRELUDE, 'prelude.js')\n context.unwrapResult(prelude).dispose()\n\n const evaluated = await context.evalCodeAsync(options.script, 'script.js')\n if (evaluated.error) {\n const error = context.dump(evaluated.error)\n evaluated.error.dispose()\n return failure(error, interruptedBy, logs)\n }\n // Drain the microtask queue so a returned promise can settle. resolvePromise()\n // takes ownership of the handle it is given — never dispose that one again.\n runtime.executePendingJobs()\n const evaluatedValue = evaluated.value\n const state = context.getPromiseState(evaluatedValue)\n if (state.type === 'pending') {\n const settledPromise = context.resolvePromise(evaluatedValue)\n runtime.executePendingJobs()\n const settled = await settledPromise\n evaluatedValue.dispose()\n if (settled.error) {\n const error = context.dump(settled.error)\n settled.error.dispose()\n return failure(error, interruptedBy, logs)\n }\n const value = context.dump(settled.value)\n settled.value.dispose()\n return { ok: true, value, logs }\n }\n if (state.type === 'rejected') {\n const error = context.dump(state.error)\n evaluatedValue.dispose()\n return failure(error, interruptedBy, logs)\n }\n const resultHandle = state.type === 'fulfilled' && !state.notAPromise ? state.value : evaluatedValue\n const value = context.dump(resultHandle)\n if (resultHandle !== evaluatedValue) resultHandle.dispose()\n evaluatedValue.dispose()\n return { ok: true, value, logs }\n } catch (error) {\n return failure(error instanceof Error ? error.message : String(error), interruptedBy, logs)\n } finally {\n context.dispose()\n runtime.dispose()\n }\n}\n\nfunction failure(\n error: unknown,\n interruptedBy: 'timeout' | 'aborted' | undefined,\n logs: SandboxLog[],\n): RunScriptResult {\n const text = describeGuestError(error)\n const reason = interruptedBy ?? (/out of memory/i.test(text) ? 'oom' : 'exception')\n return { ok: false, reason, error: text, logs }\n}\n\nfunction describeGuestError(error: unknown): string {\n if (typeof error === 'string') return error\n if (error && typeof error === 'object') {\n const e = error as { name?: unknown; message?: unknown }\n const name = typeof e.name === 'string' ? e.name : undefined\n const message = typeof e.message === 'string' ? e.message : undefined\n if (name || message) return [name, message].filter(Boolean).join(': ')\n try {\n return JSON.stringify(error)\n } catch {\n return String(error)\n }\n }\n return String(error)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/vfs.ts","../src/run-script.ts"],"sourcesContent":["export type SandboxVfs = {\n read(path: string): string | undefined\n write(path: string, content: string): void\n list(dir?: string): string[]\n snapshot(): Record<string, string>\n}\n\nexport function normalizeVfsPath(path: string): string {\n const out: string[] = []\n for (const part of path.split('/')) {\n if (part === '' || part === '.') {\n continue\n }\n if (part === '..') {\n out.pop()\n continue\n }\n out.push(part)\n }\n return '/' + out.join('/')\n}\n\nexport function createVfs(seed?: Record<string, string>): SandboxVfs {\n const files = new Map<string, string>()\n const write = (path: string, content: string): void => {\n files.set(normalizeVfsPath(path), content)\n }\n for (const [path, content] of Object.entries(seed ?? {})) {\n write(path, content)\n }\n return {\n read(path) {\n return files.get(normalizeVfsPath(path))\n },\n write,\n list(dir = '/') {\n const prefix = normalizeVfsPath(dir)\n return [...files.keys()].filter((file) => prefix === '/' || file === prefix || file.startsWith(prefix + '/')).sort()\n },\n snapshot() {\n return Object.fromEntries(files)\n },\n }\n}\n","import {\n newQuickJSAsyncWASMModuleFromVariant,\n type QuickJSAsyncVariant,\n type QuickJSAsyncWASMModule,\n type QuickJSHandle,\n} from 'quickjs-emscripten-core'\nimport type { SandboxVfs } from './vfs.ts'\n\nconst PRELUDE = `\n\"use strict\";\n(() => {\n const fmt = (v) => {\n if (typeof v === 'string') return v\n if (v === undefined) return 'undefined'\n try { return JSON.stringify(v) } catch { return String(v) }\n }\n globalThis.console = Object.freeze({\n log: (...a) => __host_log('log', a.map(fmt).join(' ')),\n warn: (...a) => __host_log('warn', a.map(fmt).join(' ')),\n error: (...a) => __host_log('error', a.map(fmt).join(' ')),\n })\n globalThis.vfs = Object.freeze({\n read: (p) => __host_vfs_read(String(p)),\n write: (p, c) => { __host_vfs_write(String(p), String(c)) },\n list: (d) => JSON.parse(__host_vfs_list(d === undefined ? '/' : String(d))),\n })\n globalThis.fetchText = (u) => __host_fetch_text(String(u))\n})();\n`\n\nexport type SandboxEngine = { module: QuickJSAsyncWASMModule }\n\nexport type SandboxVariantInput =\n | QuickJSAsyncVariant\n | { default: QuickJSAsyncVariant }\n | Promise<QuickJSAsyncVariant | { default: QuickJSAsyncVariant }>\n\nexport async function loadEngine(variant: SandboxVariantInput): Promise<SandboxEngine> {\n const resolved = await variant\n const unwrapped = 'default' in resolved ? resolved.default : resolved\n return { module: await newQuickJSAsyncWASMModuleFromVariant(unwrapped) }\n}\n\nexport type SandboxLog = { level: 'log' | 'warn' | 'error'; text: string }\n\nexport type RunScriptOptions = {\n script: string\n vfs?: SandboxVfs\n fetchText?: (url: string) => Promise<string>\n memoryLimitBytes?: number\n timeoutMs?: number\n maxStackSizeBytes?: number\n signal?: AbortSignal\n}\n\nexport type RunScriptResult =\n | { ok: true; value: unknown; logs: SandboxLog[] }\n | {\n ok: false\n reason: 'exception' | 'timeout' | 'oom' | 'aborted'\n error: string\n logs: SandboxLog[]\n }\n\nexport async function runScript(engine: SandboxEngine, options: RunScriptOptions): Promise<RunScriptResult> {\n const logs: SandboxLog[] = []\n const deadline = Date.now() + (options.timeoutMs ?? 5000)\n let interruptedBy: 'timeout' | 'aborted' | undefined\n\n const runtime = engine.module.newRuntime()\n runtime.setMemoryLimit(options.memoryLimitBytes ?? 64 * 1024 * 1024)\n runtime.setMaxStackSize(options.maxStackSizeBytes ?? 1024 * 1024)\n runtime.setInterruptHandler(() => {\n if (options.signal?.aborted) {\n interruptedBy = 'aborted'\n return true\n }\n if (Date.now() > deadline) {\n interruptedBy = 'timeout'\n return true\n }\n return false\n })\n const context = runtime.newContext()\n\n const defineHostFn = (name: string, fn: (...args: QuickJSHandle[]) => QuickJSHandle | undefined): void => {\n const handle = context.newFunction(name, (...args) => fn(...args) ?? context.undefined)\n context.setProp(context.global, name, handle)\n handle.dispose()\n }\n\n try {\n defineHostFn('__host_log', (levelHandle, textHandle) => {\n const level = context.getString(levelHandle)\n logs.push({\n level: level === 'warn' || level === 'error' ? level : 'log',\n text: context.getString(textHandle),\n })\n return undefined\n })\n defineHostFn('__host_vfs_read', (pathHandle) => {\n const content = options.vfs?.read(context.getString(pathHandle))\n return content === undefined ? undefined : context.newString(content)\n })\n defineHostFn('__host_vfs_write', (pathHandle, contentHandle) => {\n if (!options.vfs) {\n throw new Error('vfs is not enabled for this execution')\n }\n options.vfs.write(context.getString(pathHandle), context.getString(contentHandle))\n return undefined\n })\n defineHostFn('__host_vfs_list', (dirHandle) => {\n const files = options.vfs?.list(context.getString(dirHandle)) ?? []\n return context.newString(JSON.stringify(files))\n })\n {\n const fetchText = options.fetchText\n const handle = context.newAsyncifiedFunction('__host_fetch_text', async (urlHandle) => {\n const url = context.getString(urlHandle)\n if (!fetchText) {\n throw new Error('network access is not enabled for this execution')\n }\n return context.newString(await fetchText(url))\n })\n context.setProp(context.global, '__host_fetch_text', handle)\n handle.dispose()\n }\n\n const prelude = await context.evalCodeAsync(PRELUDE, 'prelude.js')\n context.unwrapResult(prelude).dispose()\n\n const evaluated = await context.evalCodeAsync(options.script, 'script.js')\n if (evaluated.error) {\n const error = context.dump(evaluated.error)\n evaluated.error.dispose()\n return failure(error, interruptedBy, logs)\n }\n runtime.executePendingJobs()\n const evaluatedValue = evaluated.value\n const state = context.getPromiseState(evaluatedValue)\n if (state.type === 'pending') {\n const settledPromise = context.resolvePromise(evaluatedValue)\n runtime.executePendingJobs()\n const settled = await settledPromise\n evaluatedValue.dispose()\n if (settled.error) {\n const error = context.dump(settled.error)\n settled.error.dispose()\n return failure(error, interruptedBy, logs)\n }\n const value = context.dump(settled.value)\n settled.value.dispose()\n return { ok: true, value, logs }\n }\n if (state.type === 'rejected') {\n const error = context.dump(state.error)\n evaluatedValue.dispose()\n return failure(error, interruptedBy, logs)\n }\n const resultHandle = state.type === 'fulfilled' && !state.notAPromise ? state.value : evaluatedValue\n const value = context.dump(resultHandle)\n if (resultHandle !== evaluatedValue) {\n resultHandle.dispose()\n }\n evaluatedValue.dispose()\n return { ok: true, value, logs }\n } catch (error) {\n return failure(error instanceof Error ? error.message : String(error), interruptedBy, logs)\n } finally {\n context.dispose()\n runtime.dispose()\n }\n}\n\nfunction failure(error: unknown, interruptedBy: 'timeout' | 'aborted' | undefined, logs: SandboxLog[]): RunScriptResult {\n const text = describeGuestError(error)\n const reason = interruptedBy ?? (/out of memory/i.test(text) ? 'oom' : 'exception')\n return { ok: false, reason, error: text, logs }\n}\n\nfunction describeGuestError(error: unknown): string {\n if (typeof error === 'string') {\n return error\n }\n if (error && typeof error === 'object') {\n const e = error as { name?: unknown; message?: unknown }\n const name = typeof e.name === 'string' ? e.name : undefined\n const message = typeof e.message === 'string' ? e.message : undefined\n if (name || message) {\n return [name, message].filter(Boolean).join(': ')\n }\n try {\n return JSON.stringify(error)\n } catch {\n return String(error)\n }\n }\n return String(error)\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workerdeck/sandbox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The WorkerDeck execution sandbox: QuickJS-NG-in-WASM guest for untrusted, LLM-generated scripts with an in-memory scratch VFS, a hardened by-value host bridge (capability host functions only — no ambient fs/network), and interpreter-enforced memory + wall-clock limits. Engine-variant-injected so server (Node asyncify) and browser (singlefile asyncify) share one guest engine. Leaf package: depends on neither core/server nor any model SDK.",
|
|
6
6
|
"license": "MIT",
|