Sandbox Internals
The Sandbox runs agent code in an isolated environment with full dev capabilities — shell, Python, Node, compilers, package managers. On macOS 26+ it's a shared Linux VM (Alpine, Apple Containerization framework) running natively on Apple Silicon; on macOS 15 it falls back to a native Seatbelt backend so sandboxed execution works everywhere.
Per-agent Linux users, vsock-bridge with per-agent bearer tokens, fail-closed network policy, SHA-256-pinned runtime artifacts. Plain-language summary on Security & Privacy.
For the everyday view, see Tasks. This page is the reference for plugin authors and contributors.
Requirements
- macOS 26 (Tahoe) or later for the Linux VM backend (Apple's Containerization framework)
- Apple Silicon (M1 or newer)
- On earlier macOS, the sandbox automatically uses the Seatbelt fallback — no VM, no download
Seatbelt fallback (macOS 15)
On Macs that can't run the Containerization VM, commands run as regular host processes confined by a deny-by-default Seatbelt profile (sandbox-exec): they can read the system but can only write inside the sandbox workspace (~/.osaurus/container/workspace/, seen by agents as /workspace) and a scratch temp directory. The backend is chosen once at launch — macOS 26+ always uses the VM, older systems always use Seatbelt.
Everything on this page applies to both backends unless noted. The differences:
| Linux VM (macOS 26+) | Seatbelt (earlier) | |
|---|---|---|
| Environment | Alpine Linux, full userland | macOS, BSD userland |
| Package managers | pip, npm, apk | pip, npm (no apk) |
Tool recipe dependencies | Supported | Not supported — install via setup with pip/npm |
| Network policy | Off, on, or per-domain allowlist | All-or-nothing. A configured domain allowlist can't be enforced and fails closed to no network |
| Isolation boundary | Hardware VM, separate filesystem | Process-level write confinement. Reads of the host are not blocked |
| Per-agent environments | Separate Linux users, optional per-agent rootfs | Shared workspace tree with per-agent home directories |
| Sandboxed MCP servers | Supported | Not supported — set the provider's Run in to Host |
| Provisioning | Kernel + rootfs download (~1 min) | Instant, no download |
Two Seatbelt behavioral notes: denied file lookups surface as "No such file or directory" rather than "Operation not permitted" (deliberate macOS anti-probing behavior), and ~ inside sandboxed commands resolves to the agent's workspace home, not your macOS home.
Provisioning
- Management → Sandbox → Container → Provision
- Osaurus downloads the Linux kernel + initial filesystem and boots the VM
- The first run takes about a minute; subsequent boots are seconds
- Sandbox tools become available to the active agent automatically
On macOS 26+ the sandbox chip defaults on for the Default agent and newly created agents, but the container is not booted eagerly — a never-set-up sandbox stays un-provisioned until the first time the model actually reaches for a sandbox tool, at which point it boots and provisions on demand. Once setup completes, later launches auto-start as normal.
A Provisioning Preflight report (Management → Sandbox) inspects the resolved paths, config, cached assets, and bridge socket before provisioning, with typed readiness states (ready / needs_setup / blocked / unproven) and a concrete repair suggestion per finding. Copy JSON produces a support artifact.
Architecture
┌──────────────────────────────────────────────────────────────┐
│ macOS Host │
│ │
│ ┌──────────────┐ ┌──────────────────────────────┐ │
│ │ Osaurus │ │ Linux VM (Alpine) │ │
│ │ │ │ │ │
│ │ SandboxMgr ─┼─────┤→ /workspace (VirtioFS) │ │
│ │ │ │→ /output (VirtioFS) │ │
│ │ HostAPI ←──┼─vsock─→ /run/osaurus-bridge.sock │ │
│ │ Bridge │ │ │ │
│ │ │ │ agent-alice (Linux user) │ │
│ │ ToolReg ←──┼─────┤ agent-bob (Linux user) │ │
│ │ │ │ ... │ │
│ └──────────────┘ └──────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
| Component | Description |
|---|---|
| Linux VM | Alpine Linux with Kata Containers ARM64 kernel, 8 GiB rootfs |
| VirtioFS mounts | /workspace ↔ ~/.osaurus/container/workspace/, /output ↔ ~/.osaurus/container/output/ |
| NAT networking | Container gets 10.0.2.15/24 via VZNATNetworkDeviceAttachment |
| Vsock bridge | Unix socket relayed via vsock connects the container to the host bridge |
| Per-agent users | Each agent gets a Linux user agent-{name} with home at /workspace/agents/{name}/ |
| Host API Bridge | HTTP server on the host, accessible from the container via the osaurus-host CLI shim |
VM configuration
Management → Sandbox → Container → Resources:
| Setting | Range | Default | Notes |
|---|---|---|---|
| CPUs | 1–8 | 2 | |
| Memory | 1–8 GB | 2 GB | |
| Network | outbound / proxy / none | outbound | outbound = unrestricted NAT; proxy = host-only network with a domain-allowlist egress proxy (selected automatically when the agent has Allowed Domains set); none = no networking |
| Per-Agent Environments | on / off | off | Experimental: boot from the provisioning agent's own copy-on-write clone of the base image |
| Auto-Start | on / off | on | Start VM when Osaurus launches |
| Rootfs | — | 8 GiB | Fixed |
With Per-Agent Environments on, system packages an agent installs with apk persist in its own clone and are invisible to other agents. The clone pool is LRU-bounded (default 3); evicted environments re-clone fresh from the pristine base template. Agent home directories live on the /workspace mount and are never affected by environment eviction or reset.
Changes require a container restart. Config file: ~/.osaurus/config/sandbox.json:
{
"autoStart": true,
"cpus": 2,
"memoryGB": 2,
"network": "outbound"
}
Built-in tools
When the container is running, sandbox tools are registered automatically for the active agent. Read-only tools are always on. Write/exec/install/secret tools require autonomous_exec.enabled on the agent. sandbox_plugin_register additionally requires autonomous_exec.pluginCreate.
Anti-confusion cheat sheet
Always prefer the dedicated tool over a shell command:
| Don't | Do |
|---|---|
cat/head/tail in sandbox_exec | sandbox_read_file |
grep/rg/find/ls in sandbox_exec | sandbox_search_files (target="content" for rg, target="files" for glob) |
sed/awk | sandbox_write_file with old_string + new_string (in-place edit) |
echo/heredoc | sandbox_write_file |
& / nohup / disown | sandbox_exec(background:true) + sandbox_process |
Reserve sandbox_exec for builds, installs, processes, network calls, and any work without a dedicated tool. For multi-step logic, write a script with sandbox_write_file and run it with sandbox_exec (e.g. python3 script.py).
Read-only (always available)
| Tool | Description |
|---|---|
sandbox_read_file | Read a file's contents (supports line ranges, tail, char cap) |
sandbox_search_files | Search file contents (target="content", ripgrep) or find files by name (target="files", glob). Replaces the discrete sandbox_search_files + sandbox_find_files + sandbox_list_directory trio. |
Requires autonomous_exec
| Tool | Description |
|---|---|
sandbox_write_file | Write content to a file (creates parent directories), or edit in place: pass old_string + new_string for an exact-match string replacement |
sandbox_exec | Run a shell command. Foreground by default with an optional idle timeout parameter, or background:true for servers/long tasks (returns pid + log_file immediately; requires the agent's background-process opt-in) |
sandbox_process | Manage background jobs from sandbox_exec(background:true) — action="poll", "wait" (default 60s, max 300s), "kill" |
sandbox_install | Install packages — manager selects apk (system, runs as root), pip (agent venv at ~/.venv/), or npm (agent workspace at ~/.osaurus/node_workspace/) |
sandbox_secret_check | Check whether a secret exists (never reveals the value) |
sandbox_secret_set | Store a secret directly (value) or prompt the user (omit value) |
sandbox_plugin_register | Register an agent-created plugin (requires pluginCreate) |
share_artifact is a global built-in registered on ToolRegistry. It's available everywhere, not just in sandbox mode, so it doesn't appear in this sandbox-specific table.
The previously-discrete sandbox_list_directory, sandbox_find_files, sandbox_move, sandbox_delete, sandbox_exec_background, sandbox_edit_file, sandbox_pip_install, sandbox_npm_install, sandbox_run_script, and sandbox_execute_code tools were consolidated. Their behavior now comes from a flag or parameter (background:true on sandbox_exec, target on sandbox_search_files, old_string on sandbox_write_file, manager on sandbox_install) or a direct shell invocation (mv / rm in sandbox_exec). Multi-step Python orchestration is now: write the script with sandbox_write_file, run it with sandbox_exec.
Install hardening
The three install managers share a hardening pipeline:
| Layer | Behavior |
|---|---|
| Per-agent serialization | SandboxInstallLock queues install ops behind each other per agent. apk's lock is container-wide so manager:"apk" installs serialize globally across every agent. npm/pip are per-agent and run concurrently across agents. |
| Auto-recovery | If the first attempt fails AND output matches a known stale-state signature (Tracker "idealTree" already exists, EEXIST, ELOCKED, Could not install packages due to an OSError, ReadTimeoutError, temporary error, unable to lock database), the tool runs cleanup and retries once. The result envelope sets retried: true. |
| Cleanup actions | npm: rm -rf node_modules/.package-lock.json && npm cache clean --force. pip: pip cache purge. apk: apk update. Same exec context as the install. |
| Workspace isolation | npm in ~/.osaurus/node_workspace/; pip in agent's venv at ~/.venv/; both bin/ on PATH from any cwd. |
| Stable flags | npm: --no-audit --no-fund --no-update-notifier. pip: --disable-pip-version-check --no-input. apk: --no-cache. |
| Timeouts | npm/pip: 240s. apk: 120s. |
Result shape
Every sandbox tool returns a ToolEnvelope JSON string. Success payloads:
- Read/inspect:
{path, content, size}(+ optionalstart_line/line_count/tail_lines/max_chars) - Search:
{pattern, target, path, matches}—targetis"content"or"files" - Exec foreground:
{stdout, stderr, exit_code, cwd} - Exec background:
{pid, log_file, cwd, background:true} - Process management:
{pid, alive|exited|killed, log_file, log_tail, ...} - Install:
{installed, exit_code, output}on success, plusretried: truewhen auto-recovery ran. Failures usekind: execution_errorand may carrycleanup_failed: true.
Path failures use kind: invalid_args with field pointing at the offending argument so the model can self-correct. The path sanitizer returns structured rejection reasons (empty, traversal, null byte, dangerous character, outside allowed roots).
Plugin recipes
Sandbox plugins are JSON recipes — no compiled dylibs, no Xcode, no code signing. They install dependencies, seed files, define custom tools, and configure secrets.
Format
{
"name": "Python Data Tools",
"description": "Data analysis toolkit with pandas and matplotlib",
"version": "1.0.0",
"author": "your-name",
"dependencies": ["python3", "py3-pip"],
"setup": "pip install --user pandas matplotlib seaborn",
"files": {
"helpers.py": "import pandas as pd\nimport matplotlib\nmatplotlib.use('Agg')\nimport matplotlib.pyplot as plt\n"
},
"tools": [
{
"id": "analyze_csv",
"description": "Load a CSV file and return summary statistics",
"parameters": {
"file": { "type": "string", "description": "Path to the CSV file" }
},
"run": "cd $HOME/plugins/python-data-tools && python3 -c \"import pandas as pd; df = pd.read_csv('$PARAM_FILE'); print(df.describe().to_string())\""
}
],
"secrets": ["OPENAI_API_KEY"],
"permissions": {
"network": "outbound",
"inference": true
}
}
Properties
| Property | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name |
description | string | Yes | Brief description |
version | string | No | Semantic version |
author | string | No | Author name |
source | string | No | Source URL (e.g. GitHub repo) |
dependencies | string[] | No | System packages installed via apk add (root) |
setup | string | No | Setup command run as the agent's Linux user |
files | object | No | Files seeded into the plugin folder (key = relative path, value = contents) |
tools | SandboxToolSpec[] | No | Custom tool definitions |
secrets | string[] | No | Secret names the plugin requires (user prompted on install) |
permissions | object | No | Network policy + inference access |
Per-agent installation
Plugins install per agent. Each agent has its own plugin set, isolated under their workspace.
Install flow:
- Validate plugin file paths (
SandboxPathSanitizer) - Start container (if not running)
- Create the agent's Linux user
- Install system dependencies via
apk - Create plugin directory and seed files via VirtioFS
- Configure secrets from Keychain
- Run the setup command
- Register plugin tools
Manage from Management → Sandbox → Plugins:
- Import from JSON files, URLs, or GitHub repos
- Create with the built-in editor
- Install to specific agents
- Export and duplicate for sharing
Plugin tools
Each tool in a plugin's tools array becomes an AI-callable tool. Tool name is {pluginId}_{toolId}. Parameters are passed as environment variables prefixed PARAM_:
| Parameter | Env var |
|---|---|
file | $PARAM_FILE |
query | $PARAM_QUERY |
output_format | $PARAM_OUTPUT_FORMAT |
The run field is a shell command executed as the agent's Linux user with the working directory set to the plugin folder.
Agent-authored plugins (Sandbox Plugin Creator)
Agents can author, package, and register new sandbox plugins at runtime. The model-facing guidance is injected when the agent has pluginCreate enabled and the sandbox is available.
Both the in-process sandbox_plugin_register tool and the host-API POST /api/plugin/create endpoint funnel through one shared registration pipeline (SandboxPluginRegistration.register) so they cannot drift.
Requirements:
autonomousExec.enabled = trueon the agentautonomousExec.pluginCreate = true(the default)- The sandbox running
Workflow:
- Agent writes script files to
~/plugins/{plugin-id}/scripts/ - Agent writes a
plugin.jsonmanifest defining name, description, tools, dependencies - Agent calls
sandbox_plugin_registerwith theplugin_id(orPOST /api/plugin/create) - Pipeline validates, applies restricted defaults, persists, runs install, hot-registers tools via
CapabilityLoadBuffer - Toast notifies the user with a Remove action
File auto-packaging: sandbox_plugin_register recursively collects every UTF-8 readable file in the plugin directory (excluding plugin.json) and merges them into the plugin's files map. Files explicitly defined in plugin.json take precedence. Binary files are rejected up front — text-only.
Restricted defaults (SandboxPluginDefaults):
permissions.network— Wildcards (outbound) collapse tonone. Comma-separated domain lists are accepted only when every entry parses as a valid domain. Plan ahead — declare exact API hostnames.permissions.inference— Forced tofalse. Agent-authored plugins cannot call inference APIs.metadata.created_bystamped toagent;metadata.created_viarecordsagent_toolorhost_bridge.
Validation: Rejected up front (no library state written) when:
- File paths fail
SandboxPathSanitizer.validatePluginFiles - The
setupcommand references a host outsideSandboxNetworkPolicy.setupAllowlist - Any tool's
runcommand references a host outside the same allowlist - A declared
secretsentry has no value inAgentSecretsKeychainfor the requesting agent - The agent exceeds
SandboxRateLimiterquota forservice: "http" - The container is not running (
unavailable→ HTTP 503)
Persistence: Plugins saved to SandboxPluginLibrary (~/.osaurus/sandbox-plugins/) survive restarts. Per-agent install state lives at ~/.osaurus/agents/{agent-id}/sandbox-plugins/installed.json.
Host API Bridge
Inside the container, the osaurus-host CLI talks to the bridge server over a vsock-relayed Unix socket.
| Command | Description |
|---|---|
osaurus-host secrets get <name> | Read a secret from macOS Keychain |
osaurus-host config get <key> | Read a plugin config value |
osaurus-host config set <key> <value> | Write a plugin config value |
osaurus-host inference chat -m <message> | Run a chat completion through Osaurus |
osaurus-host agent dispatch <id> <task> | Dispatch a task to an agent |
osaurus-host agent memory query <text> | Search agent memory |
osaurus-host agent memory store <text> | Store a memory entry |
osaurus-host events emit <type> [payload] | Emit a cross-plugin event |
osaurus-host plugin create | Create a plugin from stdin JSON |
osaurus-host log <message> | Append to the sandbox log buffer |
Bridge authentication
Every request authenticates with a per-agent bearer token:
- The host mints a 256-bit token per agent and writes it to
/run/osaurus/.tokeninside the guest, mode0600, owned by that agent's Linux user. The directory is mode0711so users open their own file by name without enumerating siblings. - The
osaurus-hostshim reads the token (allowed by uid) and sends it asAuthorization: Bearer <token>. Refuses to run if the token file is missing or unreadable. - The bridge resolves the token to an
(agentId, linuxName)pair viaSandboxBridgeTokenStore. Unknown or missing tokens get401— no fallback to a default agent. X-Osaurus-Useris no longer trusted. Identity is bound to the token, which is bound to a Linux uid by file permissions inside the guest.X-Osaurus-Pluginis still self-reported by the shim. It namespaces config and secrets within an agent but is not a security boundary between plugins of the same agent.
The agent dispatch route rejects any body whose agent_id doesn't match the token-bound identity (403); agent memory query filters results to the calling agent's pinned facts.
Tokens are revoked when the agent is unprovisioned or the container is stopped, and re-minted on the next ensureProvisioned. After an Osaurus upgrade, plugin bridge calls fail closed until the container restarts and the new shim/token files are written — this happens automatically when Sparkle relaunches the app.
Request size limits
Bridge requests are capped at 8 MiB per body. Oversized requests are rejected with 413 Payload Too Large before reaching any handler. Combined with the public HTTP server's pre-auth caps (32 MiB generic, 64 KiB on /pair), this prevents an unauthenticated client from forcing unbounded memory allocation.
Secret management
Agents check for and store secrets via sandbox_secret_check and sandbox_secret_set. Secrets are stored in the macOS Keychain, scoped per agent.
Two storage paths
| Path | When | How |
|---|---|---|
| Direct | Agent already has the value (e.g. via Host API or Telegram bot) | Pass value to sandbox_secret_set |
| Prompt | Agent needs the user to provide the value (Chat) | Omit value — a SecureField overlay appears |
The prompt path keeps secret values out of conversation history and LLM context entirely. The execution loop pauses via withCheckedContinuation until the user submits or cancels.
Prompt flow
- Agent calls
sandbox_secret_setwithoutvalue - Tool returns a
secret_promptmarker (JSON with key, description, instructions) - Chat execution loop intercepts and shows
SecretPromptOverlay - User enters secret in
SecureFieldand submits (or cancels) - Value stored in Keychain; tool result rewritten to
{"stored": true, "key": "..."}(or cancelled) - Execution resumes with the sanitized result — the LLM never sees the secret
SecretPromptState tracks a resolved flag so submit() and cancel() are idempotent. onDisappear calls cancel() as a safety net.
Secret containment
Storing a secret is only half the problem — the other half is keeping its value out of model context and every persisted record afterwards:
- Output scrubbing. Agent secrets are injected into the exec environment, so
echo $KEYwould otherwise land the value in the model's context.SecretScrubberrewrites every known secret value insandbox_execstdout/stderr, background-job log tails, and sandbox-plugin tool output to[REDACTED:<ENV_KEY>]before the result is enveloped. Longer values scrub first (substring-safe); values under 6 characters are exempt to avoid false positives. - Argument redaction (fail-closed). When the agent uses the direct
valuepath ofsandbox_secret_set, execution receives the original arguments but every recorded surface receives a secret-safe representation instead: chat and HTTP agent-run history, plugin events and streamed deltas, approval prompts, debug logs, Insights records, and remote-provider wire snapshots. The exact value is also redacted from any sibling string field it was duplicated into, and malformed or ambiguous secret payloads collapse to a minimal redacted object rather than re-emitting uncertain input. The prompt path never carries the value through the model at all and remains the recommended flow.
Security
Path sanitization
All file paths from tool arguments are validated by SandboxPathSanitizer before any container execution. Directory traversal (..) is rejected; paths are resolved relative to the agent's home directory.
Per-agent isolation
Each agent runs as a separate Linux user (agent-{name}). Standard Unix permissions prevent agents from accessing each other's files and processes.
Network policy
Container networking has three modes:
outbound— unrestricted NAT internet access (the default, and an explicit user choice)proxy— the VM boots on a host-only network with no NAT. All egress goes through a filtering HTTP/HTTPS CONNECT proxy on the host. This mode is selected automatically when the provisioning agent has a non-empty Allowed Domains list in its Agent settings.none— no guest networking at all
In proxy mode, enforcement is per-connection and per-agent: the requested hostname must match the agent's resolved allowlist (its own Allowed Domains plus the domains declared by its installed plugins). Patterns are example.com (exact) or *.example.com (subdomains, not the apex). IP literals are rejected outright, and resolved addresses are re-checked on the host — names resolving to loopback, private, link-local, or reserved space are refused (DNS-rebinding defense). Known limitation: the guest can still reach the proxy gateway address itself; everything beyond it is blocked by the host-only network.
Plugins declare their own network requirements in permissions; declarations are validated at install, reinstall, and repair time and feed the runtime allowlist.
Rate limiting
SandboxExecLimiter— caps commands an agent runs per turnSandboxRateLimiter— general rate limiting for sandbox ops and bridge calls
Artifact integrity
Every external artifact the sandbox depends on is pinned to an immutable digest, and downloaded blobs are verified before they touch the on-disk store.
| Artifact | Pin |
|---|---|
GHCR image (ghcr.io/osaurus-ai/sandbox) | Multi-arch index digest (@sha256:...); :latest tag never used at runtime |
| Kata kernel tarball | SHA-256 verified after download against an in-source constant |
| Initfs blob | SHA-256 verified after download against an in-source constant |
A digest mismatch is fail-closed: temp file deleted, no silent fallback to alternate mirrors, provisioning aborts with SandboxError.integrityCheckFailed. Hashing is bounded at 512 MiB.
Diagnostics
Management → Sandbox → Container → Run Diagnostics:
| Check | Verifies |
|---|---|
| Exec | Can execute commands in the container |
| NAT | Outbound network connectivity |
| Agent User | Agent's Linux user exists and can run commands |
| APK | Package manager is functional |
| Vsock Bridge | Host API bridge is reachable from the container |
Container management
| Action | Description |
|---|---|
| Start | Boot the container (provisions first if needed) |
| Stop | Gracefully shut down |
| Reset | Remove and re-provision. Agent workspaces preserved (they live in VirtioFS-mounted /workspace). |
| Remove | Delete container + kernel + initfs. Workspaces preserved. |
Find these under Container → Danger Zone.
Storage paths
| Path | Description |
|---|---|
~/.osaurus/container/ | Container root |
~/.osaurus/container/kernel/vmlinux | Linux kernel |
~/.osaurus/container/initfs.ext4 | Initial filesystem |
~/.osaurus/container/workspace/ | Mounted as /workspace in the VM |
~/.osaurus/container/workspace/agents/{name}/ | Per-agent home |
~/.osaurus/container/output/ | Mounted as /output |
~/.osaurus/sandbox-plugins/ | Plugin library (JSON recipes) |
~/.osaurus/agents/{agentId}/sandbox-plugins/installed.json | Per-agent installed plugin records |
~/.osaurus/config/sandbox.json | Sandbox configuration |
~/.osaurus/config/sandbox-agent-map.json | Linux username → agent UUID mapping |
Related:
- Tasks — the everyday view
- Tool Contract — envelope shape for every tool
- Plugin Authoring — non-sandbox v1/v2 plugins
- Identity Cryptography — how the bridge token, body limits, and pre-auth gating fit together