docker

package
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 24, 2026 License: MIT Imports: 17 Imported by: 0

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

View Source
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

func New(ctx context.Context, dir string, opts Options) (*Sandbox, error)

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

func (s *Sandbox) Close() error

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

func (s *Sandbox) Exec(ctx context.Context, cmd sandbox.Command) (*sandbox.Result, error)

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

func (s *Sandbox) NewSession(_ context.Context) (sandbox.Session, error)

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

func (s *Sandbox) StartProcess(ctx context.Context, cmd sandbox.Command) (sandbox.Process, error)

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).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL