npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@iso4/sandbox

v0.4.1

Published

Fast, sandboxed V8 isolate runtime for agent-generated JavaScript. Two-process architecture for crash isolation.

Readme

@iso4/sandbox

Fast, sandboxed V8 isolate runtime for agent-generated JavaScript. Runs user code in a separate Rust process for full crash isolation — an OOM or panic in the sandbox kills only the subprocess, not your host application.

Built for the AI-agent prefix/postfix pattern: precompile host setup (globals, libraries, tool bindings) once into a V8 startup snapshot, then run many agent-generated code strings against the snapshot in parallel.

Status: core execution works end-to-end. Not yet at 1.0.

Install

npm i @iso4/sandbox
# hardened fetch defaults (recommended):
npm i @iso4/fetch

Quick start

import { createSandbox } from '@iso4/sandbox'
import { createSafeFetch } from '@iso4/fetch'

const sandbox = await createSandbox()

// Compile host setup once into a V8 snapshot
const prefix = await sandbox.prepare({
  code: `
    const config = { apiBase: 'https://api.example.com' }
    globalThis.config = config
  `,
  globals: {
    fetch: createSafeFetch({ policy: ({ host }) => host === 'api.example.com' }),
  },
})

// Run agent-generated code against the snapshot — as many times as needed
const result = await prefix.execute({
  code: `
    const res = await fetch(config.apiBase + '/users')
    export default { count: res.length }
  `,
  limits: { cpuTimeMs: 200, wallTimeMs: 5_000, memoryMb: 64 },
})

if (result.ok) {
  console.log(result.exports.default) // { count: 42 }
} else {
  console.error(result.error.code, result.error.message)
}

await sandbox.dispose()

sandbox.prepare() and prefix.execute() are the current names. The former names — sandbox.precompile() and prefix.run() — remain as deprecated aliases with identical behavior and will be removed in a future major.

How globals work

globals wires any non-reserved name directly into the sandbox's global object as a bridge stub. The bridge is fully generic — fetch is not special-cased:

const options = {
  globals: {
    searchWeb: async (query: string) => {
      const res = await fetch(`https://api.example.com/search?q=${encodeURIComponent(query)}`)
      return res.json()
    },
  }
}

Functions in bridge return values are currently dropped — return plain data, not class instances with methods.

TypeScript-checked rebinding

prepare() infers the globals shape G from what you pass, and the returned Prefix<G> only allows rebinding those names at run time:

const prefix = await sandbox.prepare({
  globals: { fetch: defaultFetch, myTool: defaultTool },
})

prefix.execute({ globals: { fetch: perUserFetch } }) // ✅ rebind one
prefix.execute({ globals: { unknown: handler } }) // ❌ TS error

Resource limits

prefix.execute({
  code: agentCode,
  limits: {
    cpuTimeMs: 200, // active JS execution only (await time excluded)
    wallTimeMs: 5_000, // hard cap including async waits
    memoryMb: 64, // V8 heap + ArrayBuffer budget
    maxBridgeCalls: 10, // max host-bridge calls per run (0 = unlimited)
    maxBridgePayloadBytes: 0, // max bytes per bridge call (0 = 64 MiB framing cap)
  },
})

Async context (AsyncLocalStorage)

Run/postfix code can import a minimal, Node-compatible AsyncLocalStorage to carry an ambient value across await points — concurrency-safe, unlike a module variable:

prefix.execute({
  code: `
    import { AsyncLocalStorage } from 'node:async_hooks'
    const als = new AsyncLocalStorage()
    export default await als.run('trace-42', async () => {
      await somethingAsync()
      return als.getStore()   // 'trace-42', even several awaits deep
    })
  `,
})

Only run(store, callback, ...args) and getStore() are provided. Built on V8's continuation-preserved embedder data; no promise hooks, so it's free unless used. Not available in prepare() (prefix) code — it's for the postfix. See DESIGN.md §16.

Result shape

type RunResult
  = | { ok: true, exports: SandboxExports, stdout: string[], stderr: string[], durationMs: number, cpuTimeMs: number, bridgeCalls: BridgeCallEntry[] }
    | { ok: false, error: RunError, stdout: string[], stderr: string[], durationMs: number, cpuTimeMs: number, bridgeCalls: BridgeCallEntry[] }

// durationMs — wall-clock time of the run; cpuTimeMs — active V8 execution
// time (bridge waits excluded). Both measured in the runtime, µs resolution.

interface BridgeCallEntry { // recorded in the Rust runtime; one per attempt, in order
  name: string // 'fetch', 'myTool', or '<specifier>.<path>' for host-module imports
  startMs: number // offset from run start (same clock as durationMs)
  durationMs: number // round-trip the sandbox waited (handler + IPC)
  argBytes: number // serialized call payload size
  responseBytes: number // serialized response value size (0 on handler error)
  ok: boolean
  blocked: boolean // blocked by a limit runtime-side; never reached the host
}

interface RunError {
  code: RunErrorCode
  name: string
  message: string
  stack?: string
  fields?: Record<string, unknown> // all other own-enumerable props of the thrown error
}

run() never throws for sandboxed failures — only for infrastructure errors (subprocess crashed, binary not found). ok: false with an error code is the normal failure path.

Thrown errors keep their identity across the bridge, in both directions:

  • Sandbox → host: an uncaught sandbox throw surfaces as ERR_USER_CODE with the error's real name, message, stack, and every other own-enumerable property under error.fields (namespaced so a custom code property can't collide with the iso4 error.code). name/message/stack are reserved and never appear inside fields.
  • Host → sandbox: a host handler that throws rejects the sandbox call with a real Error carrying the same name (instanceof TypeError works for built-ins) and its extra properties re-attached directly (e.status, e.reason, …). Sandbox code can catch it and continue; uncaught it fails the run with ERR_HOST_BRIDGE. The host stack never crosses into the sandbox.

Architecture

V8 runs in a separate Rust subprocess communicating over a Unix domain socket. A pool of connections (one per maxIsolates slot) provides concurrency — five concurrent prefix.execute() calls each get their own slot and execute in parallel.

License

MIT