Compartments

Run code you didn't write.
Grant it nothing you didn't mean to.

A Compartment is isolation as a language primitive: a fresh realm, capabilities only by explicit grant, and a timeout it cannot escape. Run a plugin, a dependency's callback, or a model's output in-process, without handing it the machine.

run-plugin.ts
// a plugin we did not write, run with exactly two capabilities
const plugin = `
  const total = cart.reduce((n, i) => n + i.price, 0);
  discount(total > 100 ? 0.1 : 0);
`;

const c = new Compartment({
  timeout_ms: 50,                    // a budget it can't escape
  globals: {                         // the only things it can reach
    cart: [{ price: 60 }, { price: 55 }],
    discount: (rate) => applyDiscount(rate),
  },
});

c.evaluate(plugin);
c.evaluate("typeof fetch");   // the plugin never sees the network
apply discount: 0.1
typeof fetch → undefined

No ambient authority

The realm starts empty

Ordinary JavaScript runs with everything the process can do lying around on the global object. A Compartment starts with none of it. require, fetch, process: not there unless you hand them over. Authority is the reference, so if the code does not hold it, it cannot use it.

const c = new Compartment();

c.evaluate("typeof require");  // undefined
c.evaluate("typeof fetch");    // undefined
c.evaluate("typeof process");  // undefined

// grant exactly one capability, nothing more
const box = new Compartment({
  globals: { readKey: () => "sk-live-..." },
});
box.evaluate("readKey()");      // "sk-live-..."

The one trusted root

Everything grows from the host realm

Your program runs in the host realm: the single privileged realm that actually holds authority, the filesystem, the network, the process, and the only thing that can mint a Compartment or grant a capability into one. A Compartment is a child realm. It starts empty, and it can only ever hold what the host hands down.

This inverts the usual model. Ordinarily every module in a process shares one ambient global carrying all authority, and isolation is a convention you hope holds. Here authority flows one way only: down, by explicit reference, from host to child. A grant is usually a host function closing over real authority, so the tenant calls through the host rather than holding the thing itself. The host supervises; the compartment only executes.

How the host realm works →

// your program runs in the host realm:
// the one realm that holds real authority
import { readFile } from "node:fs/promises";

// mint a child; grant a narrow, wrapped
// slice of the host's own authority
const tenant = new Compartment({
  globals: {
    readConfig: () => readFile("/cfg.json"),
  },
});

tenant.evaluate("readConfig()");    // ok
tenant.evaluate("typeof readFile");  // undefined
const c = new Compartment({ timeout_ms: 50 });

c.evaluate(`
  try {
    while (true) {}      // a hostile loop
  } catch (e) {
    "escaped?"           // tries to swallow it
  }
`);
cruft: evaluation error: Interrupted

A deadline below the language

A timeout it cannot escape

The time budget is enforced beneath JavaScript. The interrupt is not an exception, so there is no catch to wrap it in and no event to intercept. A tenant cannot defeat the budget by trapping it, because from inside the language there is nothing to trap. Runaway and hostile loops alike are stopped.

The boundary holds both ways

Nothing leaks across it

A compartment has its own copies of the intrinsics. Code inside cannot reach out to poison the host, and one compartment cannot see into another. Isolation is real, not cooperative.

No prototype pollution

const c = new Compartment();
c.evaluate("Array.prototype.hacked = 1");

[].hacked;   // host: undefined

No cross-talk

const a = new Compartment();
const b = new Compartment();
a.evaluate("globalThis.secret = 42");

b.evaluate("typeof secret");  // undefined

Whole-process capability control

Seal the process from the outside

Where a Compartment isolates a region of code from inside, the sandbox flags constrain an entire cruft process, without changing a line of source. Cruft treats I/O as a capability: --audit records every capability a program uses, and --sealed denies them unless the program declares what it legitimately needs.

# watch every I/O capability a program touches
$ cruft --audit app.mjs
file:///app.mjs   stdio   write(stdout)

# deny all I/O; ungranted operations throw
$ cruft --sealed app.mjs
TypeError: no stdio capability granted
// package.json: grant exactly what's needed
{ "caps": { "stdio": { "stdout": true } } }
# your code runs normally;
# node_modules is denied I/O
$ cruft --sealed-deps app.mjs

# a transitive dependency cannot reach
# the filesystem or network behind your back

The supply-chain middle ground

Box just the dependencies

--sealed-deps is the common case: your first-party code keeps normal access, but everything under node_modules is denied I/O. A compromised or curious transitive dependency cannot quietly touch the disk or open a socket. --allow-net-loopback re-grants just local listen authority when you need it.

The installer

Dependencies arrive without running code

cruft install runs no lifecycle scripts. Not preinstall, install, postinstall, or prepare, from the root or from any dependency. There is no process spawn anywhere in the installer, which closes the largest npm supply-chain vector by leaving it out. A skipped script is reported, so nothing fails invisibly.

Every tarball is verified before it is extracted: its sha512 subresource-integrity hash is checked byte for byte against the lockfile, a mismatch aborts the install and nothing lands on disk, and a package that ships no integrity at all is refused rather than trusted. The tarball URL must be HTTPS and the registry's own origin, so a swapped mirror is rejected before a byte is fetched.

The lockfile is origin-pinned, not signature-authenticated: the installer still trusts the registry it fetches from, so it is not yet safe against a fully compromised registry.

Read more about Cruft PM →

// cruft-lock.json pins every dependency by content
"[email protected]": {
  "integrity": "sha512-...",   // checked before extraction
  "tarball": "https://registry.npmjs.org/left-pad/..."
}
$ cruft install
verifying 214 packages against the lockfile
no lifecycle scripts run
integrity mismatch: [email protected]   install aborted
# advisory: report supply-chain risks, then run npm
$ cruft trust install -- npm install
lifecycle-script     esbuild        postinstall
native-addon         node-sass      binding.gyp
known-malicious      [email protected]   MAL-2024-1234
known-vulnerability  [email protected]  GHSA-...

# enforce: stop known-bad before npm launches
$ cruft trust install --enforce -- npm install
blocked: [email protected] (known-malicious)   exit 77

Before an npm install runs

Known-bad packages, stopped at the door

cruft trust install is a preflight over what an npm install is about to touch. It checks each package against the OSV advisory database at its one pinned endpoint, api.osv.dev, and classifies the risks it finds: lifecycle scripts, native addons, known vulnerabilities, and known-malicious advisories (the OSV MAL- class). By default it reports them and lets the install proceed.

With --enforce it stops a known-bad package before npm ever launches, exiting 77. Enforcement blocks lifecycle scripts, native addons, and known-malicious advisories; an ordinary vulnerability is reported but not blocked, and if the OSV lookup cannot reach the network it reports no advisories rather than failing the install. The same lookup is available inside an agent as the osv.query tool.

Agents and their tools →

The Node front door

Run real Node under an OS sandbox

cruft wrap runs your existing node, npm, and npx commands as a supervised child. The child is the real Node binary, so nothing about how it behaves changes; what Cruft adds is a boundary around it. --sandbox=macos-strict makes that boundary an OS-level one: on macOS the child runs under sandbox-exec with the network denied, filesystem writes denied, and a scrubbed environment. A write or a socket connect fails with EPERM.

This is the front door's one enforced control, and it is exact about its edges. It is macOS-only today, it denies general process exec but re-allows the child to launch its own binary, so it is not complete child-process denial, and the profile names (ci, paranoid, locked) are reporting tone, not stronger enforcement. cruft policy and cruft doctor print each control's real level.

The Node front door →

# run your existing node/npm/npx, supervised
$ cruft wrap -- node app.js

# put a real OS sandbox around the child (macOS)
$ cruft wrap --sandbox=macos-strict -- node build.js
sandbox-profile=macos-strict
controls=filesystem-write,network,environment,external-process-exec
// inside the child: no writes, no network, scrubbed env
fs.writeFileSync("/tmp/x", "1")   // EPERM

Isolation you can afford

Cheap enough to use by default

Because a worker compartment is a realm over one shared, garbage-collected heap rather than a whole VM per worker, isolating at high cardinality stops being a memory emergency. The same production worker loads, at 2048 workers delivering 2000 events each:

Workload (2048 workers)CruftNode worker_threads
webhook transform274 MiB / 1.0 s6,247 MiB / 27.5 s
API policy272 MiB / 0.9 s8,265 MiB / 32.2 s

Peak resident memory and wall time, same events delivered, both exiting cleanly. A per-plugin, per-request, or per-agent compartment is an ordinary thing to spin up, not a cost you have to ration.

The whole model

Compartments, capabilities, and the sealed process are one idea at three sizes: name the boundary, check what crosses it, refuse the unsound case loudly. Read how it fits the rest of the stack.