Overview
JavaScriptCore-native sandbox for Bun. Heap-isolated execution for untrusted code, with an isolated-vm-shaped API.
This is still not broad launch mode. The 0.8.x line moved isolated-jsc from raw isolate primitives into a product API for the Bun isolation wedge: policy recipes, one-shot execution, pooled keyed runners, precompiled callables, capability manifests, redacted bounded audit events, execution receipts, and output limits for tenant scripts, AI-generated code, and plugin execution. The 0.9.0 through 0.11.0 releases add the multi-tenant economics layer on top: hibernating keyed pools, operator-shaped pool metrics, and OpenTelemetry tracing.
The fastest backend is not automatically the right trust boundary; choose from workload provenance and deployment controls.
Best for: High-volume code you control or tightly broker through explicit host capabilities.
Tradeoffs: Lowest overhead, but it is a same-process boundary and must not be described as an OS sandbox.
RequirementsAs of 0.11.0 the package keeps the proof pack but adds the API shape services actually want: choose a policy, run one-off source with runIsolated(), create a pooled createIsolatedRunner(), warm hot functions with precompile(), hibernate idle tenants with createHibernatingIsolatePool(), review declared host powers with capability manifests, redact and bound audit events, cap host capability outputs, and inspect every run with receipts.
Headline numbers from the repeatable bench:proof run (100 warm / 10 cold iterations):
| Feature | Why it matters |
|---|---|
| Benchmark proof pack | Reproducible local measurements for FFI, Worker, Bun process-spawn, and optional Node isolated-vm baselines. |
| Migration guide | A concrete path for teams moving a Node isolated-vm workload to Bun and JavaScriptCore. |
| Security model | Explicit FFI vs Worker guarantees, Worker residuals, resource limits, and deployment hardening guidance. |
| TypeScript helpers | Bun-native transpilation helpers for string source and real .ts/.tsx/.js/.jsx files before isolate execution. |
| Capability broker | Named host tools with validation hooks, timeout, concurrency, output byte caps, tenant context, manifests, redacted audit events, bounded audit buffers, typed tool definitions, and typed host-side direct calls. |
| Execution receipts | Per-run receipts for scripts, callables, one-shot execution, and pooled runners, including backend, policy, timing, metrics, output size, capability summaries, console bounds, and dropped-audit counts. |
| Output boundaries | Run-level result byte limits, per-capability output byte limits, and isolate-level console entry/byte limits so successful outputs, tool outputs, and captured logs stay bounded. |
| Doctor CLI | A package bin that reports Bun/platform details, FFI availability, checked JSC paths, and install hints. |
| Agent-tool example | A runnable demo combining TypeScript callables, brokered tools, tenant context, metrics, and audit events. |
| Policy presets + runners | Product-ready recipes for AI tools, tenant scripts, plugins, and trusted code, including recommended result, console, audit, broker, and pool defaults. |
| Hibernating isolate pool | A keyed isolate + context pool that checkpoints idle tenants down to bytes and wakes them transparently on the next call, with a pluggable hibernation store (0.9.0). |
| Pool operator surface | metrics(), drain(), and warm(key) on both pools for cost metering, graceful shard shutdown, and predictive pre-warm (0.10.0). |
| OpenTelemetry tracing | Optional tracerProvider on the hibernating pool emits an isolated_jsc.run span per pool.run with tenant and wake attributes (0.11.0). |
The new launch framing is not just "run code in a sandbox." The host gets reviewable capability manifests, redacted audit events, bounded audit buffers, and execution receipts that survive success and failure paths. Receipts carry the backend, policy, tenant and purpose labels, timing, metrics, output size, capability call summaries, console overflow flags, and capability audit truncation counts.
maxResultBytes rejects oversized successful outputs with ResultSizeError. maxConsoleEntries and maxConsoleBytes bound captured console output. createCapabilityAuditBuffer()bounds retained capability events and records how many were dropped. Capability tools can set maxOutputBytes so oversized host-tool results reject with a CapabilityErrorbefore sandbox code receives them, and receipts retain machine-readable error codes such as CAPABILITY_OUTPUT_SIZE_LIMIT.
Every release since 0.8.11 tightened this surface. The line runs from error-metadata parity through checkpoints and receipts to the hibernating pool and tracing:
import {
createCapabilityAuditBuffer,
runIsolated
} from "@absolutejs/isolated-jsc";
const audit = createCapabilityAuditBuffer({ maxEvents: 32 });
const { result, receipt } = await runIsolated("await tools('now'); 42", {
policy: "tenant-script",
globals: { tools: broker.reference },
maxConsoleEntries: 4,
maxConsoleBytes: 512,
run: {
...audit.receiptOptions(),
executionId: "exec_123",
maxResultBytes: 16_384,
purpose: "tenant-plugin",
tenant: "tenant_acme",
},
withReceipt: true
});
receipt.capabilityCallsDropped; // number of audit events not retained
receipt.capabilityCallsTruncated; // true when the audit buffer overflowed
receipt.console.truncated; // true when console capture overflowed
receipt.error?.code; // machine-readable failure, when present
receipt.schemaVersion; // 1 — stable receipt schema marker
receipt.outputBytes; // estimated successful-result bytesThe positioning is narrow on purpose: Bun already makes TypeScript execution fast. @absolutejs/isolated-jsc makes untrusted TypeScript and JavaScript execution embeddable inside Bun.
For the deeper market and objection-handling frame, see isolated-jsc for Bun.
isolated-vm get familiar nouns: Isolate, Context, Script, Reference, and ExternalCopy.isolated-jsc doctor shows the backend, checked JSC paths, and platform-specific install guidance before teams hit runtime errors.bun add @absolutejs/isolated-jsc
bunx @absolutejs/isolated-jscThe example is the intended adoption shape: define typed host tools, call them directly from the host when useful, pass the brokered dispatcher as a Reference to untrusted code, set a timeout, and capture per-call metrics.
import {
createCapabilityAuditBuffer,
createCapabilityBroker,
createIsolatedRunner,
defineCapabilityTool
} from "@absolutejs/isolated-jsc";
type TenantContext = { id: string; plan: "free" | "pro" };
type OrderLookup = { id: string };
type Order = { id: string; status: string; totalUsd: number };
const audit = createCapabilityAuditBuffer<TenantContext>({ maxEvents: 100 });
const broker = createCapabilityBroker(
{
lookupOrder: defineCapabilityTool<
OrderLookup,
Order | null,
TenantContext
>({
description: "Read one order by id for the current tenant",
input: "OrderLookup",
output: "Order | null",
maxOutputBytes: 16_384,
risk: "read-only",
timeoutMs: 100,
redactAuditInput: (input) => ({ id: (input as { id?: unknown }).id }),
redactAuditOutput: (output) => {
if (output === null) return null;
const order = output as Order;
return { id: order.id, status: order.status };
},
validateInput: (input) => {
if (input === null || typeof input !== "object") {
throw new Error("lookupOrder input must be an object");
}
const id = (input as { id?: unknown }).id;
if (typeof id !== "string") {
throw new Error("lookupOrder input requires a string id");
}
return { id };
},
handler: async ({ id }, tenant) => lookupOrderForTenant(tenant.id, id)
})
},
{
context: { id: "tenant_acme", plan: "pro" },
onAudit: audit.onAudit
}
);
const manifest = broker.manifest();
// Direct host calls infer Order | null from the tool map.
const order = await broker.call("lookupOrder", { id: "ord_123" });
const runner = createIsolatedRunner({
policy: "ai-tool",
pool: { maxSize: 64, idleMs: 60_000 },
});
await runner.precompile(
"agentLookup",
'async (tools, orderId) => await tools("lookupOrder", { id: orderId })',
{ key: "tenant_acme" }
);
const { result, metrics } = await runner.call(
"agentLookup",
'async (tools, orderId) => await tools("lookupOrder", { id: orderId })',
[broker.reference, "ord_123"],
{
key: "tenant_acme",
run: {
...audit.receiptOptions(),
maxResultBytes: 16_384,
purpose: "agent-tool-call",
tenant: "tenant_acme"
},
withMetrics: true
}
);
const stats = runner.stats();
await runner.dispose();Use backend: 'ffi' for hostile-code production paths on macOS or Linux where JavaScriptCore is available. Use backend: 'auto' for local development, demos, CI smoke tests, and portable defaults. When the Worker fallback is the only option, keep it behind a process or container boundary if hostile workloads could reach meaningful host secrets.
Reference or the typed capability broker, then validate, timeout, and audit every call.The release keeps the security language explicit. FFI closes the indirect eval and Function-constructor residuals by disabling eval per context. The Worker backend remains a portable fallback, but hostile-code workloads that care about those residuals should requirebackend: 'ffi' and compose with process, container, uid, or network boundaries when host secrets are high value.
bun run example:agent-tool from the repo to see the proof-pack path end to end.Current package surface
Import surface · click to copy
Outcomes
JavaScriptCore-native sandbox for Bun. Heap-isolated execution for untrusted code, with an isolated-vm-shaped API.
Bun has no equivalent to Node's isolated-vm. The Node library is V8-specific — it links against V8's HasCustomHostObject ABI symbol — and Bun uses JavaScriptCore, not V8. So bun install isolated-vm succeeds, then import fails with undefined symbol: HasCustomHostObject.
Why not Node isolated-vm? It is the right shape for Node/V8, but Bun is JavaScriptCore. isolated-jsc ports the isolate-shaped API to Bun/JSC instead of trying to load a V8 addon.
Hardening checklist
Follow in order