Skip to content

Commit 4f4058a

Browse files
authored
feat: add QuickJS executor as a sibling backend to isolated-vm (#11)
Adds `QuickJSExecutor` (pure-WASM `quickjs-emscripten`) as an optional sibling backend to `IsolatedVMExecutor`. `createExecutor` picks by runtime: Bun → QuickJS first; Node → isolated-vm first. Unblocks Bun / CF Workers / browser hosts where isolated-vm cannot dlopen. isolated-vm remains the recommended production backend on Node. - New `executor-contract.ts` parameterized test suite — parity is structural, not intent-based - `ExecuteStats` shape parity locked by test; structured-clone divergence (Date/Map/BigInt) also locked - Documented upstream `quickjs-emscripten@0.32.0` bugs (#258, #261) reproduce on Node AND Bun, not engine-specific - Bun detection via officially documented `process.versions.bun` pattern - Batched in pending Renovate dep updates: vitest 4, typescript 6, mise-action v4, all-non-major - Version: 0.1.6 → 0.2.0
1 parent bdce03d commit 4f4058a

13 files changed

Lines changed: 1394 additions & 546 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
steps:
1313
- uses: actions/checkout@v6
1414

15-
- uses: jdx/mise-action@v2
15+
- uses: jdx/mise-action@v4
1616

1717
- run: task install
1818

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
steps:
1414
- uses: actions/checkout@v6
1515

16-
- uses: jdx/mise-action@v2
16+
- uses: jdx/mise-action@v4
1717

1818
- run: task install
1919

README.md

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,13 @@ Fetches the real Petstore OpenAPI spec from the web, then runs search + execute
2828
```bash
2929
pnpm add @robinbraemer/codemode
3030

31-
# Install the sandbox runtime:
32-
pnpm add isolated-vm # V8 isolates
31+
# Install a sandbox runtime (at least one):
32+
pnpm add isolated-vm # V8 isolates — recommended for production on Node
33+
pnpm add quickjs-emscripten # WASM QuickJS — fallback for Bun / CF Workers / browser
3334
```
3435

36+
If both are installed, the auto-selector (`createExecutor`) picks `isolated-vm` on Node and `quickjs-emscripten` on Bun (where `isolated-vm` cannot dlopen because Bun's JavaScriptCore engine does not export the V8 symbols `isolated-vm` requires).
37+
3538
## Quick Start
3639

3740
```typescript
@@ -269,11 +272,21 @@ const tags = extractTags(rawSpec);
269272
270273
## Executors
271274
272-
CodeMode uses `isolated-vm` (V8 isolates) for sandboxed execution. You can pass a custom instance:
275+
CodeMode ships two executor backends. `IsolatedVMExecutor` is the recommended production backend on Node. `QuickJSExecutor` is a compatibility fallback for environments where `isolated-vm` cannot load (Bun, Cloudflare Workers, browser).
276+
277+
Use `createExecutor()` for automatic selection, or pass an executor instance explicitly:
273278
274279
```typescript
275-
import { CodeMode, IsolatedVMExecutor } from '@robinbraemer/codemode';
280+
import { CodeMode, createExecutor, IsolatedVMExecutor, QuickJSExecutor } from '@robinbraemer/codemode';
276281

282+
// Automatic — picks isolated-vm on Node, quickjs-emscripten on Bun
283+
const codemode = new CodeMode({
284+
spec,
285+
request: handler,
286+
executor: await createExecutor({ memoryMB: 128, timeoutMs: 60_000 }),
287+
});
288+
289+
// Or explicit
277290
const codemode = new CodeMode({
278291
spec,
279292
request: handler,
@@ -285,9 +298,17 @@ const codemode = new CodeMode({
285298
});
286299
```
287300
288-
| Executor | Package | Performance | Portability |
289-
|----------|---------|-------------|-------------|
290-
| `IsolatedVMExecutor` | `isolated-vm` | Native V8 speed | Node.js |
301+
| Executor | Package | Performance | Portability | Production-ready |
302+
|----------|---------|-------------|-------------|------------------|
303+
| `IsolatedVMExecutor` | `isolated-vm` | Native V8 speed | Node.js | ✅ |
304+
| `QuickJSExecutor` | `quickjs-emscripten` | Slower (interpreted WASM) | Node, Bun, CF Workers, browser | ⚠️ fallback only — see caveats |
305+
306+
### `QuickJSExecutor` caveats
307+
308+
- **Not a production backend.** Exists so the package loads on runtimes where `isolated-vm` cannot dlopen. Production callers on Node should use `IsolatedVMExecutor`.
309+
- **Sandboxed code must avoid sequential `await` on host functions.** Use `Promise.all([fn1(), fn2()])` for parallel calls instead. Chained sequential `await`s currently crash with an upstream `quickjs-emscripten@0.32.0` release-asyncify regression ([justjake/quickjs-emscripten#258](https://github.com/justjake/quickjs-emscripten/issues/258)) — reproduces identically on Node and Bun.
310+
- **Return-value semantics differ from `isolated-vm`.** Host ↔ guest values cross via a `JSON.stringify` envelope. `Date`, `Map`, `Set`, `BigInt` are converted to strings/objects, not preserved as instances. `isolated-vm` uses structured clone and preserves them. Stick to plain JSON-safe shapes in sandboxed code that targets both backends.
311+
- **CPU timeout is wall-clock-based.** `isolated-vm` uses true CPU time; QuickJS uses elapsed time. Async host calls that take wall time count against the CPU budget under QuickJS.
291312
292313
### Custom Executor
293314

packages/codemode/package.json

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@robinbraemer/codemode",
3-
"version": "0.1.6",
3+
"version": "0.2.0",
44
"description": "Code Mode MCP tools from OpenAPI specs. Two tools (search + execute) replace hundreds of individual MCP tools.",
55
"type": "module",
66
"main": "./dist/index.js",
@@ -46,21 +46,26 @@
4646
"url": "https://github.com/cnap-tech/codemode.git"
4747
},
4848
"peerDependencies": {
49-
"isolated-vm": "6"
49+
"isolated-vm": "6",
50+
"quickjs-emscripten": ">=0.31"
5051
},
5152
"peerDependenciesMeta": {
5253
"isolated-vm": {
5354
"optional": true
55+
},
56+
"quickjs-emscripten": {
57+
"optional": true
5458
}
5559
},
5660
"devDependencies": {
5761
"@modelcontextprotocol/sdk": "^1.12.1",
5862
"hono": "^4.7.6",
5963
"isolated-vm": "^6.0.2",
64+
"quickjs-emscripten": "^0.32.0",
6065
"tsup": "^8.4.0",
6166
"tsx": "^4.21.0",
62-
"typescript": "^5.7.3",
63-
"vitest": "^3.0.5",
67+
"typescript": "^6.0.0",
68+
"vitest": "^4.0.0",
6469
"zod": "^4.0.0"
6570
}
6671
}
Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,73 @@
11
import type { Executor, SandboxOptions } from "../types.js";
22

33
/**
4-
* Create an executor using the isolated-vm peer dependency.
4+
* Detect whether we're running under Bun. On Bun, isolated-vm cannot dlopen
5+
* (it relies on V8 symbols like `v8::ValueSerializer::Delegate::IsHostObject`
6+
* that Bun's JavaScriptCore engine does not export), so we prefer the WASM
7+
* QuickJS backend.
8+
*
9+
* Uses Bun's officially documented detection pattern:
10+
* https://bun.com/docs/guides/util/detect-bun
11+
*
12+
* The `typeof process` guard keeps this safe in non-Node-shaped runtimes
13+
* (Cloudflare Workers, browser) where `process` is undefined.
14+
*/
15+
function isBun(): boolean {
16+
// Cast through globalThis to avoid requiring @types/node just for `process`.
17+
const proc = (globalThis as { process?: { versions?: { bun?: string } } }).process;
18+
return !!proc?.versions?.bun;
19+
}
20+
21+
/**
22+
* Pick a sandbox runtime automatically.
23+
*
24+
* Order of preference:
25+
* - **Bun** → QuickJS first (isolated-vm cannot load native bindings under
26+
* JavaScriptCore), fall back to isolated-vm only if QuickJS isn't
27+
* installed.
28+
* - **Node** → isolated-vm first (V8 JIT is faster, mature, no upstream
29+
* async bugs), fall back to QuickJS if isolated-vm isn't installed (e.g.
30+
* ARM Linux without build tools, or a Node minor without a prebuild).
31+
*
32+
* Production deployments on Node should always have `isolated-vm` installed
33+
* — QuickJS is a compatibility fallback, not a recommended production
34+
* backend. See `QuickJSExecutor`'s docstring for the upstream
35+
* `quickjs-emscripten` bugs it inherits.
36+
*
37+
* Both `isolated-vm` and `quickjs-emscripten` are optional peer dependencies.
538
*/
639
export async function createExecutor(
740
options: SandboxOptions = {},
841
): Promise<Executor> {
9-
try {
10-
// @ts-ignore — optional peer dependency
11-
await import("isolated-vm");
12-
const { IsolatedVMExecutor } = await import("./isolated-vm.js");
13-
return new IsolatedVMExecutor(options);
14-
} catch {
15-
// Not available
42+
const order = isBun() ? (["quickjs", "isolated-vm"] as const) : (["isolated-vm", "quickjs"] as const);
43+
44+
/* oxlint-disable no-await-in-loop */
45+
for (const backend of order) {
46+
if (backend === "isolated-vm") {
47+
try {
48+
// @ts-ignore — optional peer dependency
49+
await import("isolated-vm");
50+
const { IsolatedVMExecutor } = await import("./isolated-vm.js");
51+
return new IsolatedVMExecutor(options);
52+
} catch {
53+
// not available — try the next backend
54+
}
55+
} else {
56+
try {
57+
// @ts-ignore — optional peer dependency
58+
await import("quickjs-emscripten");
59+
const { QuickJSExecutor } = await import("./quickjs.js");
60+
return new QuickJSExecutor(options);
61+
} catch {
62+
// not available — try the next backend
63+
}
64+
}
1665
}
66+
/* oxlint-enable no-await-in-loop */
1767

1868
throw new Error(
19-
"No sandbox runtime found. Install isolated-vm:\n" +
20-
" npm install isolated-vm # V8 isolates (Node.js)",
69+
"No sandbox runtime found. Install one of:\n" +
70+
" npm install isolated-vm # V8 isolates (Node.js, fastest)\n" +
71+
" npm install quickjs-emscripten # WASM QuickJS (Bun, Workers, browser)",
2172
);
2273
}

0 commit comments

Comments
 (0)