Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
steps:
- uses: actions/checkout@v6

- uses: jdx/mise-action@v2
- uses: jdx/mise-action@v4

- run: task install

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
steps:
- uses: actions/checkout@v6

- uses: jdx/mise-action@v2
- uses: jdx/mise-action@v4

- run: task install

Expand Down
35 changes: 28 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@ Fetches the real Petstore OpenAPI spec from the web, then runs search + execute
```bash
pnpm add @robinbraemer/codemode

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

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).

## Quick Start

```typescript
Expand Down Expand Up @@ -269,11 +272,21 @@ const tags = extractTags(rawSpec);

## Executors

CodeMode uses `isolated-vm` (V8 isolates) for sandboxed execution. You can pass a custom instance:
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).

Use `createExecutor()` for automatic selection, or pass an executor instance explicitly:

```typescript
import { CodeMode, IsolatedVMExecutor } from '@robinbraemer/codemode';
import { CodeMode, createExecutor, IsolatedVMExecutor, QuickJSExecutor } from '@robinbraemer/codemode';

// Automatic — picks isolated-vm on Node, quickjs-emscripten on Bun
const codemode = new CodeMode({
spec,
request: handler,
executor: await createExecutor({ memoryMB: 128, timeoutMs: 60_000 }),
});

// Or explicit
const codemode = new CodeMode({
spec,
request: handler,
Expand All @@ -285,9 +298,17 @@ const codemode = new CodeMode({
});
```

| Executor | Package | Performance | Portability |
|----------|---------|-------------|-------------|
| `IsolatedVMExecutor` | `isolated-vm` | Native V8 speed | Node.js |
| Executor | Package | Performance | Portability | Production-ready |
|----------|---------|-------------|-------------|------------------|
| `IsolatedVMExecutor` | `isolated-vm` | Native V8 speed | Node.js | ✅ |
| `QuickJSExecutor` | `quickjs-emscripten` | Slower (interpreted WASM) | Node, Bun, CF Workers, browser | ⚠️ fallback only — see caveats |

### `QuickJSExecutor` caveats

- **Not a production backend.** Exists so the package loads on runtimes where `isolated-vm` cannot dlopen. Production callers on Node should use `IsolatedVMExecutor`.
- **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.
- **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.
- **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.

### Custom Executor

Expand Down
13 changes: 9 additions & 4 deletions packages/codemode/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@robinbraemer/codemode",
"version": "0.1.6",
"version": "0.2.0",
"description": "Code Mode MCP tools from OpenAPI specs. Two tools (search + execute) replace hundreds of individual MCP tools.",
"type": "module",
"main": "./dist/index.js",
Expand Down Expand Up @@ -46,21 +46,26 @@
"url": "https://github.com/cnap-tech/codemode.git"
},
"peerDependencies": {
"isolated-vm": "6"
"isolated-vm": "6",
"quickjs-emscripten": ">=0.31"
},
"peerDependenciesMeta": {
"isolated-vm": {
"optional": true
},
"quickjs-emscripten": {
"optional": true
}
},
"devDependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"hono": "^4.7.6",
"isolated-vm": "^6.0.2",
"quickjs-emscripten": "^0.32.0",
"tsup": "^8.4.0",
"tsx": "^4.21.0",
"typescript": "^5.7.3",
"vitest": "^3.0.5",
"typescript": "^6.0.0",
"vitest": "^4.0.0",
"zod": "^4.0.0"
}
}
71 changes: 61 additions & 10 deletions packages/codemode/src/executor/auto.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,73 @@
import type { Executor, SandboxOptions } from "../types.js";

/**
* Create an executor using the isolated-vm peer dependency.
* Detect whether we're running under Bun. On Bun, isolated-vm cannot dlopen
* (it relies on V8 symbols like `v8::ValueSerializer::Delegate::IsHostObject`
* that Bun's JavaScriptCore engine does not export), so we prefer the WASM
* QuickJS backend.
*
* Uses Bun's officially documented detection pattern:
* https://bun.com/docs/guides/util/detect-bun
*
* The `typeof process` guard keeps this safe in non-Node-shaped runtimes
* (Cloudflare Workers, browser) where `process` is undefined.
*/
function isBun(): boolean {
// Cast through globalThis to avoid requiring @types/node just for `process`.
const proc = (globalThis as { process?: { versions?: { bun?: string } } }).process;
return !!proc?.versions?.bun;
}

/**
* Pick a sandbox runtime automatically.
*
* Order of preference:
* - **Bun** → QuickJS first (isolated-vm cannot load native bindings under
* JavaScriptCore), fall back to isolated-vm only if QuickJS isn't
* installed.
* - **Node** → isolated-vm first (V8 JIT is faster, mature, no upstream
* async bugs), fall back to QuickJS if isolated-vm isn't installed (e.g.
* ARM Linux without build tools, or a Node minor without a prebuild).
*
* Production deployments on Node should always have `isolated-vm` installed
* — QuickJS is a compatibility fallback, not a recommended production
* backend. See `QuickJSExecutor`'s docstring for the upstream
* `quickjs-emscripten` bugs it inherits.
*
* Both `isolated-vm` and `quickjs-emscripten` are optional peer dependencies.
*/
export async function createExecutor(
options: SandboxOptions = {},
): Promise<Executor> {
try {
// @ts-ignore — optional peer dependency
await import("isolated-vm");
const { IsolatedVMExecutor } = await import("./isolated-vm.js");
return new IsolatedVMExecutor(options);
} catch {
// Not available
const order = isBun() ? (["quickjs", "isolated-vm"] as const) : (["isolated-vm", "quickjs"] as const);

/* oxlint-disable no-await-in-loop */
for (const backend of order) {
if (backend === "isolated-vm") {
try {
// @ts-ignore — optional peer dependency
await import("isolated-vm");
const { IsolatedVMExecutor } = await import("./isolated-vm.js");
return new IsolatedVMExecutor(options);
} catch {
// not available — try the next backend
}
} else {
try {
// @ts-ignore — optional peer dependency
await import("quickjs-emscripten");
const { QuickJSExecutor } = await import("./quickjs.js");
return new QuickJSExecutor(options);
} catch {
// not available — try the next backend
}
}
}
/* oxlint-enable no-await-in-loop */

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