Skip to content

@doable/dovault

Zero-overhead runtime jail for Node.js processes. Three independent layers you can compose:

  1. Config Guard — lock server-side config files (Vite, Tailwind, Postgres, …) to safe templates.
  2. Process Jail — Node.js Permission Model wrapper.
  3. Resource Limiter — OS-level limits via systemd cgroups (Linux) or Win32 Job Objects (Windows).

Lives at packages/dovault/. Zero runtime dependencies.

Quick start

import { createVault } from "@doable/dovault";

const vault = createVault({
  resourceLimits: { memoryMax: "150M", cpuQuota: "30%", tasksMax: 32 },
});

const proc = await vault.spawn("vite", ["--port", "3100"], {
  cwd: projectPath,
  jail: projectPath,         // file system root the process can read/write
});

proc.on("exit", (code) => console.log("vite exited", code));

The spawned process can only:

  • Read & write inside projectPath.
  • Use up to 150 MB RAM, 30% of one CPU, 32 tasks.

API

createVault(options)

Returns a Vault instance.

Option Notes
resourceLimits { memoryMax, cpuQuota, tasksMax } — strings as accepted by systemd, or numbers (bytes/percent)
backend 'auto' \| 'systemd' \| 'windows' \| 'win-heap' \| 'direct'

Vault#spawn(command, args, opts)

spawn(
  command: string,
  args: string[],
  opts?: SpawnOptions,
): Promise<JailedProcess>

SpawnOptions:

Field Notes
cwd Working directory
jail Filesystem root the process is confined to
env Environment variables
extraReadPaths Extra paths the process may read (e.g. node_modules)
extraWritePaths Extra writable paths
permissions { allowFs?: boolean, allowNet?: boolean, allowChildProcess?: boolean, allowWorker?: boolean }

Vault#exec(command, args, opts)

Same as spawn but waits for completion and returns { stdout, stderr, exitCode }.

Standalone primitives

If you want one layer without the others:

import { ConfigGuard, ProcessJail, ResourceLimiter } from "@doable/dovault";

ConfigGuard.lock("/path/to/vite.config.ts", "vite-default");
const wrap = ProcessJail.wrap("node", ["server.js"], { jail: projectPath });
const wrap2 = ResourceLimiter.wrap(wrap.command, wrap.args, { memoryMax: "200M" });

Backends

Backend Where What it does
SystemdBackend Linux with systemd systemd-run --user --scope --property=MemoryMax=… etc.
WindowsBackend Windows Win32 Job Objects (kill-on-close, memory limit)
WindowsHeapBackend Windows fallback V8 heap cap via --max-old-space-size
DirectBackend Anywhere No isolation — for dev/macOS

createVault({ backend: "auto" }) picks the strongest available.

Why the layers are independent

  • A static-site builder may need Config Guard but no resource limits.
  • A short-lived script may need Process Jail but no Config Guard.
  • A long-running daemon needs all three.

Composing them only when needed keeps overhead at literal zero in the cases that don't.

Used in Doable

The API spawns the per-project Vite dev server through vault.spawn(). See services/api/src/projects/dev-server.ts.

The AI agent's worker processes (started by @doable/docore) also go through dovault when the configured isolation backend is "direct" — adding a permission model layer even without nsjail.

See also