Documentation / guides
Sandboxing
Build least-authority command and process execution with profiles, OS confinement, and truthful capability reporting.
Sandboxing is the authority boundary for a command or process that may be influenced by an agent, a tool, or user input. You describe the authority once in a ProfileConfig, intersect it with any narrower ceiling, and let an Executor turn the result into an OS-specific spawn. The sandbox reports what the selected host actually enforced. It does not turn an unavailable mechanism into a stronger claim.
This is a separate concern from Tools and from the Harness step that executes tool calls. A Tool prepares an operation and its requirement. Harness decides whether that requirement is admitted. Sandboxing enforces the approved process boundary and can consume a single-use grant. The Harness model request step and Inference’s model selection contract remain the places to understand model configuration, not the place to grant host authority.
For the upstream capability vocabulary and candidate approval flow, pair this guide with Tools’ Safety, Permissions, and Gates.
The boundary at a glance
%%{init: {"theme":"dark"}}%%
flowchart LR
C[ProfileConfig] --> V[NewProfile: validate and normalize]
V --> R[Restrict: component-wise intersection]
R --> S[NewExecutorSet: memoized owner]
S --> E[Executor.For key]
E --> G{Command access}
G -->|Allow| P[Prepare and spawn]
G -->|Gated plus grant| P
G -->|Deny or missing grant| X[No child process]
P --> O[OS backend confinement]
O --> Q[Level, Guarantees, CompileReport]
Gated is a policy decision made before a child exists. Level, Guarantees, and CompileReport describe the separate OS enforcement decision for a spawn. A successful gate does not imply that a host can provide every requested guarantee, and an excellent OS level does not approve a command that policy denied.
Start here
Read the pages in this order:
- Profiles and access dimensions defines
Deny,Gated, andAllow, the filesystem roots,Home, andIsolation. - Restriction and least authority explains how a base profile and a ceiling become one immutable profile.
- Enforcement overview explains how profiles become native policy.
- Platform levels and guarantees explains why
LevelFullis not a promise that every host can make. - Compilation reports shows how to inspect narrowed or unenforced features.
- Filesystem, HOME, and environment covers roots, isolated temporary directories, and environment scrubbing.
- Network routes and target grants covers routes, proxy authorization, and target-scoped network authority.
- Runtime overview explains how compiled authority reaches a child process.
- Executors and ExecutorSets covers ownership, memoization, limits, and cleanup.
- RunArgv and confinement covers shell versus direct argv and the exit-code contract.
- Prepared processes and lifetime covers live pipes, TTY requests, cancellation, and process-tree teardown.
- Typed errors and recovery provides an error classification that survives wrappers.
- Harness gates and prepared Tools connects sandbox admission to the higher-level gate and Tool contracts.
A minimal execution
The example uses a profile with command authority Allow. It still inspects the achieved guarantees because those values are host observations. On Linux, call sandbox.Init as the first line of main so the re-exec helper can dispatch safely.
package main
import (
"context"
"fmt"
"os"
"github.com/looprig/sandbox"
)
func main() {
sandbox.Init()
workspace, err := os.MkdirTemp("", "sandbox-workspace-")
if err != nil {
panic(err)
}
defer os.RemoveAll(workspace)
scratch, err := os.MkdirTemp("", "sandbox-scratch-")
if err != nil {
panic(err)
}
defer os.RemoveAll(scratch)
profile, err := sandbox.NewProfile(sandbox.ProfileConfig{
WorkspaceRoot: workspace,
WorkspaceRead: sandbox.Allow,
WorkspaceWrite: sandbox.Allow,
HostRead: sandbox.Allow,
HostWrite: sandbox.Allow,
Network: sandbox.Allow,
Command: sandbox.Allow,
Home: sandbox.IsolatedHome,
Isolation: sandbox.Sandboxed,
})
if err != nil {
panic(err)
}
set, err := sandbox.NewExecutorSet(profile,
sandbox.WithScratchRoot(scratch),
sandbox.WithMaxExecutors(1),
)
if err != nil {
panic(err)
}
defer set.Close()
executor, err := set.For("demo")
if err != nil {
panic(err)
}
output, exitCode, err := executor.RunArgv(context.Background(), workspace, []string{"printf", "hello\\n"})
if err != nil {
panic(err)
}
fmt.Printf("exit=%d output=%q level=%d guarantees=%+v\\n",
exitCode, output, executor.Level(), executor.Guarantees())
}
The code owns the ExecutorSet, not the caller-owned scratch parent. Close removes the set’s child, revokes executor grants, and makes future work fail closed. See executor ownership before sharing one set across workers.