
Research
/Security News
Mini Shai-Hulud Campaign Hits Red Hat Cloud Services npm Packages
A mini Shai-Hulud campaign compromised Red Hat Cloud Services npm packages to steal developer and CI/CD secrets during installation.
@ricsam/isolate
Advanced tools
Unified runtime host for app servers, script runtimes, browser runtimes, module resolution, file bindings, and typechecking
@ricsam/isolate is a runtime host for running JavaScript and TypeScript inside isolated V8 sandboxes. It gives you one host API for short-lived scripts, long-lived app servers, browser-backed tests, persistent sessions, module loading, files, fetch, and nested sandboxes.
Use it when you want a higher-level sandbox than raw isolated-vm: the host stays in control of capabilities, while sandboxed code gets a web-style runtime surface.
npm add @ricsam/isolate @ricsam/isolated-vm
@ricsam/isolate expects the async-context-enabled @ricsam/isolated-vm peer. Upstream isolated-vm does not provide the required createContext({ asyncContext: true }) support and will fail fast during runtime boot.
Install Playwright when you want browser-enabled runtimes or test runtimes:
npm add playwright
createIsolateHost() will auto-start a daemon when needed, so the default setup is usually enough to get going.
import {
createFileBindings,
createIsolateHost,
createModuleResolver,
} from "@ricsam/isolate";
const host = await createIsolateHost({
daemon: {
socketPath: "/tmp/isolate.sock",
},
});
const runtime = await host.createRuntime({
bindings: {
console: {
onEntry(entry) {
if (entry.type === "output") {
console.log(entry.stdout);
}
},
},
fetch: async (request) => await fetch(request),
files: createFileBindings({
root: process.cwd(),
allowWrite: true,
}),
modules: createModuleResolver()
.virtual(
"@/env",
`export const mode = "sandbox";`,
{ filename: "env.ts", resolveDir: "/app" },
)
.virtual(
"/app/main.ts",
`
import { mode } from "@/env";
const response = await fetch("https://example.com");
console.log("mode:", mode);
console.log("status:", response.status);
console.log(await greet("isolate"));
`,
{ filename: "main.ts", resolveDir: "/app" },
),
tools: {
greet: async (name: string) => `hello ${name}`,
},
},
});
try {
await runtime.eval(`import "/app/main.ts";`, { filename: "/app/entry.ts" });
} finally {
await runtime.dispose();
await host.close();
}
That example wires together the most common capabilities:
console to forward sandbox outputfetch for outbound HTTP requestsfiles for root-scoped filesystem accessmodules for virtual modules and source treestools for async host functionsEvent-style callbacks such as console.onEntry(...), runtime.test.onEvent(...), and Playwright onEvent(...) are sync-only, best-effort notifications. Returned promises are ignored after rejection logging, so schedule any async follow-up work from inside the synchronous handler.
The host can create four runtime styles:
host.createRuntime() for scripts, agents, and ad hoc executionhost.createAppServer() for long-lived serve() request handlershost.createTestRuntime() for test suites with describe, test, hooks, and expecthost.getNamespacedRuntime() for persistent sessions that survive soft dispose and can be reacquired laterBindings define how sandboxed code talks to the host:
console forwards runtime and browser console outputfetch handles outbound HTTP requestsfiles exposes a safe, root-scoped filesystemmodules resolves virtual modules, source trees, mounted packages, and fallbackstools exposes async host functions and async iteratorsbrowser exposes a Playwright-like browser surfaceEvery host callback receives a HostCallContext with an AbortSignal, runtime identity, resource identity, and request metadata.
fetch is disabled unless you provide a host binding. If sandbox code calls fetch(...) without a bindings.fetch callback, the runtime rejects the request instead of falling back to the Node.js process fetch. To allow outbound network access, pass an explicit policy-enforcing callback:
const runtime = await host.createRuntime({
bindings: {
fetch: async (request, context) => {
// Validate, log, rate-limit, meter, or rewrite here.
return await fetch(request, {
signal: context.signal,
});
},
},
});
When exposing browser support, choose exactly one mode per runtime:
createContext() and optionally createPage(), readFile(), and writeFile()handler, usually from createPlaywrightSessionHandler(...)Do not mix handler with createContext() / createPage() / readFile() / writeFile() in the same binding.
Keep bindings plain-data and host-owned. Do not leak raw isolated-vm handles or other engine objects into untrusted code.
Sandbox code can import @ricsam/isolate and create child runtimes against the same top-level host connection when the parent runtime opts in with nestedHost.
const runtime = await host.createRuntime({
bindings: {
console: {
onEntry(entry) {
if (entry.type === "output") {
console.log(entry.stdout);
}
},
},
fetch: async (request) => await fetch(request),
},
nestedHost: {
fetch: "inherit",
maxTotalResources: 8,
maxRuntimes: 6,
maxAppServers: 2,
maxMemoryLimitMB: 128,
maxExecutionTimeoutMs: 30_000,
maxAppServerLifetimeMs: 10 * 60_000,
},
});
await runtime.eval(`
import { createIsolateHost } from "@ricsam/isolate";
const nestedHost = createIsolateHost();
const child = await nestedHost.createRuntime({
bindings: {
tools: {
greet: async (name) => "hello " + name,
},
},
});
await child.eval('console.log(await greet("nested"))');
await child.dispose();
await nestedHost.close();
`);
Nested hosts support:
createRuntime()createAppServer()createTestRuntime()getNamespacedRuntime()disposeNamespace()diagnostics()close()Nested resources are brokered by the parent host. The parent nestedHost policy controls whether child runtimes inherit the parent fetch binding, how many nested resources can exist across the whole descendant tree, and the maximum memory and execution timeout each child may request. Namespace keys created from inside a nested host are internally scoped so sandbox code can dispose only namespaces it created through that nested host.
The available policy fields are:
fetch: "inherit" | "disabled" - defaults to "disabled"maxTotalResources - total nested runtimes, test runtimes, namespaced runtimes, and app serversmaxRuntimes - nested script, test, and namespaced runtimesmaxAppServers - nested app serversmaxMemoryLimitMB - maximum per-child memory limitmaxExecutionTimeoutMs - maximum per-child execution timeoutmaxAppServerLifetimeMs - optional hard lifetime for nested app serversIf nestedHost is omitted, nested hosts use conservative defaults and do not inherit fetch. Pass nestedHost: false to disable the synthetic sandbox @ricsam/isolate module entirely for a runtime.
createAppServer() is the long-lived server API. It boots a runtime around an entry module that calls serve() and lets the host dispatch requests into it.
import { createIsolateHost, createModuleResolver } from "@ricsam/isolate";
const host = await createIsolateHost();
const server = await host.createAppServer({
key: "example/server",
entry: "/server.ts",
bindings: {
modules: createModuleResolver().virtual(
"/server.ts",
`
serve({
fetch(request) {
return Response.json({
pathname: new URL(request.url).pathname,
});
},
});
`,
),
},
});
const result = await server.handle(new Request("http://localhost/hello"));
if (result.type === "response") {
console.log(await result.response.json());
}
await server.dispose();
await host.close();
server.handle() returns either an HTTP response or WebSocket upgrade metadata. For upgraded connections, server.ws lets the host send open, message, close, and error events back into the runtime.
If you provide bindings.browser, script and app runtimes get a global browser factory even when they are not full Playwright browser runtimes.
import { chromium } from "playwright";
import { createIsolateHost } from "@ricsam/isolate";
const browser = await chromium.launch();
const host = await createIsolateHost();
const runtime = await host.createRuntime({
bindings: {
browser: {
createContext: async (options) =>
await browser.newContext(options ?? undefined),
createPage: async (contextInstance) =>
await contextInstance.newPage(),
},
},
});
await runtime.eval(`
const ctx = await browser.newContext({
viewport: { width: 1280, height: 720 },
});
const page = await ctx.newPage();
await page.goto("https://example.com");
console.log(await page.title());
console.log(typeof browser.close);
await page.close();
await ctx.close();
`);
await runtime.dispose();
await browser.close();
await host.close();
Inside these runtimes:
browser.newContext() is availablebrowser.contexts() is availablecontext.newPage() is availablecontext.pages() is availablepage.close() and context.close() are availablebrowser.close() is not exposed inside the sandboxpage and context are never injected as implicit globalscreateTestRuntime() enables describe, test / it, hooks, and expect. If you also provide bindings.browser, the same test runtime gets Playwright-style browser access and matcher support.
import { chromium } from "playwright";
import { createIsolateHost } from "@ricsam/isolate";
const browser = await chromium.launch();
const host = await createIsolateHost();
const runtime = await host.createTestRuntime({
key: "example/browser-test",
bindings: {
browser: {
captureConsole: true,
createContext: async (options) =>
await browser.newContext(options ?? undefined),
createPage: async (contextInstance) =>
await contextInstance.newPage(),
},
},
});
const unsubscribe = runtime.test.onEvent((event) => {
if (event.type === "testStart") {
console.log("running", event.test.fullName);
}
});
const result = await runtime.run(
`
let ctx;
let page;
beforeAll(async () => {
ctx = await browser.newContext();
page = await ctx.newPage();
});
afterAll(async () => {
await ctx.close();
});
test("loads a page", async () => {
expect((await browser.contexts()).length).toBe(1);
expect((await ctx.pages()).length).toBe(1);
await page.goto("https://example.com", {
waitUntil: "domcontentloaded",
});
await expect(page).toHaveTitle(/Example Domain/);
});
`,
{
filename: "/browser-test.ts",
timeoutMs: 10_000,
},
);
console.log(result);
unsubscribe();
await runtime.dispose();
await browser.close();
await host.close();
From inside another sandbox, nestedHost.createTestRuntime() can reuse the sandbox browser handle:
import { createIsolateHost } from "@ricsam/isolate";
const nestedHost = createIsolateHost();
const child = await nestedHost.createTestRuntime({
bindings: {
browser,
},
});
await child.run(`
let ctx;
let page;
beforeAll(async () => {
ctx = await browser.newContext();
page = await ctx.newPage();
});
afterAll(async () => {
await ctx.close();
});
test("loads a nested page", async () => {
await page.goto("https://example.com");
await expect(page).toHaveTitle(/Example Domain/);
});
`);
await child.dispose();
await nestedHost.close();
host.getNamespacedRuntime(key, options) is the persistent-session API. Use it when you want one underlying runtime to survive across multiple calls while refreshing host bindings on each acquire.
import { chromium } from "playwright";
import { createIsolateHost } from "@ricsam/isolate";
import { createPlaywrightSessionHandler } from "@ricsam/isolate/playwright";
const browser = await chromium.launch();
const host = await createIsolateHost();
const playwright = createPlaywrightSessionHandler({
createContext: async (options) =>
await browser.newContext(options ?? undefined),
createPage: async (context) =>
await context.newPage(),
});
const session = await host.getNamespacedRuntime("playwright:preview:session", {
bindings: {
browser: {
handler: playwright.handler,
},
},
});
await session.eval(`
globalThis.ctx = await browser.newContext();
globalThis.page = await globalThis.ctx.newPage();
await globalThis.page.goto("https://example.com");
`);
await session.dispose();
const reused = await host.getNamespacedRuntime("playwright:preview:session", {
bindings: {
browser: {
handler: playwright.handler,
},
},
});
const unsubscribe = reused.test.onEvent((event) => {
if (event.type === "testStart") {
console.log("running", event.test.fullName);
}
});
const results = await reused.runTests(`
test("sees the existing browser state", async () => {
const contexts = await browser.contexts();
expect(contexts.length).toBe(1);
const pages = await contexts[0].pages();
expect(pages.length).toBe(1);
});
`);
console.log(results.success);
unsubscribe();
await host.disposeNamespace("playwright:preview:session");
await browser.close();
await host.close();
Lifecycle notes:
runTests(code) resets test registration before loading and running the provided suitesession.test.onEvent(...) exposes suite and test lifecycle events for timeout and progress reportingRuntimes created by @ricsam/isolate enable the TC39 proposal-style AsyncContext global inside the sandbox. This experimental surface is also used to implement the node:async_hooks shim exposed to sandboxed code.
This shim is for async context propagation inside the sandbox. It is not a full reimplementation of Node's async_hooks lifecycle, resource graph, or profiling APIs.
Currently supported:
AsyncContext.VariableAsyncContext.Snapshotnode:async_hooks AsyncLocalStoragenode:async_hooks AsyncResourcenode:async_hooks createHook()node:async_hooks executionAsyncId()node:async_hooks triggerAsyncId()node:async_hooks executionAsyncResource()node:async_hooks asyncWrapProvidersimport {
createHook,
executionAsyncResource,
} from "node:async_hooks";
const hook = createHook({
init(asyncId, type, triggerAsyncId, resource) {
resource.requestTag = type + ":" + asyncId;
},
before() {
console.log(executionAsyncResource().requestTag ?? null);
},
}).enable();
setTimeout(() => {
console.log(executionAsyncResource().requestTag);
hook.disable();
}, 0);
@ricsam/isolate exports createIsolateHost(), createModuleResolver(), createFileBindings(), getTypeProfile(), typecheck(), formatTypecheckErrors(), and public types such as HostBindings, NestedHostPolicy, and runtime handles@ricsam/isolate/playwright exports createPlaywrightSessionHandler() and related Playwright handler types@ricsam/isolate is also available as a synthetic module that exports sandbox-only createIsolateHost() for nested runtimescreateIsolateHost()createIsolateHost() creates the top-level host connection. The returned host exposes:
createRuntime(options) for script executioncreateAppServer(options) for long-lived serve() entrypointscreateTestRuntime(options) for testsgetNamespacedRuntime(key, options) for persistent sessionsdisposeNamespace(key, options?) for hard-deleting a namespacediagnostics() for host-level diagnosticsclose() to shut everything downCreateIsolateHostOptions currently supports engine: "auto" and daemon options such as socketPath, entrypoint, cwd, timeoutMs, and autoStart.
Runtime creation options support:
bindings for host-owned capabilitiescwd for path resolutionexecutionTimeout for eval/test execution limitsmemoryLimitMB for isolate memory limitsnestedHost for controlling sandbox-created child runtimes, or false to disable nested host accesscreateModuleResolver()createModuleResolver() returns a fluent builder. You can mix and match:
virtual(specifier, source, options) for inline modulesvirtualFile(specifier, filePath, options) for a host file mapped to a virtual specifiersourceTree(prefix, loader) for lazy source loading under a virtual pathmountNodeModules(virtualMount, hostPath) for package resolution from a real node_modulesfallback(loader) for custom last-resort resolutioncreateFileBindings()createFileBindings({ root, allowWrite }) creates a filesystem bridge that stays inside the configured root directory. Attempts to escape that root are rejected, and write operations are disabled unless allowWrite is true.
The typecheck helpers let you validate sandbox code against supported capability profiles before executing it.
import {
formatTypecheckErrors,
getTypeProfile,
typecheck,
} from "@ricsam/isolate";
const profile = getTypeProfile({
profile: "browser-test",
capabilities: ["files"],
});
console.log(profile.include);
const result = typecheck({
code: "page.goto('/')",
profile: "browser-test",
});
if (!result.success) {
console.error(formatTypecheckErrors(result.errors));
}
Built-in profiles:
backendagentbrowser-testCapabilities can extend a profile with fetch, files, tests, browser, tools, console, crypto, encoding, and timers.
@ricsam/isolate/playwrightcreatePlaywrightSessionHandler() builds a handler-first browser binding for namespaced sessions and other Playwright-backed runtimes.
It accepts host callbacks such as:
createContextcreatePagereadFilewriteFileevaluatePredicateIt returns:
handler for bindings.browser.handlergetCollectedData() for collected browser artifactsgetTrackedResources() for active contexts and pagesclearCollectedData() to reset collected artifactsonEvent(callback) for sync-only, best-effort Playwright event subscriptionsisolate-daemonThe package also exposes an isolate-daemon binary:
isolate-daemon --socket /tmp/isolate.sock
By default, createIsolateHost() will auto-start a daemon when needed. You can also point the host at an already-running daemon with daemon.socketPath, or disable auto-start with daemon.autoStart: false.
FAQs
Unified runtime host for app servers, script runtimes, browser runtimes, module resolution, file bindings, and typechecking
The npm package @ricsam/isolate receives a total of 36 weekly downloads. As such, @ricsam/isolate popularity was classified as not popular.
We found that @ricsam/isolate demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
A mini Shai-Hulud campaign compromised Red Hat Cloud Services npm packages to steal developer and CI/CD secrets during installation.

Research
/Security News
The North Korean malware loader hides in a Packagist-listed package and its GitHub branch to fetch and execute remote code in a likely Contagious Interview-style lure.

Security News
The Rust project is moving toward formal rules on LLM use in contributions after months of internal debate over maintainer burden, code quality, and contributor experience.