Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,25 +80,26 @@ once it's there? That's the sandbox.
When profiles are configured in `project.conf`, the host daemon does not spawn
passthrough commands directly. It wraps each one inside a
[bubblewrap](https://github.com/containers/bubblewrap) sandbox. Bubblewrap uses
Linux user namespaces to create a thin, unprivileged container around the
command with exactly the capabilities it needs and nothing more.
Linux namespaces to create a thin container around the command. The profile
defines the binaries, paths, environment, and network policy supplied to it.

A sandbox profile is a small YAML file that declares:

- Which exact host binaries the command may access (bind-mounted read-only)
- Which directories are visible and whether they are writable
- Which exact host binaries the command may access
- Which host paths are visible and where they appear in the guest
- Whether network access is permitted
- Which environment variables the command inherits

Bunkerbox ships profiles for common build systems — `rust`, `make`, `go`,
`node`, `python` — and you can write your own. When multiple profiles are
active, they merge: the union of all binaries and directories is available to
the sandboxed command.
active, they merge: the union of all binaries and paths is available to the
sandboxed command.

Inside the sandbox, the command sees a scratch `/home`, an empty `/tmp`, its
own `/proc`, no network, and only the binaries you explicitly allowed. It
cannot read your SSH keys, curl a payload, enumerate host processes, or write
anywhere outside the overlay workspace.
own `/proc`, no network, and only the binaries and paths you explicitly
allowed. Home-relative cache paths are deliberate writable carryover paths;
profile declarations are trusted host policy, not a complete rogue-process
capability model.

See the [Profiles guide](guides/profiles.md) for the full reference.

Expand Down
12 changes: 6 additions & 6 deletions docs/config/project.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ sandbox. The host daemon constructs a minimal Linux namespace from the profile
rules before spawning the real command.

- Only the binaries listed in the profile are visible inside the sandbox.
They are bind-mounted read-only at the paths where the command expects them.
- Filesystem access is limited to the directories the profile declares.
They are bind-mounted at the paths where the command expects them.
- Filesystem access is limited to the `paths` declared in the profile.
Everything else is invisible.
- The network is isolated (`--unshare-net`) unless a profile explicitly
allows it.
Expand All @@ -129,12 +129,12 @@ path. The format is the same as the built-ins. See the [Profiles
guide](../guides/profiles.md) for the full reference.

**Merging.** When multiple profiles are configured, their rules are merged.
The union of all binaries, read-only directories, writable directories, and
environment variables is available to the sandboxed command.
The union of all binaries, paths, and environment variables is available to
the sandboxed command.

**When profiles are empty** (the default), passthrough commands run directly
on the host with no sandbox — the pre-bwrap legacy behavior. This is useful
when you trust the tool completely or are debugging, but it offers no
on the host with no sandbox. This is useful when you trust the tool completely
or are debugging, but it offers no
additional isolation beyond the vsock whitelist.

### `image`
Expand Down
20 changes: 14 additions & 6 deletions docs/guides/passthrough.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ When the AI agent invokes one of those commands, the symlink points to
`bunkerbox-vscomm`, which proxies the call through a virtio-vsock channel to a
daemon running on the host. The daemon checks the whitelist one more time,
spawns the real command inside the overlay workspace at `.bunkerbox/workspace/`,
and streams stdout, stderr, and the exit code back.
and streams the requested command's stdout, stderr, and exit code back. Daemon
and sandbox-launcher diagnostics are kept separate from the command stream.

```
┌─ Bunkerbox VM ──────────────────────────────────────┐
Expand All @@ -35,7 +36,7 @@ and streams stdout, stderr, and the exit code back.
│ │ │ │
│ │ └─ symlink → bunkerbox-vscomm │
│ │ │ │
│ │ │ vsock (port 9999)
│ │ │ toolchain vsock (port 9999) │
│ │ ▼ │
│ │ "run make build in /workspace" │
└────┼───────────────────────────────────────────────┘
Expand All @@ -52,10 +53,17 @@ and streams stdout, stderr, and the exit code back.
└────────────────────────────────────────────────────┘
```

The AI agent sees standard output exactly as if `make` ran locally. The host
daemon runs inside the overlay workspace, so all output — compiled binaries,
generated files, test results — lands in the upper layer of the overlay and is
auto-synced back to your real repo when the container exits.
The AI agent sees the requested command's standard output and standard error as
if it ran locally. The host daemon runs inside the overlay workspace, so all
output — compiled binaries, generated files, and test results — lands in the
upper layer of the overlay and is auto-synced back to your real repo when the
container exits. Bunkerbox and sandbox-launcher diagnostics never enter the
command stream; use `--log PATH` to retain those diagnostics.

The command channel uses vsock port `9999`. TUI status and dialog commands use
the separate `bunkerbox-status` client and vsock port `10000`; the
`bunkerbox-vscomm` command client remains silent when its own protocol fails
and makes a best-effort error notification through port `10000`.

## Configuration

Expand Down
85 changes: 46 additions & 39 deletions docs/guides/profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,28 @@ Bunkerbox ships five profiles that cover the most common build systems. You
can use them by name — no files to write, no paths to manage.

**`rust`** — for projects with a `Cargo.toml`. Provides `cargo`, `rustc`,
`rustfmt`, and `cc`. Mounts `~/.cargo` and `~/.rustup` read-write so your
crates and toolchains are cached. Sets `CARGO_HOME` and `RUSTUP_HOME` so cargo
knows where to look.
`rustfmt`, and `cc`. Carries over `.cargo` and `.rustup` into the sandbox home
and sets `CARGO_HOME` and `RUSTUP_HOME` to their guest paths.

**`make`** — for projects with a `Makefile`. Provides `make`, `gcc`, `g++`,
`ar`, `ld`, `as`, and `strip`. No writable directories by default — make
output goes to the overlay workspace.

**`go`** — for projects with a `go.mod`. Provides `go` and `gofmt`. Mounts
the Go toolchain directory read-only and `~/go` read-write for the module
cache. Sets `GOROOT` and `GOPATH`.
the Go toolchain directory and carries over `go` and `.cache/go-build`. Sets
`GOROOT`, `GOPATH`, and `GOCACHE` to guest paths.

**`node`** — for projects with a `package.json`. Provides `node`, `npm`, and
`npx`. Mounts `~/.npm` and `~/.node-gyp` read-write so packages are cached
between runs.
`npx`. Carries over `.npm` and `.node-gyp` so packages are cached between
runs.

**`python`** — for projects with `pyproject.toml` or `setup.py`. Provides
`python3` and `pip3` (also aliased as `python` and `pip`). Mounts the pip
cache read-write.
`python3` and `pip3` (also aliased as `python` and `pip`). Carries over the
`.cache/pip` cache.

All profiles share the same base rules: system libraries (`/lib`, `/lib64`,
`/usr/lib`) are mounted read-only, the network is disabled, and the shell is
`/bin/sh`.
`/usr/lib`) are available at their standard paths, the network is disabled,
and the shell is `/bin/sh`.

## Using profiles

Expand All @@ -66,7 +65,7 @@ both Cargo and Make, add both `rust` and `make`. The sandboxed command will
have access to the union of all binaries and directories from both profiles.

If `profiles` is empty or absent, passthrough commands run directly on the
host with no sandbox — the pre-bwrap legacy behavior.
host with no sandbox.

## Custom profiles

Expand All @@ -81,14 +80,12 @@ bin:
my-compiler: /opt/toolchain/bin/my-compiler
my-linker: /opt/toolchain/bin/my-linker

ro:
- /lib
- /lib64
- /usr/lib
- /opt/toolchain/lib

rw:
- "${HOME}/.cache/my-toolchain"
paths:
- src: /lib
- src: /lib64
- src: /usr/lib
- src: /opt/toolchain/lib
- src: .cache/my-toolchain

env:
TOOLCHAIN_HOME: /opt/toolchain
Expand All @@ -115,18 +112,27 @@ resolves the path against your `PATH` before mounting, so you can write
`cargo: /usr/bin/cargo` and it will still work if cargo lives at
`~/.cargo/bin/cargo` — the daemon finds it for you.

**`ro`** — directories mounted read-only inside the sandbox. Use these
for system libraries, toolchain directories, SSL certificates, timezone data,
and anything else the tools need to read but should never modify.
**`paths`** — host paths made available inside the sandbox. A relative `src`
is resolved below the host user's home and appears below `/home` in the guest:

```yaml
paths:
- src: .cargo
- src: /usr/lib/some/cpp/includes/crap
- src: /opt/sdk/include
dst: /toolchain/include
```

**`rw`** — directories mounted read-write inside the sandbox. Use these
for caches, build artifacts that should persist between runs, and any
directory the toolchain needs to write to. The `${HOME}` variable expands to
your host home directory.
Relative paths are writable carryover data and use `/home/<src>` as the guest
destination. Absolute paths below the host home use the corresponding `/home`
destination. Absolute paths outside the host home keep the same destination and
are mounted as system/toolchain inputs. An explicit `dst` overrides the
destination. The profile author is responsible for the host paths selected.

**`env`** — environment variables set inside the sandbox. Use these for
toolchain configuration (`CARGO_HOME`, `GOPATH`, etc.). `${HOME}`, `${USER}`,
and `${TERM}` are expanded automatically.
**`env`** — guest environment variables set inside the sandbox. Use guest paths
such as `/home/.cargo` for toolchain configuration (`CARGO_HOME`, `GOPATH`,
etc.). `${HOME}` expands to `/home`; `${USER}` and `${TERM}` use the host
runtime values when present.

**`network`** — currently only `none` is supported. The sandboxed command has
no network access.
Expand All @@ -136,23 +142,24 @@ no network access.

## How rules translate to isolation

When a profile is active, each binary in the list is bind-mounted read-only
at its expected path inside the sandbox. If the binary is a symlink (common
When a profile is active, each binary in the list is bind-mounted at its
expected path inside the sandbox. If the binary is a symlink (common
with rustup, where `cargo` and `rustc` both point to the same `rustup`
binary), the daemon follows the link and mounts the real file — so the
sandbox sees a working executable, not a dangling symlink.

Read-only directories are mounted recursively, so `/usr/lib` brings in the
full tree. Writeable directories are plain bind mounts — changes inside the
sandbox are visible on the host. The overlay workspace at `.bunkerbox/workspace`
is always mounted read-write at `/workspace` inside the sandbox, so build
output always lands in the copy-on-write layer.
Absolute system and toolchain paths are mounted at their standard destinations.
Home-relative paths are plain writable carryover binds, so changes are visible
on the host. The overlay workspace at `.bunkerbox/workspace` is always mounted
read-write at `/workspace` inside the sandbox, so build output always lands in
the copy-on-write layer.

The command gets a clean environment: no host variables leak in, and the
profile's `env` block provides exactly what the toolchain needs. `/proc` and
`/dev` are the sandbox's own — the command cannot see host processes or raw
devices. `/tmp` and `/home` are empty tmpfs mounts, discarded when the
command exits.

All of this is enforced by bubblewrap using unprivileged user namespaces — no
root, no setuid, no kernel modules.
Bubblewrap provides the namespace boundary. Host paths selected by a profile
remain trusted profile policy; path declarations are not a substitute for
capability dropping or a non-root threat model.
2 changes: 1 addition & 1 deletion docs/guides/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ workspace and re-creates it.
│ ~/bunkerbox-tutorial/.bunkerbox/ ← overlay upper layer │
│ .bunkerbox/workspace/ ← where AI actually writes │
│ │
│ Bunkerbox daemon listens on vsock port 9999.
│ Bunkerbox toolchain daemon listens on vsock port 9999. │
│ When AI calls `cargo build`: │
│ → checks whitelist ("cargo *" ✓) │
│ → runs `cargo build` inside .bunkerbox/workspace/ │
Expand Down
29 changes: 29 additions & 0 deletions docs/reference/config-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,35 @@ project:
# - extra.api.example.com
```

## Sandbox profile

Profiles are host-side YAML files selected by the project configuration.

```yaml
name: rust
bin:
cargo: /usr/bin/cargo
paths:
- src: /lib
- src: /usr/lib
- src: .cargo
- src: .rustup
- src: /opt/sdk/include
dst: /toolchain/include
env:
CARGO_HOME: /home/.cargo
RUSTUP_HOME: /home/.rustup
network: none
shell: /bin/sh
```

Relative `src` paths are resolved below the host user's home and appear below
`/home` in the guest. Absolute paths below the host home use the corresponding
`/home` destination. Absolute paths outside the host home retain their source
path as the guest destination unless `dst` is supplied. Home-relative paths are
writable carryover data; absolute system and toolchain paths are read-only
inputs by default. These declarations are trusted host policy.

During development, runtime configs live in `runtime/`. In a packaged install, they live under:

```text
Expand Down
29 changes: 14 additions & 15 deletions profiles/go.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,23 @@ bin:
go: /usr/local/go/bin/go
gofmt: /usr/local/go/bin/gofmt

ro:
- /lib
- /lib64
- /usr/lib
- /usr/lib64
- /etc/ssl
- /etc/localtime
- /usr/share/zoneinfo
- /usr/lib/locale
- /usr/local/go

rw:
- "${HOME}/go"
- "${HOME}/.cache/go-build"
paths:
- src: /lib
- src: /lib64
- src: /usr/lib
- src: /usr/lib64
- src: /etc/ssl
- src: /etc/localtime
- src: /usr/share/zoneinfo
- src: /usr/lib/locale
- src: /usr/local/go
- src: go
- src: .cache/go-build

env:
GOROOT: /usr/local/go
GOPATH: "${HOME}/go"
GOPATH: /home/go
GOCACHE: /home/.cache/go-build
TERM: "${TERM}"

network: none
Expand Down
20 changes: 9 additions & 11 deletions profiles/make.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,15 @@ bin:
as: /usr/bin/as
strip: /usr/bin/strip

ro:
- /lib
- /lib64
- /usr/lib
- /usr/lib64
- /etc/ssl
- /etc/localtime
- /usr/share/zoneinfo
- /usr/lib/locale

rw: []
paths:
- src: /lib
- src: /lib64
- src: /usr/lib
- src: /usr/lib64
- src: /etc/ssl
- src: /etc/localtime
- src: /usr/share/zoneinfo
- src: /usr/lib/locale

env:
TERM: "${TERM}"
Expand Down
26 changes: 12 additions & 14 deletions profiles/node.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,20 @@ bin:
npm: /usr/bin/npm
npx: /usr/bin/npx

ro:
- /lib
- /lib64
- /usr/lib
- /usr/lib64
- /etc/ssl
- /etc/localtime
- /usr/share/zoneinfo
- /usr/lib/locale

rw:
- "${HOME}/.npm"
- "${HOME}/.node-gyp"
paths:
- src: /lib
- src: /lib64
- src: /usr/lib
- src: /usr/lib64
- src: /etc/ssl
- src: /etc/localtime
- src: /usr/share/zoneinfo
- src: /usr/lib/locale
- src: .npm
- src: .node-gyp

env:
npm_config_cache: "${HOME}/.npm"
npm_config_cache: /home/.npm
TERM: "${TERM}"

network: none
Expand Down
Loading
Loading