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.bashis 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:
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:
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.
| Option | Effect |
|---|---|
mounts | docker-style host↔sandbox bind mounts. Default []. |
files | seed files for the in-memory base filesystem |
env | base environment inside the sandbox |
python | enable python3 (CPython→WASM). Off by default. |
javascript | enable js-exec (QuickJS). Off by default. |
network | curl/wget URL allowlist. Off by default. |
timeoutMs | per-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.
import { createNoneSandbox } from "@drover/sandbox";
const sandbox = createNoneSandbox({
allowedRoots: ["/tmp/safe"],
allowShell: false, // default
});| Operation | Constraint |
|---|---|
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:
// 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
createNoneSandbox(); // allowedRoots: []Empty means no boundary — drover treats it as “trusted runtime.”
Wiring a stronger sandbox
The interface:
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:
| adapter | provides |
|---|---|
sandbox-exec (macOS SBPL) | per-call seatbelt profile constraining read+exec at the kernel level |
firejail (Linux) | namespaced filesystem + network isolation |
docker | full container per run, network gated |
daytona / Cloudflare container | remote 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:
- Does it call
sandbox.runwith a user-supplied path as argv? Gate the path withsandbox.assertPathAllowed(target)before spawning.grep,find,lsdo this. (Underjust-bashthe check is a no-op; undernoneit is load-bearing — write tools to satisfy both.) - Does it shell out? It inherits the sandbox’s shell limitations — only inject under capability-checked sandboxes.
- Does it read or write a user-supplied path? Use
readFile/writeFiledirectly; they enforce the boundary.