sandbox

package module
v0.1.3 Latest Latest
Warning

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

Go to latest
Published: Jun 14, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

README

sandbox

github.com/strongo/sandbox runs a command in a hardened, single-use Docker sandbox: inject inputs, run the command to completion under resource and timeout limits, collect output artifacts, then always tear the container down.

The hardened container profile (read-only rootfs, CAP_DROP=ALL, non-root user, no-new-privileges, default seccomp, init) is applied unconditionally via the isolation subpackage and cannot be weakened through the public API. Only the tunable knobs (CPU / memory / PID caps, network, timeout) are exposed.

Packages

  • github.com/strongo/sandboxRunOnce, Job, Limits, Mount, Result.
  • github.com/strongo/sandbox/isolation — the shared hardened Docker preset (Preset, NonRootUser, …).

Testing

go test ./... runs the unit tests and never requires a Docker daemon. The Docker integration test is env-guarded: it is skipped under go test -short and unless SANDBOX_INTEGRATION=1 is set.

SANDBOX_INTEGRATION=1 go test -run Integration ./...

License

Apache License 2.0. Copyright 2026 Sneat.co. See LICENSE and NOTICE.

Documentation

Overview

Package sandbox provides a reusable, one-shot sandboxed command runner built on the same Docker client and hardened isolation preset (the isolation subpackage) that synchestra's long-lived runner uses.

Where the synchestra runner targets long-lived agent-in-container sessions that register back over gRPC, this package fills the complementary need: run a single command to completion inside a locked-down container, capture its logs and selected artifact files, then always tear the container down. It is intended for external modules — e.g. a coverage runner that executes `go test -coverprofile` inside a cloned repo and collects the produced profiles.

Security flags (read-only rootfs, CAP_DROP=ALL, non-root 65532, no-new-privileges, the daemon's default seccomp profile, init) are applied unconditionally via the shared isolation preset and cannot be weakened. The tunable knobs (CPU/memory/PID caps, network, timeout) live on Limits.

Index

Constants

View Source
const DefaultInputDir = "/workspace"

DefaultInputDir is where Job.InputTar is unpacked when Job.InputDir is empty. It is one of the preset's writable tmpfs paths.

View Source
const DefaultNetwork = "bridge"

DefaultNetwork is the network mode used when Job.Limits.Network is empty.

Tradeoff: a fully sealed sandbox would use "none", but the canonical workload for this package — `go test` against a cloned repo — needs network egress to reach the module proxy / GOPROXY and git. We therefore default to "bridge" (NAT egress, no inbound, no host network) rather than "none". The container is still hardened in every other dimension (read-only rootfs, dropped caps, non-root, seccomp). Callers that do not need egress should set Limits.Network = "none" explicitly.

Variables

This section is empty.

Functions

This section is empty.

Types

type Job

type Job struct {
	Image   string            // runner image carrying the toolchain
	Cmd     []string          // command to run in the container
	Env     map[string]string // environment variables
	WorkDir string            // working directory inside the container
	Mounts  []Mount           // trusted, read-only bind mounts into the container

	// InputTar is an OPTIONAL tar archive of files/dirs handed to the workload as
	// a writable source tree, with no path back to the host filesystem. This is
	// the safe way to give untrusted code (e.g. `go test`) something to run on. A
	// `git archive` tarball can be passed straight in.
	//
	// It is injected into an ephemeral named VOLUME mounted at InputDir, not a
	// host bind mount: a trusted prep container creates the volume, chmods it so
	// the non-root run user can write, and the tar is copied in while that prep
	// container is running. The hardened main container then mounts the populated
	// volume; the volume is removed when RunOnce returns. (Copying into the
	// stopped main container instead would land on its rootfs and block the
	// volume from mounting — hence the prep step.)
	//
	// The image must provide a POSIX shell with `chmod` and `sleep` (busybox,
	// alpine, debian, golang, … all qualify). Tar entry paths are relative to
	// InputDir (entry "main.go" -> <InputDir>/main.go). Set WorkDir to InputDir
	// (or a subdir) to run Cmd there.
	InputTar io.Reader
	InputDir string // absolute dir the InputTar is unpacked into; defaults to "/workspace"

	Collect []string // absolute in-container paths to copy out after exit
	Limits  Limits
}

Job is one sandboxed command to run to completion.

type Limits

type Limits struct {
	CPUs     float64       // CPU quota, e.g. 2.0; 0 = unlimited
	MemoryMB int64         // RAM cap in MiB; 0 = unlimited
	PIDs     int64         // max processes; 0 = unlimited
	Timeout  time.Duration // hard wall-clock kill; 0 = no timeout
	Network  string        // "none" | "bridge" | named network; empty = DefaultNetwork
}

Limits are the tunable, non-security knobs of the hardened preset. Zero values mean "unset" (Docker default) except where noted.

type Mount

type Mount struct {
	Source   string // host path
	Target   string // container path
	ReadOnly bool
}

Mount describes a bind mount from the host filesystem into the container. It mirrors the existing internal Mount type so callers and the rest of the codebase speak the same shape.

Mounts are intended for TRUSTED, read-only host data. Read-WRITE host bind mounts are discouraged for untrusted workloads (e.g. running arbitrary `go test` code): a malicious test could corrupt host files through the mount. To give untrusted code a writable source tree, inject it as a tar via Job.InputTar instead — the container then works on its own ephemeral copy on writable tmpfs, with no path back to the host filesystem.

type Result

type Result struct {
	ExitCode  int               // container exit status
	Logs      []byte            // combined, de-multiplexed stdout+stderr
	Artifacts map[string][]byte // Collect path -> file bytes (missing paths are omitted)
	TimedOut  bool              // true if the container was killed at Limits.Timeout
}

Result is the outcome of a RunOnce call.

func RunOnce

func RunOnce(ctx context.Context, j Job) (Result, error)

RunOnce pulls Image if it is not already present, creates a container with the hardened preset (tuned by Limits), runs Cmd to completion (or kills it at Limits.Timeout), captures combined stdout+stderr, copies out the Collect artifacts, and always removes the container before returning.

The returned Result is populated on a best-effort basis: a non-zero ExitCode or a timeout is reported through Result, not as an error. A non-nil error means the run could not be carried out (pull/create/start/wait failure); even then the container is force-removed.

Directories

Path Synopsis
Package isolation is the single source of truth for the hardened container security preset ("profile B" in the generic-runners design spec): read-only rootfs, all Linux capabilities dropped, a non-root user, no-new-privileges, the default seccomp profile, an init process to reap zombies, and CPU / memory / PID resource caps.
Package isolation is the single source of truth for the hardened container security preset ("profile B" in the generic-runners design spec): read-only rootfs, all Linux capabilities dropped, a non-root user, no-new-privileges, the default seccomp profile, an init process to reap zombies, and CPU / memory / PID resource caps.

Jump to

Keyboard shortcuts

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