bonsai

Command sandbox

The sandbox is an enforcement floor independent of autonomy and permission rules: autonomy decides whether a command may run without asking; the sandbox decides what that spawned command can physically do if it runs. It confines writes to approved roots and (by default) denies network egress. Reads stay broad so toolchains can inspect SDKs and system files — protecting secret reads is redaction’s job.

The sandbox confines only the child command, never the bonsai process itself (which must keep writing ~/.bonsai). It is independent of yolo: enabling yolo does not turn the sandbox off; leaving confinement is always an explicit /sandbox off.

Backends

Platform Backend Mechanics
macOS Seatbelt /usr/bin/sandbox-exec -p <profile> with a generated SBPL profile: allow default, deny file-write* except the writable roots (+ /dev/null, /dev/tty, pty devices), optionally deny network*. sandbox-exec execs into the shell, so process-group signals reach the real child.
Linux Bubblewrap /usr/bin/bwrap --unshare-all --die-with-parent --ro-bind / /, re-binding each writable root read-write; network allowed only by adding --share-net. A startup probe actually spawns a bwrap sandbox to confirm namespace creation works; a second probe checks whether network isolation is supported (containers often block it, degrading to “network deny unenforced” with a visible warning).
Other none /sandbox on reports that no backend is available. Autonomy and permission rules still apply, but no OS confinement is claimed.

Writable roots

Resolved once at startup, canonicalized and de-duplicated:

  1. The project root.
  2. A private per-session temp directory (a fresh bonsai-session-* tempdir), exported to children as $TMPDIR, $TMP, and $TEMP — so concurrent sessions never share scratch space. npm, XDG, pip, and Go build caches are redirected below it (npm_config_cache, XDG_CACHE_HOME, PIP_CACHE_DIR, GOCACHE).
  3. Shared dependency stores that cannot be cleanly overlaid: CARGO_HOME (default ~/.cargo) and an explicitly configured GOMODCACHE. This is the deliberate compatibility exception to per-session cache isolation — cargo and go mix immutable dependencies with resolver locks in one directory. rustup stays read-only, so toolchain installation requires an approved sandbox escape.
  4. Extra roots from BONSAI_SANDBOX_WRITABLE_ROOTS (colon-separated) and writable_roots in the [sandbox] config section (unioned).

Network policy

Denied by default. Seatbelt appends (deny network*); Bubblewrap simply omits --share-net from the unshared namespace. /sandbox net on denies, /sandbox net off allows. When the Linux backend cannot isolate networking, the status and command output say the denial is unenforced rather than silently claiming confinement.

Sandbox escapes

A confined command that legitimately needs to step outside (e.g. rustup update) can request escape_sandbox: true on the bash tool. This is a single audited step-past:

The shared enabled-flag is untouched by an escape — every other command stays confined.

Commands and settings

Environment defaults:

Variable Values Default
BONSAI_SANDBOX 1/on/true/yes/enabled vs 0/off/false/no/disabled on when a backend is available
BONSAI_SANDBOX_NETWORK deny/denied/block/off vs allow/allowed/on denied
BONSAI_SANDBOX_WRITABLE_ROOTS colon-separated paths empty

Env wins over the [sandbox] config section, per the normal precedence.

Interactions with the rest of the system

Where this lives in the code

Concern Location
Shared sandbox handle & env defaults src/sandbox/mod.rs
Seatbelt profile src/sandbox/macos.rs
Bubblewrap wrapper & probes src/sandbox/linux.rs
Writable-root policy & escapes src/sandbox/policy.rs
Doctor enforcement probe src/doctor.rs