Skip to content

Latest commit

 

History

History
405 lines (294 loc) · 14 KB

File metadata and controls

405 lines (294 loc) · 14 KB

Sandbox Engines

Visor supports three sandbox engines for isolating command execution. Each engine provides different tradeoffs between isolation strength, platform support, and performance.

Overview

Engine Platform Startup Overhead Dependencies Isolation Model
Docker Linux, macOS, Windows ~500ms+ Docker daemon Full container
Bubblewrap Linux only ~5-50ms bwrap binary Linux namespaces
Seatbelt macOS only ~10-30ms Built-in (sandbox-exec) SBPL policy profiles

All three engines implement the same SandboxInstance interface and are configured through the same sandboxes: config block. The engine field determines which backend handles execution.

Quick Start

# .visor.yaml
sandboxes:
  # Linux: use bubblewrap for lightweight isolation
  bwrap:
    engine: bubblewrap
    network: true

  # macOS: use seatbelt for native isolation
  mac:
    engine: seatbelt
    network: true

  # Any platform: use Docker for full container isolation
  docker:
    image: node:20-alpine

# Default all steps to a sandbox
sandbox: bwrap  # or mac, or docker

steps:
  lint:
    type: command
    exec: eslint src/

  build:
    type: command
    sandbox: docker  # Override: use Docker for this step
    exec: npm run build

Configuration

Common Options

These options apply to all engine types:

Field Type Default Description
engine 'docker' | 'bubblewrap' | 'seatbelt' 'docker' Sandbox engine backend
network boolean true Enable/disable network access
read_only boolean false Mount repository as read-only
workdir string '/workspace' Working directory inside sandbox. Use "host" to keep the real repo path. (Docker/Bubblewrap only)
env_passthrough string[] Glob patterns for host env vars to forward
bind_paths SandboxBindPath[] Additional host paths to mount into sandbox
visor_path string '/opt/visor' Where visor is mounted inside the sandbox

Docker-Only Options

These fields are only valid when engine is 'docker' (or omitted):

Field Type Description
image string Docker image (e.g., node:20-alpine)
dockerfile string Path to Dockerfile
dockerfile_inline string Inline Dockerfile content
compose string Path to docker-compose file
service string Service name within compose file
resources object Memory/CPU limits (memory: '512m', cpu: 1.0)
cache object Cache volume configuration

Using Docker-only fields with engine: bubblewrap or engine: seatbelt produces a validation error.

Bind Paths

The bind_paths option mounts additional host directories into the sandbox. This works with all three engines.

Each entry is a SandboxBindPath object:

Field Type Required Default Description
host string Yes Host path to mount (supports ~ for home directory)
container string No Resolved host path Mount destination inside the sandbox
read_only boolean No true Mount as read-only
sandboxes:
  dev:
    engine: bubblewrap
    bind_paths:
      - host: ~/.gitconfig             # read-only by default
      - host: ~/.ssh                    # read-only by default
      - host: /opt/custom-tools        # read-only, same path inside sandbox
        container: /opt/custom-tools
      - host: ~/shared-cache           # writable mount
        container: /cache
        read_only: false

Security note: Host paths are validated — .. path traversal is rejected, and container paths must be absolute.

Host Workdir Mode

By default, the repository is mounted at /workspace inside Docker and Bubblewrap sandboxes. Setting workdir: "host" keeps the real repository path instead.

This is useful when AI agents or tools reference files by their absolute host paths — the paths remain valid inside the sandbox.

sandboxes:
  agent-sandbox:
    engine: bubblewrap
    workdir: "host"
    bind_paths:
      - host: ~/.gitconfig

With workdir: "host", if your repo is at /home/user/projects/myapp, commands inside the sandbox run from /home/user/projects/myapp rather than /workspace.

Note: Seatbelt always uses the real repo path regardless of the workdir setting.


Bubblewrap Engine

Bubblewrap (bwrap) provides lightweight process isolation using Linux kernel namespaces. It creates an isolated filesystem view, PID namespace, and optionally isolated network for each command execution.

Requirements

  • Linux only (uses kernel namespaces, which are not available on macOS/Windows)
  • bwrap binary must be installed:
# Debian / Ubuntu
apt install bubblewrap

# Fedora / RHEL / CentOS
dnf install bubblewrap

# Arch Linux
pacman -S bubblewrap

# Alpine
apk add bubblewrap

How It Works

Each exec() call spawns a fresh bwrap process with:

  1. Mount namespace: Read-only system dirs (/usr, /bin, /lib, /etc) + writable workspace at /workspace
  2. PID namespace: Sandboxed process cannot see host PIDs (--unshare-pid)
  3. Clean environment: --clearenv strips all host env vars; only explicitly passed vars are visible (--setenv)
  4. Session isolation: --new-session prevents terminal injection attacks
  5. Orphan cleanup: --die-with-parent kills sandbox if parent dies
  6. Network isolation: --unshare-net when network: false

Configuration

sandboxes:
  bwrap:
    engine: bubblewrap
    network: true        # Allow network access (default: true)
    read_only: false     # Writable workspace (default: false)
    workdir: /workspace  # Working directory inside sandbox (default: /workspace)

Filesystem Layout Inside Sandbox

Path Access Source
/workspace Read-write (or read-only) Host repository directory
/usr, /bin, /lib Read-only Host system directories
/etc/resolv.conf, /etc/ssl Read-only DNS and TLS certificates
/tmp Read-write Fresh tmpfs per execution
/dev, /proc Minimal Virtual filesystems
~/.ssh, ~/.aws, ~/.config Not mounted Not mounted by default (use bind_paths)

Security Properties

Property Status
Filesystem isolation Commands cannot access files outside allowed paths
Process isolation PID namespace hides other processes
Environment isolation --clearenv prevents credential theft
Terminal injection --new-session prevents TIOCSTI attacks
Orphan cleanup --die-with-parent ensures cleanup
Network isolation Optional via --unshare-net
Resource limits Not enforced (use cgroups separately)

CI/CD Notes

  • Works in unprivileged CI runners (no Docker-in-Docker needed)
  • May need --cap-add SYS_ADMIN or --privileged when running inside Docker containers
  • On non-Linux platforms, engine: bubblewrap will fail at runtime with a clear error

Seatbelt Engine

Seatbelt uses macOS's built-in sandbox-exec with dynamically-generated SBPL (Seatbelt Profile Language) profiles. Unlike bubblewrap, it does not create mount namespaces — commands see the real filesystem but are restricted by ACL-style policy rules.

Requirements

  • macOS only (sandbox-exec ships with macOS)
  • No additional installation needed

How It Works

Each exec() call:

  1. Generates an SBPL profile with (deny default) base policy and explicit (allow ...) rules
  2. Runs sandbox-exec -p '<profile>' /usr/bin/env -i KEY=VAL ... /bin/sh -c '<command>'
  3. Resolves symlinks via realpathSync (macOS uses /var -> /private/var, /tmp -> /private/tmp)
  4. Cleans environment using env -i (sandbox-exec inherits parent env, unlike bubblewrap's --clearenv)

Configuration

sandboxes:
  mac:
    engine: seatbelt
    network: true        # Allow network access (default: true)
    read_only: false     # Writable workspace (default: false)

Note: The workdir field is ignored for seatbelt — commands run from the real repository path (no mount remapping).

SBPL Profile

The generated profile follows a deny-by-default model:

(version 1)
(deny default)

;; Process execution
(allow process-exec)
(allow process-fork)

;; System paths (read-only)
(allow file-read*
  (literal "/")
  (subpath "/usr") (subpath "/bin") (subpath "/sbin")
  (subpath "/Library") (subpath "/System")
  (subpath "/private") (subpath "/var") (subpath "/etc")
  (subpath "/dev") (subpath "/tmp"))

;; Temp and device writes
(allow file-write*
  (subpath "/tmp") (subpath "/private/tmp") (subpath "/dev"))

;; xcrun cache (macOS Xcode tools)
(allow file-write* (regex #"/private/var/folders/.*/T/xcrun_db"))

;; Workspace access
(allow file-read* (subpath "/path/to/repo"))
(allow file-write* (subpath "/path/to/repo"))  ;; omitted when read_only

;; Network (omitted when network: false)
(allow network*)

;; System operations
(allow sysctl-read)
(allow mach-lookup)
(allow signal)

Filesystem Access Inside Sandbox

Path Access Notes
Repository directory Read-write (or read-only) Real filesystem path
/usr, /bin, /Library, /System Read-only System binaries and libraries
/private, /var, /etc Read-only System config (symlink-resolved)
/tmp Read-write Temporary files
~/Documents, ~/Desktop Denied "Operation not permitted"
~/.ssh, ~/.aws, ~/.claude Denied Denied by default (use bind_paths)
~/.gitconfig, ~/.zsh_history Denied Denied by default (use bind_paths)

Security Properties

Property Status
Filesystem isolation ACL-style policy blocks access to unauthorized paths
Process isolation Limited (no PID namespace)
Environment isolation env -i strips inherited vars; only explicitly passed vars visible
Network isolation Optional via omitting (allow network*) rule
Resource limits Not enforced
Write protection read_only: true omits file-write rules for workspace

Known Limitations

  • No mount namespaces: Commands see real filesystem paths (no /workspace remapping)
  • Git worktrees: If the repository is a git worktree, the .git file points outside the repo directory. Git commands may fail because the parent .git directory is not in the allowed paths. Standalone git repos work fine.
  • Deprecated API: Apple has deprecated sandbox-exec but it remains functional on current macOS versions. There is no replacement API for command-line use.

Choosing an Engine

Use Docker When

  • You need custom runtimes, specific OS packages, or language versions
  • Full container isolation is required
  • Cross-platform consistency matters
  • You need cache volumes for persistent caches (e.g., node_modules)

Use Bubblewrap When

  • Running on Linux and need fast, lightweight isolation
  • CI runners don't have Docker available
  • You're running many short-lived commands (the ~5-50ms overhead adds up much less than Docker's ~500ms)
  • You need namespace-level isolation (PID, mount, network) without containers

Use Seatbelt When

  • Running on macOS (local development, macOS CI runners)
  • You want filesystem and network restrictions without Docker
  • You want near-zero setup (sandbox-exec is built into macOS)

Mixing Engines

You can define multiple sandboxes with different engines and assign them per-step:

sandboxes:
  fast:
    engine: bubblewrap  # Quick commands
    network: false

  full:
    image: node:20-alpine  # Heavy builds
    cache:
      paths: [node_modules]

sandbox: fast  # Default to bubblewrap

steps:
  lint:
    type: command
    exec: eslint src/         # Uses bubblewrap (fast)

  build:
    type: command
    sandbox: full              # Uses Docker (full isolation)
    exec: npm run build

  test:
    type: command
    exec: npm test             # Uses bubblewrap (fast)
    read_only: true

Credential Propagation

All sandbox engines work with Visor's credential propagation system. The injectGitHubCredentials() function from src/github-auth.ts passes authentication via environment variables:

  • GITHUB_TOKEN / GH_TOKEN for gh CLI
  • GIT_CONFIG_COUNT / GIT_CONFIG_KEY_* / GIT_CONFIG_VALUE_* for authenticated git HTTPS access

No temp files are written and no global git config is modified, so credentials work inside any sandbox engine without special handling.


Environment Variable Filtering

Before commands execute in any sandbox engine, environment variables pass through filterEnvForSandbox() from src/sandbox/env-filter.ts. This applies glob-based patterns from env_passthrough config and a built-in passthrough list (PATH, HOME, USER, CI, NODE_ENV, LANG).

Only filtered variables are passed into the sandbox:

  • Bubblewrap: Via --setenv KEY VALUE args
  • Seatbelt: Via env -i KEY=VALUE args
  • Docker: Via -e KEY=VALUE args to docker exec

Telemetry

All sandbox engines emit telemetry events:

Event Attributes
visor.sandbox.bwrap.exec visor.sandbox.name, visor.sandbox.exit_code
visor.sandbox.seatbelt.exec visor.sandbox.name, visor.sandbox.exit_code
visor.sandbox.docker.exec visor.sandbox.name, visor.sandbox.exit_code

These integrate with Visor's OpenTelemetry tracing via src/sandbox/sandbox-telemetry.ts.


Related Documentation