@pugi/plugin-sandbox
v0.1.0-alpha.4
Published
Pugi sandbox plugin - throws on policy violations in tool.execute.before; wraps shell calls in Seatbelt (macOS), bubblewrap (Linux), or Docker (opt-in cross-platform).
Maintainers
Readme
@pugi/plugin-sandbox
OS-level isolation for Pugi tool calls. Wraps bash invocations in Seatbelt (macOS) or bubblewrap (Linux), enforces a binary allowlist, blocks classes of dangerous syntax (
rm -rf /,eval, subshell substitution, system redirects), and persists every decision to a JSONL audit log.
Part of the Pugi 1.0 soft fork sprint (see ADR-0081).
Status
Day 49 ships Docker as an opt-in third wrap strategy alongside Seatbelt
(macOS default) and bubblewrap (Linux default). 51 node:test specs cover
parser, path guard, profile generators, audit log, hook surface, and the
new Docker argv builder + strategy chooser. Audit mode remains the
default; flip to enforce once your team has reviewed the audit log
and tuned the allowlist.
Install
pnpm add @pugi/plugin-sandboxUsage
// pugi.config.ts
export default {
plugin: [
[
'@pugi/plugin-sandbox',
{
mode: 'audit', // 'audit' | 'enforce' | 'permissive'
profile: 'workspace', // 'minimal' | 'workspace' | 'network-allow' | 'custom'
workspaceRoot: process.cwd(), // defaults to the Pugi worktree
allowedDomains: ['anvil.pugi.io', 'api.pugi.io', 'github.com'],
allowedBinaries: ['node', 'npm', 'pnpm', 'git', 'bun'],
enableNetworkEgress: true, // flip to false for offline runs
},
],
],
};Modes
| Mode | Behaviour | When to use |
|---|---|---|
| audit (default) | Logs violations to .pugi/sandbox-audit.jsonl. Never throws (except for rm -rf outside workspace, which is always blocked). | Onboarding new repos. Lets you tune the allowlist before flipping to enforce. |
| enforce | Throws SandboxViolation on any violation. Wraps every bash call in the OS sandbox. | Production / customer-facing runs. |
| permissive | Skips violation throws but still wraps commands. | Break-glass debugging only. |
Docker wrap strategy (Day 49, opt-in)
Set preferDocker: true to route every wrapped bash call through a
docker run invocation instead of the OS-native sandbox. Useful for:
- Cross-platform parity (the same image runs the same on macOS, Linux, and Windows).
- Workloads that need stronger filesystem isolation than Seatbelt can provide on macOS.
- Windows hosts, which previously fell through to audit-only mode.
Existing Linux and macOS customers see no behaviour change when
preferDocker is unset; bubblewrap and Seatbelt remain the defaults.
export default {
plugin: [
[
'@pugi/plugin-sandbox',
{
mode: 'enforce',
preferDocker: true, // opt-in
dockerImage: 'ghcr.io/pugi-io/sandbox:default',
dockerMemoryLimit: '512m',
dockerCpuQuota: '1.0',
dockerWorkspaceWritable: false, // mount /workspace ro by default
dockerAllowNetwork: false, // --network none unless opted in
dockerEnvAllowlist: ['HOME', 'PATH', 'LANG', 'LC_ALL'],
},
],
],
};Per-run security posture applied to every Docker invocation:
| Flag | Value | Purpose |
|---|---|---|
| --rm | always | Ephemeral container; nothing persists. |
| --read-only | always | Root filesystem is read-only. |
| --tmpfs /tmp:size=64m | always | Small writable scratch only. |
| --security-opt no-new-privileges | always | Blocks setuid escalation. |
| --cap-drop ALL | always | Drops every Linux capability. |
| --pids-limit 64 | always | Prevents fork-bomb. |
| --ulimit nofile=128:128 | always | Caps file descriptors. |
| --network none | unless dockerAllowNetwork | Default offline. |
| -v ws:/workspace:ro | unless dockerWorkspaceWritable | Read-only mount. |
| --memory <limit> | from dockerMemoryLimit | cgroup memory cap. |
| --cpus <quota> | from dockerCpuQuota | cgroup cpu cap. |
| --env <name>=<value> | per dockerEnvAllowlist | Only listed env vars cross. |
Operator prerequisites
- Docker Engine reachable from the Pugi process (Docker Desktop on macOS / Windows, native docker on Linux).
- The default image
ghcr.io/pugi-io/sandbox:defaultavailable on the host. Customers can warm it viapugi doctor --warm-sandboxon first install, or pre-pull it during provisioning. - To rebuild the default image from source:
docker build -t ghcr.io/pugi-io/sandbox:default \ -f packages/pugi-plugins/sandbox/docker/Dockerfile.default . docker push ghcr.io/pugi-io/sandbox:default
Performance impact
A cold docker run on the default image adds roughly 200-500 ms of
container start overhead per bash call. For interactive workloads this
is unnoticeable; for tight loops consider keeping preferDocker: false
and relying on the OS-native wrapper.
Timeout
Per-call wallclock cap is enforced by the outer Pugi bash tool, not by
the sandbox plugin. Tune it via the existing tool-level timeout in
pugi.jsonc rather than expecting a Docker-specific knob here -
duplicating the timeout inside the sandbox would race the outer cap
and only Linux ships timeout(1) out of the box.
Profile matrix
| Profile | File access | Network | Binaries | Notes |
|---|---|---|---|---|
| minimal | Read system, no workspace write | Off | Allowed list only | Use for read-only analysis runs. |
| workspace (default) | Read system + workspace; write workspace + /tmp | Egress to allowed domains | Allowed list only | The CEO-blessed default for Pugi. |
| network-allow | Workspace read/write | Egress unrestricted | Allowed list only | Use when an LLM needs to fetch arbitrary packages. |
| custom | Whatever your .sb / bwrap-args.json says | Per profile | Per profile | For advanced teams. Requires customProfile. |
Threat model
In scope
- Accidental dangerous commands from the LLM (
rm -rf /,eval, network exfil to unknown hosts). - Path traversal to sensitive files (
~/.ssh,~/.aws,~/.env). - Command injection via subshell substitution (
$(...), backticks) or piping to a fresh shell (| sh,| bash). - Workspace data exfiltration via
curl/wgetto unauthorized domains (best-effort on macOS due to Seatbelt limitations; properly enforced on Linux when network egress is disabled). - Writing to system paths even when the user accidentally allowlists a
shell command that contains a
>redirect.
Out of scope (document explicitly so users do not get a false sense of security)
- Kernel exploits.
sandbox-execandbubblewrapdepend on the host kernel's security model. A kernel CVE that lets unprivileged code escape the sandbox is not something this plugin can defend against. - Side-channel attacks. Timing, cache, and Spectre-class attacks are outside the scope of any user-space sandbox.
- Network MITM. Use TLS verification and certificate pinning at the HTTP-client layer; the sandbox does not inspect TLS traffic.
- Supply chain.
npm installof a malicious dependency executes the install scripts inside the sandbox, but the sandbox still allows network egress toregistry.npmjs.orgby default. Use the personas signing flow in@pugi/plugin-personasfor supply-chain defence. - macOS Seatbelt does NOT do reliable host-level network filtering.
allowedDomainsis advisory only on darwin; usenetwork-allow: offfor true offline runs on macOS, or move to Linux/Docker for real egress gating.
Defence in depth
The plugin layers protections so a bypass in one layer does not silently become an exploit:
- Command parser rejects
rm -rf,eval, subshell substitution, pipe-to-shell, and system-path redirects regardless of OS. - Binary allowlist restricts which executables can run.
- Path guard blocks
..segments, absolute paths outside the workspace, and symlinks pointing outside the workspace. - OS sandbox wraps the resulting command in
sandbox-exec(macOS) orbwrap(Linux) so even if a bypass slips through, the kernel contains it. - Audit log records every decision for post-mortem.
Threat actor model
We assume the LLM is the only adversary inside the sandbox. The user
is trusted (they can always run bash outside Pugi). External network
attackers are out of scope; that is the firewall and reverse-proxy
team's problem.
Audit log format
.pugi/sandbox-audit.jsonl:
{
"ts": "2026-06-06T12:34:56.789Z",
"mode": "enforce",
"violation": "binary_not_allowed",
"tool": "bash",
"command": "docker run hello-world",
"binary": "docker",
"sessionId": "abc123",
"decision": "blocked"
}Successful tool calls in audit mode go to .pugi/sandbox-success.jsonl
so the violation log stays focused.
Hook surface
tool.execute.before- inspect, audit, wrap.tool.execute.after- lightweight success audit in audit mode.
License
MIT. See LICENSE.
