Documentation
¶
Overview ¶
Package docker is the container-backed sandbox backend. It runs the agent's Exec inside a container (the IsolationProcess tier — a shared host kernel) and, by flipping one runtime flag to gVisor's runsc, the IsolationKernel tier — a userspace application kernel. File ops (ReadFile/WriteFile/ListDir + the path fence) are delegated to an embedded *local.Sandbox operating host-side on the workspace dir, which is bind-mounted into the container at /workspace so the host write and the in-container read see the same bytes (same inode via the mount). See ../../docs/specs/SANDBOX.md for the decided direction and ../sandbox.go for the interface this implements.
The threat model is narrow and deliberate: the EXECUTED command is hostile. File reads/writes/listing are OUR Go code (already fenced) on the workspace — not part of the threat surface. So only Exec is containerized; everything else rides the tested local fence.
Index ¶
- Constants
- type Options
- type Sandbox
- func (s *Sandbox) Capabilities() sandbox.Capabilities
- func (s *Sandbox) Close() error
- func (s *Sandbox) Exec(ctx context.Context, cmd sandbox.Command) (*sandbox.Result, error)
- func (s *Sandbox) NewSession(_ context.Context) (sandbox.Session, error)
- func (s *Sandbox) StartProcess(ctx context.Context, cmd sandbox.Command) (sandbox.Process, error)
Constants ¶
const ( DefaultImage = "driver-os-sandbox:latest" DefaultWorkdir = "/workspace" DefaultMemory = "512m" DefaultPidsLimit = 256 DefaultCPUs = "1.0" DefaultBinary = "docker" // DefaultMaxOutputBytes caps retained stdout/stderr per Exec (8 MiB each) so a // flooding command can't OOM the agent. DefaultMaxOutputBytes = 8 << 20 // DefaultStartTimeout bounds container startup (NOT command execution — that's // sandbox.Command.Timeout). Generous because a first-run image pull can be slow. DefaultStartTimeout = 2 * time.Minute )
Defaults. Tuned for "run one untrusted CPU command safely", not for a long-lived service. Override per Options field when a task needs more.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Options ¶
type Options struct {
// Image is the container image to run. It MUST contain everything the toolset
// shells out to — sh (the run tool), rg (the search tool execs it directly),
// git, and go if in-sandbox builds are expected. See ./Dockerfile. Empty =>
// DefaultImage.
Image string
// Runtime selects the OCI runtime. "" (or "runc") is the standard
// shared-kernel container => IsolationProcess. "runsc" is gVisor — syscalls
// hit a userspace app-kernel, not the host kernel => IsolationKernel. This is
// the one-flag gVisor graduation (docs/specs/SANDBOX.md): it threads straight into
// `docker run --runtime=`. The host must have the runtime registered with the
// daemon; we don't verify that here — a missing runtime surfaces as a New
// error from the daemon.
Runtime string
// Network, when false (the default), runs the container with --network none:
// no exfiltration, no callbacks, no module fetches. This is the point for
// genuinely untrusted code — the workspace must be self-contained (see the
// module story: ExtraMounts can carry a read-only GOMODCACHE). Set true for
// trusted-but-isolated cases that need egress.
Network bool
// User is the in-container user as "uid:gid". Defaulting it to the HOST uid:gid
// (New does this when empty) serves two purposes at once: it runs non-root in
// the container, AND files the container writes to the bind mount are owned by
// the host user, so host-side file ops can read them back (the uid-mismatch
// gotcha, §13). Set explicitly to override.
User string
// WorkdirMount is the in-container path the workspace dir is bind-mounted to
// and the working directory for every Exec. Empty => DefaultWorkdir
// ("/workspace"). The mount is the ONLY writable mount; the root filesystem is
// read-only.
WorkdirMount string
// Memory, PidsLimit, CPUs cap resource use to contain a fork bomb, a memory
// bomb, or CPU spin (§6). They map to --memory, --pids-limit, --cpus. Empty/0
// => the DefaultLimits below. A limit of "0" / NoLimit can be set explicitly to
// disable a given cap (not recommended for untrusted code).
Memory string // e.g. "512m"; "" => DefaultMemory.
PidsLimit int // e.g. 256; 0 => DefaultPidsLimit; negative => unlimited.
CPUs string // e.g. "1.0"; "" => DefaultCPUs.
// WritableRootFS drops the --read-only root filesystem. The default (false,
// read-only) is the right posture for OUR sandbox image, where the workspace
// mount is the only thing a task should write. Set true ONLY for prebuilt
// third-party images whose tooling must write outside the workspace — e.g.
// SWE-bench instance images, where the conda env under /opt/miniconda3 needs
// .pyc/install writes for the baked-in test tooling to run. This widens the
// blast radius from "workspace only" to "container filesystem" — the container
// boundary (caps dropped, no-new-privileges, resource caps, --rm teardown) is
// then the whole guarantee, and the rootfs is no longer attestable as pristine.
WritableRootFS bool
// ExtraMounts are additional bind mounts as raw `docker run -v` specs, e.g.
// "/home/u/go/pkg/mod:/go/pkg/mod:ro" to give in-container `go build` a
// read-only host module cache while --network none stays on (the module story,
// §13). Each is passed through verbatim; the caller owns the trust decision for
// what it mounts (mount read-only for anything outside the workspace).
ExtraMounts []string
// ExtraEnv are environment variables set on every Exec, each "KEY=VALUE". Use
// for things like GOFLAGS=-mod=mod or GOPROXY=off that a network-off build
// needs. Per-command Env (sandbox.Command.Env) is merged on top of these.
ExtraEnv []string
// Binary is the container CLI to invoke. Defaults to "docker"; Podman is
// CLI-compatible, so setting "podman" works without any other change. We add no
// Podman-specific code — this is the entire Podman story (a configurable
// binary), per the design decision.
Binary string
// StartTimeout bounds `docker run` (image pull + container start). 0 =>
// DefaultStartTimeout. A slow pull shouldn't hang New forever.
StartTimeout time.Duration
// MaxOutputBytes caps how much stdout (and, separately, stderr) Exec retains
// from a command. Hostile code can flood output (`yes`, a stderr loop) to OOM
// the agent; the runner stops STORING past this many bytes while still draining
// the stream so the process isn't blocked. 0 => DefaultMaxOutputBytes; negative
// => unlimited (only for trusted use). The tool layer clips again for the model
// (agent/tools.go); this is the memory-safety bound underneath it (P4).
MaxOutputBytes int64
}
Options configures a docker Sandbox. The zero value is unusable on its own — New fills in defaults via withDefaults — but every field is documented with the specific escape or failure mode it governs so the security posture is legible at the call site, not buried in New.
type Sandbox ¶
type Sandbox struct {
*local.Sandbox // file ops + path fence, host-side on the workspace dir (D1).
// contains filtered or unexported fields
}
Sandbox is the container-backed sandbox.Sandbox. It composes the local backend (D1): file ops and the path fence are the embedded *local.Sandbox operating host-side on the workspace dir, while Exec — the ONLY part of the threat surface — is overridden to run inside a long-lived container (D3). Capabilities is also overridden to report the real isolation level instead of local's "none".
func New ¶
New starts a long-lived container rooted at the workspace dir and returns a Sandbox that execs commands inside it. The container runs `sleep infinity` and stays up for the Sandbox's lifetime (D3); every Exec is a `docker exec` against it, and Close tears it down. dir is bind-mounted to opts.WorkdirMount (/workspace) read-write — the only writable mount — so a host-side WriteFile and an in-container read see the same bytes.
Callers owning an UNTRUSTED task should root this at a throwaway copy, not the live checkout (§13) — the backend just takes dir; the trust decision is the caller's.
func (*Sandbox) Capabilities ¶
func (s *Sandbox) Capabilities() sandbox.Capabilities
Capabilities OVERRIDES the embedded local backend's (which reports IsolationNone). It reports the real boundary: IsolationKernel under gVisor (Runtime=="runsc"), IsolationProcess otherwise, and whether the network is on. This is what the MinIsolation policy gate reads to decide if untrusted code may run here.
func (*Sandbox) Close ¶
Close stops and removes the container. It is idempotent (a second call, or a call after a failed start, is a no-op) and best-effort — a leaked container is worse than a noisy error, so we always attempt the removal. Because the container was started with --rm, `docker rm -f` both kills and removes it.
func (*Sandbox) Exec ¶
Exec runs cmd inside the container via `docker exec`. The contract matches local.Exec exactly (Slice 3 conformance): a non-zero exit is a normal *Result, not a Go error; err is reserved for the docker machinery failing. cmd.Timeout and ctx cancellation both bound the run, whichever fires first, and the kill sets Result.TimedOut.
On timeout the in-container `timeout -s KILL <secs>` SIGKILLs the command it launched (the common runaway-loop case). It does NOT chase a process that deliberately daemonizes itself into a new session — that is what the CONTAINER boundary is for: such a process is confined, capped by --pids-limit/--memory, and destroyed when Close does `docker rm -f`. The timeout is a liveness lever to hand control back to the agent loop; the container is the security boundary (cf. the gated policy's "heuristic, not a boundary" note).
func (*Sandbox) NewSession ¶
NewSession opens a stateful session over THIS already-running container — no new container is started; successive Exec calls just share shell state via a state file under the container's /tmp tmpfs (writable and container-lifetime). The state dir is unique per session (pid + counter) so independent sessions over the same container don't collide. The session's Close removes that dir; the container itself is reaped by the Sandbox's own Close.
func (*Sandbox) StartProcess ¶
StartProcess launches cmd as a long-lived process in the container. Streams are the `docker exec -i` client's stdio (NOT -t: a TTY would cook and merge the streams, corrupting framed binary protocols like LSP). cmd.Stdin/cmd.Timeout are ignored (live stdin + caller-managed lifetime).