Sandboxes

How drover constrains filesystem and shell access — and what it doesn't.

A SandboxAdapter is the boundary between drover tools and the host filesystem / processes. drover ships two:

  • just-bash — the default. A virtual sandbox: an in-process bash interpreter with its own filesystem. A real isolation boundary — the agent reaches the host only through explicit mounts, has no network unless you configure one, and runs under execution limits. bash is safe here.
  • none — an in-process adapter that enforces a path allowlist for file I/O. Not a security boundary; the shell escapes it. For trusted runs.

just-bash — the default

Every run gets a just-bash sandbox unless you pass options.sandbox. The facade mounts the run’s cwd read-write:

ts
import { createJustBashSandbox } from "@drover/sandbox-just-bash";

const sandbox = createJustBashSandbox({
  mounts: [{ source: process.cwd(), target: process.cwd(), mode: "readwrite" }],
});

Mounts are docker-style and explicit. With no mounts the sandbox is fully in-memory — the agent sees no host files at all. Each mount is readonly (default) or readwrite:

ts
createJustBashSandbox({
  mounts: [
    { source: "/repo", target: "/repo", mode: "readwrite" }, // writes hit disk
    { source: "/refs", target: "/refs", mode: "readonly" },  // COW; host untouched
  ],
});

Either way the agent cannot escape a mount root — no .., absolute-path, or symlink traversal reaches outside source. An unmounted path simply does not exist.

OptionEffect
mountsdocker-style host↔sandbox bind mounts. Default [].
filesseed files for the in-memory base filesystem
envbase environment inside the sandbox
pythonenable python3 (CPython→WASM). Off by default.
javascriptenable js-exec (QuickJS). Off by default.
networkcurl/wget URL allowlist. Off by default.
timeoutMsper-call default timeout (30 s)

Because it is a genuine boundary, just-bash advertises capabilities.shell: true unconditionally — bash composes by default, no opt-in needed. assertPathAllowed always succeeds: the mount namespace is the boundary, so there is nothing to gate against.

none — the trusted escape hatch

none is in-process and enforces a path allowlist. Use it when the agent is meant to operate on the real host with the runtime’s own reach — CLIs, eval runners, CI — or when isolation already exists at another layer.

ts
import { createNoneSandbox } from "@drover/sandbox";

const sandbox = createNoneSandbox({
  allowedRoots: ["/tmp/safe"],
  allowShell: false, // default
});
OperationConstraint
readFile(path)resolved + realpath → must be inside allowedRoots
writeFile(path)resolved + realpath → must be inside allowedRoots (parent ancestor checked when path doesn’t exist)
assertPathAllowed(path)same realpath + allowedRoots check, surfaced for tools that pass argv to spawned processes (grep, find, ls)
run(cmd, args)only spawn cwd is checked. argv is opaque.

What bash does to the none boundary

bash runs /bin/sh -c <command>. Under none the shell can cat /etc/passwd, ls /, write anywhere your user can. The sandbox checks cwd — that’s all it can check. So drover refuses to compose bash into a run unless the sandbox advertises shell capability:

ts
// bash is silently dropped from the toolset
createNoneSandbox({ allowedRoots: [cwd] });

// bash is composed, with the documented escape
createNoneSandbox({ allowedRoots: [cwd], allowShell: true });

tools: ["bash"] passed to the first sandbox is a no-op — silently dropping is safer than silently exposing. (Under just-bash, bash always composes.)

Empty allowedRoots

ts
createNoneSandbox(); // allowedRoots: []

Empty means no boundary — drover treats it as “trusted runtime.”

Wiring a stronger sandbox

The interface:

ts
interface SandboxAdapter {
  readonly id: string;
  readonly capabilities: SandboxCapabilities;
  run(cmd, args, opts?): Effect<ExecResult, SandboxError>;
  readFile(path): Effect<string, SandboxError>;
  writeFile(path, contents): Effect<void, SandboxError>;
  resolvePath(path, cwd): string;
  assertPathAllowed(path): Effect<void, SandboxError>;
}

Adapter shapes drover doesn’t ship; common production picks:

adapterprovides
sandbox-exec (macOS SBPL)per-call seatbelt profile constraining read+exec at the kernel level
firejail (Linux)namespaced filesystem + network isolation
dockerfull container per run, network gated
daytona / Cloudflare containerremote VM, attach via API

Advertise capabilities.shell: true only when the spawned process genuinely cannot escape.

Path / sandbox boundary checklist

When auditing a new tool:

  1. Does it call sandbox.run with a user-supplied path as argv? Gate the path with sandbox.assertPathAllowed(target) before spawning. grep, find, ls do this. (Under just-bash the check is a no-op; under none it is load-bearing — write tools to satisfy both.)
  2. Does it shell out? It inherits the sandbox’s shell limitations — only inject under capability-checked sandboxes.
  3. Does it read or write a user-supplied path? Use readFile/writeFile directly; they enforce the boundary.

Type to search…

↑↓ navigate open esc close