Skip to content

Confined command runner

runConfinedCommand runs one command inside the same confinement the engine uses for its own sandboxed processes. It is a supported public seam over existing pieces: the kernel bubblewrap invocation builder, the sandbox posture decision, the egress proxy allowlist and the confined process lifecycle. It adds no confinement logic of its own, so a command gets exactly the namespaces, dropped capabilities and restrictions the engine applies elsewhere. Tools the kernel dispatches get the same set (see Native kernel confinement).

import { runConfinedCommand } from '@hellmai/lumenflow-host/confinement';

const run = await runConfinedCommand({
  command: '/bin/sh',
  args: ['-c', 'make test'],
  cwd: workDir,
  mounts: [
    { path: sourceDir, mode: 'ro' },
    { path: workDir, mode: 'rw' },
  ],
  env: { CI: '1' },
  network: 'off',
  limits: { wallTimeoutMs: 60_000, maxStdoutBytes: 262_144, maxStderrBytes: 65_536 },
  signal: controller.signal,
});

run.stdout.on('data', (chunk) => process.stdout.write(chunk));
const result = await run.wait();
// { code, signal, timedOut, truncated, aborted, confined, posture, stderrTail }
OptionMeaning
command, argsThe program and its arguments. Use an absolute path, or a program found on the default PATH.
cwdWorking directory. When confined it must lie inside one of the mounts.
mountsExtra mounts, each { path, mode: 'ro' | 'rw' }. A path is absolute and canonical (no symlink in it) and is bound at the same path.
envThe only environment the command sees, plus PATH=/usr/bin:/bin unless you set it. Nothing is inherited from the calling process.
network'off' (default) leaves the command with only a loopback interface. { allowlist, forwarderPath } is described below.
limitswallTimeoutMs (default none), maxStdoutBytes and maxStderrBytes (default 1 MiB each).
signalAn AbortSignal that ends the whole process tree.
securityEventEmitterOptional. Receives the launch’s sandbox:admission events (its engine admission verdict and typed reasons), and is forwarded to the allowlist proxy it starts. Observation only: nothing about the launch changes.
recordAdmissionOptional. Receives the launch’s admission record (SurfaceAdmissionRecord). Observation only.

The system directories /usr, /bin, /sbin, /lib, /lib64 and /etc are always bound read-only. Anything else the command needs, including a Node toolchain, must be listed in mounts. A write outside the rw mounts, or to an ro mount, fails inside the command.

The returned handle exposes pid, streamed stdout and stderr, confined, posture, wait() and stop().

  • wait() resolves, and never rejects, once the command has exited and its output was delivered, with code, signal, timedOut (the wall timeout fired), truncated (a stream exceeded its cap), aborted (the signal or stop() ended it), confined, posture and stderrTail.
  • An output stream delivers at most its cap. Bytes beyond it are discarded while the pipe keeps draining, so the command is never blocked by a full pipe and memory stays bounded.
  • stop() ends the command and everything it started, whatever stage of start-up it is in. It is bounded and idempotent: every call returns the same result.
  • A timeout, an abort and stop() all end the whole tree, including processes the command started in the background.

network: { allowlist: ['registry.example.test:443'], forwarderPath } lets the command reach only the listed exact host:port pairs, through the confined egress proxy. Entries follow the per-job allowlist grammar (exact host and port, no bare host, wildcard or CIDR) and an invalid entry rejects the call with proxy.job-allowlist.* before anything starts. The proxy still vets every resolved address. forwarderPath is the path of the standalone forwarder bridge that ships with the CLI package; the host package never depends on the CLI, so the caller supplies it. onDenied receives each denial, and violations maps each denial to a kernel sandbox:violation event. The command needs the Node toolchain mounted read-only, as for any other program outside the system directories.

Confinement needs Linux (x64 or arm64) with a working bwrap that supports the required flags. Elsewhere runConfinedCommand rejects with confinement.refused and the posture reason, before anything is spawned. A network allowlist is refused without confinement (confinement.allowlist-requires-confinement), because nothing would enforce it.

CodeCause
confinement.refusedThe posture is not enforced.
confinement.allowlist-requires-confinementAn allowlist was requested for an unconfined run.
confinement.mount-not-absoluteA mount path is not absolute.
confinement.cwd-not-mountedcwd is not inside any mount.
confinement.env-name-invalidAn env name is not a valid variable name.
confinement.limit-invalidA limit is not a positive integer.
confinement.abortedThe signal was already aborted.
confinement.proxy-not-readyThe allowlist proxy did not report ready.