URL: /drover/guides/sandboxes

---
title: Sandboxes
description: 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.

| 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.

<Warning>
`just-bash` runs without VM-level isolation. It is a strong boundary against
filesystem and network reach, not a defence against a hostile native exploit.
For untrusted code at scale, still layer an OS sandbox underneath.
</Warning>

## `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
});
```

| 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:

```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:

| 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:

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.
