runner

package
v0.8.1 Latest Latest
Warning

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

Go to latest
Published: Sep 29, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package runner defines the target-agnostic sandbox lifecycle contract. The control plane ONLY talks to this interface — zero Docker/Kubernetes-specific code. Drivers live in subpackages and are conformance-tested identically.

The deployment's ENV ceiling over user drives whose bytes live on a host path (WARDYN_USER_DRIVE_HOST_ROOTS): operator/MDM-set, never a row the product itself writes, so a console compromise cannot widen it. Unset fails closed — no roots configured means no host_path drive may be authored at all.

Index

Constants

View Source
const (
	ScratchTmpPath   = "/tmp"
	ScratchWorkPath  = "/home/agent/work"
	ScratchCachePath = "/home/agent/.cache"
)

ScratchTmpPath, ScratchWorkPath and ScratchCachePath are where the Kubernetes substrate mounts its three disk_mib-sized emptyDirs, the bytes that substrate's `eviction` enforcement counts against a run's disk cap. Live here, not private to that driver, so the run page's disk-used reading walks exactly the paths the kubelet meters and the two can't drift.

View Source
const (
	DefaultCPUMillis int64 = 2000 // 2 vCPU
	DefaultMemoryMiB int64 = 4096 // 4 GiB hard memory cap
	DefaultPidsLimit int64 = 512  // max processes/threads (fork-bomb guard)
)

Conservative platform resource defaults, applied when a Resources field is zero, so EVERY agent sandbox is capped even when policy sets nothing: without them one runaway or prompt-injected agent can OOM-kill the host, fork-bomb the host PID space, or fill host storage and take sibling runs down with it (see types.ResourceLimits). A policy value always overrides.

View Source
const (
	// DriveSourceRefused covers every way the source check can refuse the
	// resolved path; which reason applies is an operator-filesystem fact, so
	// it goes to the log, not here.
	DriveSourceRefused = "cannot be bound on this host: the directory this deployment resolved for it is not one a drive may bind here — " +
		"wardynd's log names the rule that refused it, and an operator can fix it"
	// DriveHomeNameRefused is the sibling-symlink case; the path it would name
	// is another principal's home directory.
	DriveHomeNameRefused = "resolves to a directory named after somebody else — a home replaced by a link to a sibling " +
		"would bind another person's directory"
)

The MEMBER halves of the two bind refusals the Docker driver makes about a share's source. Constants rather than literals at the call sites so the audience rule is checkable in one place: neither may name a path, and RefuseUserDriveBind's own test asserts they do not.

View Source
const AgentIdleScript = `` /* 1038-byte string literal not displayed */

AgentIdleScript is the agent sandbox's main (idle) process for non-interactive runs: installs the per-run TLS-MITM CA (when delivered), required so claude trusts the proxy's TLS termination of api.anthropic.com, then idles while the task Exec does the real work. Writes the exact paths internal/api pins (/tmp/wardyn, any-uid-writable) and assembles the COMBINED bundle (system roots + per-run CA) that SSL_CERT_FILE/REQUESTS_CA_BUNDLE/CURL_CA_BUNDLE point at, since those vars REPLACE the client trust store. Keep in lockstep with install_mitm_ca in deploy/images/common/agent-run-lib.sh. No-op when WARDYN_MITM_CA_PEM is unset. The idle loop is TERM-aware: as PID 1, a bare `sleep` ignores SIGTERM and would wait out the full kill timeout; this exits 143/130, not 0, so an out-of-band stop still reads as a signal kill downstream.

View Source
const DefaultManagedFileMode fs.FileMode = 0o644

DefaultManagedFileMode is the mode a ManagedFile with Mode == 0 is delivered with: readable by the agent, writable only by root.

View Source
const DriveTarget = "/home/agent/drive"

DriveTarget is the RESERVED in-container path a user drive mounts at, under /home/agent. Reserving it stops an authored workspace mount, repo, or source from landing on the same path and shadowing (or being shadowed by) the member's persistent storage — reserved rather than first-come, since the two are authored by different people at different times, and a collision would surface as silent disappearance rather than a refusal.

View Source
const ManagedFileDir = "/etc/claude-code"

ManagedFileDir is the one directory a managed file may be delivered into — where Claude Code reads its managed settings on Linux, the only consumer. It is an allowlist, not a path-shape rule, because the ceiling depends on where the file is: /etc is root-owned and unwritable by the agent, nothing loosens it after delivery, and no Wardyn image ships it. A second location joins only once checked against each of those.

View Source
const ManagedFilesMaxBytes = 256 << 10

ManagedFilesMaxBytes caps the total content one spec may carry. The binding constraint is Kubernetes: every managed file rides the same per-run Secret (capped at 1 MiB) as the proxy config and SecretEnv, so this is refused on both substrates rather than diverging into docker-succeeds/k8s-413s.

View Source
const ProxyListenPort = 3128

ProxyListenPort is the port wardyn-proxy listens on inside the per-run internal network/namespace. The agent's ONLY reachable address.

Variables

View Source
var ErrDriveInUse = errors.New("runner: the drive's storage is still held by a running sandbox")

ErrDriveInUse is the sentinel a DriveReclaimer returns when a sandbox still holds the object; the caller answers 409. A refusal rather than a force-delete: a forced remove would pull the volume out from under a live agent mid-write.

View Source
var ErrDriveNotReclaimable = errors.New("runner: the object under this drive's name is not the storage it allocated")

ErrDriveNotReclaimable is the sentinel a DriveReclaimer returns when the object answering to the drive's name is NOT the storage it allocated — a reclaim that gets this wrong deletes another member's files.

View Source
var ErrEndUnsupported = errors.New("runner: this substrate cannot keep an ended sandbox")

ErrEndUnsupported is EndSandbox's (and StopProxy's) answer from a router whose substrate for ref cannot keep a stopped (or lost) sandbox.

View Source
var ErrExecNeverStarted = errors.New("runner: agent exec never started")

ErrExecNeverStarted: a Runner's Wait returns this when it can prove the agent exec will NEVER reach a terminal state (e.g. ImagePullBackOff) — distinct from every other Wait error ("might still be running, retry").

View Source
var ErrExecStreamUnsupported = errors.New("runner: ExecStream not supported")

ErrExecStreamUnsupported is the sentinel a Runner/Substrate returns from ExecStream when unimplemented, so the conformance suite can skip cleanly.

View Source
var ErrFreezeUnsupported = errors.New("runner: this substrate cannot freeze a sandbox")

ErrFreezeUnsupported is Freezer's answer from a router that cannot pause ref.

View Source
var ErrProxyReplaceFailed = errors.New("runner: the old proxy may be gone and the new one did not start")

ErrProxyReplaceFailed marks a ReplaceProxy that removed, or may have removed, the old proxy and did not start the new one.

View Source
var ErrReviveUnsupported = errors.New("runner: this substrate cannot replace a sandbox's proxy")

ErrReviveUnsupported is ProxyReviver's answer from a router whose substrate for ref cannot replace a proxy in place.

View Source
var TerminalWaitingReasons = map[string]bool{
	"ImagePullBackOff":           true,
	"ErrImagePull":               true,
	"CreateContainerError":       true,
	"CreateContainerConfigError": true,
	"InvalidImageName":           true,
	"CrashLoopBackOff":           true,
}

TerminalWaitingReasons are the substrate "still waiting" reasons that will never resolve on their own (waiting longer changes nothing; only a person fixing the image, credential, or entrypoint does).

Lives here in the tagless runner package, not the k8s driver that reads it, because three places must agree on the same six strings and two can't see a `k8s`-tagged symbol: the driver's fast-fail poll, the control plane's read projection (api/runs_status_detail.go), and the console's mirror (ui/.../run-status-detail.ts's TERMINAL_STATUS_REASONS). Not exhaustive — every other reason is covered by the wait's own timeout.

Functions

func BuildProxyConfig added in v0.5.0

func BuildProxyConfig(runID uuid.UUID, pc ProxyConfig, port int) ([]byte, error)

BuildProxyConfig marshals a run's ProxyConfig (egress policy, MITM CA, injection rules, run token, ...) into the JSON payload every substrate delivers to its wardyn-proxy sidecar (on stdin on Docker, as a staged file on Kubernetes; never in its environment) — the proxy fails closed without it. port is the sidecar's listen port (ProxyListenPort in production; parameterized for tests).

func DeniedSourcePrefix added in v0.7.0

func DeniedSourcePrefix(src string) string

DeniedSourcePrefix returns the deny-list prefix that refuses src, or "" for every OTHER refusal reason (empty, relative, uncleaned, host root, runtime socket).

Exists for the one surface with its own FROZEN vocabulary for this refusal (a user drive's host_root, UserDriveHostRootCheck): naming the prefix lets it explain WHICH prefix bit without keeping a second copy of the list to drift out of sync.

Answers on the same TWO paths deniedSource runs against — lexical and symlink-resolved — so a root resolving INTO a denied tree names the prefix that caught it. A resolve error is not this function's to report: it answers "" and the caller's own ValidateMountSource call says why.

func DriveSubject added in v0.7.0

func DriveSubject(drive *types.DriveMount) string

DriveSubject names a mount the way a member-facing refusal may: which drive, whose directory, and NO PATH. A mount carrying no drive name (older control plane, or a directly-built spec) names the directory alone — never the object, which on a share IS the operator's absolute path.

Exported so the driver's own refusals (RefuseUserDriveBind, docker's drive arm) compose the same sentence rather than drift on what may be disclosed.

func IsKnownNonVaultRuntime added in v0.5.0

func IsKnownNonVaultRuntime(name string) bool

IsKnownNonVaultRuntime reports whether name is a runtime positively known to deliver less than a VM boundary (the CC3/Vault floor guard). Prefix match, so "runc"/"crun-foo"/"sysbox-runc"/"runsc-*" are all caught. "krun" is NOT matched by the "crun" family (different leading byte), so libkrun (a real KVM microVM) stays eligible for CC3.

func IsTerminalWaitingReason added in v0.7.6

func IsTerminalWaitingReason(reason string) bool

IsTerminalWaitingReason reports whether reason is one that waiting cannot fix. The empty reason is not: "we have not read one yet" is not a verdict.

func ManagedFileDirs added in v0.8.0

func ManagedFileDirs(files []ManagedFile) []string

ManagedFileDirs returns every distinct parent directory across files, sorted, so a driver that materialises directories (docker's archive) or mount points (the k8s Secret volumes) walks them deterministically.

func MountCeilingOverlapWarnings added in v0.7.0

func MountCeilingOverlapWarnings(member UserMountPolicy, driveRoots []string) []string

MountCeilingOverlapWarnings returns boot WARNINGS when the two operator-set mount ceilings (WARDYN_USER_WORKSPACE_ROOTS and WARDYN_USER_DRIVE_HOST_ROOTS) name overlapping trees. SECURITY: a drive's host_root binds only one person's subdirectory, while a workspace root may be bound WHOLE and writable — overlap lets a member onboard a drive's share as a workspace and bind every person's home through a surface that never consults allocation.

A WARNING, not a refusal: an operator may have deliberately opened a tree to both, and refusing at boot would take a running deployment down on upgrade. Comparison is LEXICAL and covers the shared list plus every per-principal override.

func ParseUserDriveHostRoots added in v0.7.0

func ParseUserDriveHostRoots(raw string) (roots []string, warnings []string, err error)

ParseUserDriveHostRoots parses WARDYN_USER_DRIVE_HOST_ROOTS — a CSV of absolute, already-cleaned host directories — into the roots a host_path drive may be authored inside, plus boot WARNINGS for a dangerously wide root. Reuses parseRootList, so a malformed value REFUSES BOOT.

Three values are allowed but warned about: the daemon's own $HOME (too WIDE); "/" (DEAD — withinAnyRoot never matches it, so it refuses every drive); and a root under a denied bind prefix (dead the same way).

The deny-list runs LEXICALLY on the configured value, not the resolved path: boot must not touch a share that may not be mounted yet. UserDriveHostRootCheck re-runs the same list on the real path later.

func ProxySidecarEnvKnobNames added in v0.7.4

func ProxySidecarEnvKnobNames() []string

ProxySidecarEnvKnobNames returns a COPY of the knob-name list: exported for the envdoc guard, cloned so no caller can append to or reorder the list that decides what reaches every proxy sidecar.

func ProxySidecarEnvKnobs added in v0.7.2

func ProxySidecarEnvKnobs() [][2]string

ProxySidecarEnvKnobs returns the operator knobs the wardyn-proxy sidecar reads from ITS OWN environment — as name/value pairs, only for the ones wardynd has set — for a substrate to copy into the sidecar it creates.

The sidecar does not inherit wardynd's environment on any substrate, so a knob a substrate forgets is UNREACHABLE rather than merely "off" — hence one list here, beside BuildProxyConfig, for every substrate. Values pass through verbatim; each knob's own reader does the parsing and fail-closed decision.

func RecorderArgv added in v0.5.0

func RecorderArgv(castDir, outDir, uploadURL string, runID uuid.UUID, agentArgv []string) []string

RecorderArgv builds the argv an agent sandbox runs when session recording is enabled. It delegates to wardyn-rec (a thin binary in the agent image) so the GPL recorder (asciinema) is exec'd as a subprocess, never linked into Wardyn.

Layout:

wardyn-rec -cast-dir <dir> [-out-dir <mount>] [-upload-url <proxy route>] -run <id> -- <agent argv...>

uploadURL is the DEFAULT delivery path (the proxy's brokered recording route), letting the control plane MASK secrets before persisting the cast. outDir is the shared-mount fallback: no cross-run isolation, and its cast is UNMASKED since wardyn-rec holds no secret values. The two are MUTUALLY EXCLUSIVE — when uploadURL is set, -out-dir is dropped so an unmasked cast can never reach the API-served replay store.

Only called when recording is enabled.

func RefuseUserDriveBind added in v0.7.0

func RefuseUserDriveBind(drive *types.DriveMount, source, real, reason string, cause error) error

RefuseUserDriveBind is the ONE place a DRIVER-side refusal about a share's bind source becomes an error, and it splits the audience in two (same shape as k8s.refuseForbiddenDriveClaim).

The OPERATOR gets the paths in a log line (source, resolved real path, and the underlying check's sentence). The MEMBER gets only drive, directory, and `reason` — `reason` carries no path, since it becomes the run's failure_hint verbatim and is read by whoever launched the run.

Field NAMES are kept (host_root, source) — a field name is not a value.

func UserDriveHomeWithinItsRoot added in v0.7.0

func UserDriveHomeWithinItsRoot(drive *types.DriveMount, real string) error

UserDriveHomeWithinItsRoot asserts that real — the symlink-resolved directory a host_path drive is about to be bound from — is a STRICT SUBDIRECTORY of hostRoot, THIS drive's own root, resolved the same way.

SECURITY: the deployment ceiling (UserDriveMountSourceCheck) only bounds the bind to the union of every configured root, not to which root THIS drive was authored against — so on a deployment with two share drives, a home replaced host-side by a link into a DIFFERENT drive's root would pass every other check and bind the wrong drive's tree. The per-drive root closes that gap.

An empty hostRoot is a REFUSAL, not a skip: HostRoot is set for every host_path drive, so "" means the mount was built by something that doesn't know this field.

STRICT: a source resolved TO the root is refused here too, since a check stated only once is a check a refactor can drop. The returned error names the drive and directory only; host_root, real path, and resolve error go to slog for the operator.

func UserDriveHostRootCheck added in v0.7.0

func UserDriveHostRootCheck(roots []string) func(hostRoot string) error

UserDriveHostRootCheck returns the ceiling predicate the API write boundary composes with types.ValidateUserDrive: nil when hostRoot is inside one of roots, an error naming the ceiling otherwise.

Empty roots refuses EVERY host_path drive by design — a deployment that has not opted in cannot acquire one via an admin form.

Resolves symlinks and matches on the REAL path, failing closed on any resolve error — stricter than ValidateMountSource, since a drive's host root asserts a share is mounted HERE, not merely a path being looked at.

func UserDriveMountSourceCheck added in v0.7.0

func UserDriveMountSourceCheck(roots []string) func(source string) (string, error)

UserDriveMountSourceCheck is the DRIVER-side check over the path a host_path drive is actually bound from (this principal's subdirectory), as opposed to the authored host_root UserDriveHostRootCheck decides about.

Adds one rule beyond the shared check: the resolved source must be a STRICT subdirectory of a root, never the root itself — a source that resolved TO the root would bind the whole share into one sandbox, which only a bug (a home name resolving to "." or a symlink back to its parent) could produce.

Returns the resolved real path (not just error) so the one caller that needs it — the Docker driver — doesn't re-resolve the same path. On error the path is always "".

func ValidateAuthoredTarget added in v0.7.0

func ValidateAuthoredTarget(tgt string) error

ValidateAuthoredTarget is ValidateTarget PLUS the reserved-target rule: what every AUTHORED in-container target goes through (a policy's workspace_mounts/workspace_repos entry, a workspace source's target).

The split names a boundary, not a stricter mood: ValidateTarget answers "is this a legal place in the sandbox", which the drive's own mount must still pass; this answers "may a HUMAN name this place", which the drive's mount is exactly the exception to. Folding the reserved rule into ValidateTarget would make the drive fail its own validation.

func ValidateManagedFiles added in v0.8.0

func ValidateManagedFiles(files []ManagedFile) error

ValidateManagedFiles reports whether files can be delivered on EVERY substrate. Drivers call it before they create anything, so an impossible request is refused rather than half-applied. On Kubernetes, ManagedFileDir becomes the mount point of a read-only Secret volume, not a subPath mount (the apiserver forbids that on the agent's ephemeral container).

func ValidateMount

func ValidateMount(m Mount) error

ValidateMount enforces the host bind-mount deny-list: nil for an allowed mount, a descriptive error otherwise, single source of truth shared by the policy validator and the docker driver.

Rules (all fail closed):

  1. Source must be a non-empty, absolute, already-cleaned path (path.Clean is idempotent on it) — no "..", no relative path.
  2. Source must not be "/" and must not equal or be nested under any deniedSourcePrefixes entry.
  3. Source must not reference a Docker socket (any path whose base is docker.sock), wherever it lives.
  4. Target must be a non-empty, absolute, cleaned path under one of allowedTargetPrefixes.

func ValidateMountSource added in v0.4.4

func ValidateMountSource(src string) error

ValidateMountSource enforces rules 1-3 above — the SOURCE half of ValidateMount, extracted so a surface that only vets a reusable host path can run the same deny-list without inventing a placeholder target. Source half ONLY: any surface that actually BINDS the path must call ValidateMount.

func ValidateTarget

func ValidateTarget(tgt string) error

ValidateTarget enforces the in-container mount/clone target shape: a non-empty, absolute, cleaned path under an allowedTargetPrefixes entry. Target half of ValidateMount, extracted so a surface placing something at an in-container path WITHOUT a host bind-mount source (e.g. a git-cloned WorkspaceRepo target) enforces the same invariant without duplicating the list.

func ValidateUserMountSource added in v0.8.0

func ValidateUserMountSource(src string, roots []string) error

ValidateUserMountSource is the BIND-time gate: the source half only, run against the roots already resolved for the run (SandboxSpec.UserMountRoots) as the last thing before the driver hands the bind to the container runtime. Writability is not re-derived here — it was decided at authoring time and rides on the mount itself, so a path swap cannot change it.

Types

type AttachOptions

type AttachOptions struct {
	Cols uint16
	Rows uint16
}

AttachOptions configures an interactive attach; zero Cols/Rows let the driver pick a sane default (e.g. 80x24).

type Capabilities

type Capabilities struct {
	Driver                    string                            `json:"driver"`                                // e.g. "docker"
	ConfinementClasses        []types.ConfinementClass          `json:"confinement_classes"`                   // strongest last
	Resolved                  map[types.ConfinementClass]string `json:"confinement_substrates,omitempty"`      // substrate label per class, for /healthz
	StructuralEgress          bool                              `json:"structural_egress"`                     // L0: no default route, sole egress = wardyn-proxy
	NetworkPolicy             bool                              `json:"network_policy"`                        // L1 (nftables / NetworkPolicy default-deny)
	NetworkPolicyAcknowledged bool                              `json:"network_policy_acknowledged,omitempty"` // operator accepted a canary failure as expected rather than proven
	SessionRecording          bool                              `json:"session_recording"`                     // wardyn-rec sidecar support
	// UserDrives and ManagedFiles are fail-closed: false means the control
	// plane refuses rather than admit a request the driver would reject.
	UserDrives   bool `json:"user_drives,omitempty"`
	ManagedFiles bool `json:"managed_files,omitempty"` // can deliver SandboxSpec.ManagedFiles (root-owned, AGENT CANNOT MODIFY)
	// EphemeralDiskEnforcement names what binds Resources.DiskMiB:
	// `filesystem` (docker quota), `eviction` (k8s, never refuses the write),
	// or `none`/empty. Aggregated as the WEAKEST across substrates.
	EphemeralDiskEnforcement types.StorageEnforcement `json:"ephemeral_disk_enforcement,omitempty"`
	// Freeze reports, PER CLASS, pause/resume support; runsc/Kata pause is
	// UNVERIFIED and never claimed here. Callers MUST check per the run's class.
	Freeze map[types.ConfinementClass]bool `json:"freeze,omitempty"`
}

Capabilities declares what a driver (on this host/cluster) can actually enforce. The control plane refuses to schedule a run whose policy demands more than the driver declares. Never claim a control that is not structurally enforced.

type DriveProbe added in v0.8.0

type DriveProbe struct {
	Result DriveProbeResult
	Detail string // operator-facing context; never shown to the member
}

DriveProbe is a DriveProber's answer for one resolved mount.

type DriveProbeResult added in v0.8.0

type DriveProbeResult string

DriveProbeResult is the closed set of answers a DriveProber gives about one resolved drive mount. Three states, not a bool, because "I checked and it is fine" and "I could not tell" are different claims with different remedies — see DriveProbeUnknown.

const (
	DriveProbeReadable   DriveProbeResult = "readable"   // probe ran AS THE AGENT'S OWN UID (never root) and could read the mount
	DriveProbeUnreadable DriveProbeResult = "unreadable" // agent uid could NOT read it — the failure a daemon-side root os.Stat cannot see
	// DriveProbeUnknown: the probe could not be run to a conclusion (e.g. k8s
	// can only Get the claim's phase). A caller MUST NOT treat Unknown as
	// DriveProbeReadable.
	DriveProbeUnknown DriveProbeResult = "unknown"
)

type DriveProber added in v0.8.0

type DriveProber interface {
	// ProbeDrive answers whether mount would be readable by the sandbox's
	// uid, bounded by ctx. Called BEFORE a sandbox exists and MUST honour
	// ctx's deadline.
	ProbeDrive(ctx context.Context, mount types.DriveMount) (DriveProbe, error)
}

DriveProber is an OPTIONAL Runner capability, modelled on ImageChecker: a substrate that can ask whether the SANDBOX'S OWN USER — not the daemon's own root process — can actually read a resolved user-drive mount (a daemon-side os.Stat always runs as root and misses this).

type DriveReclaimOutcome added in v0.8.0

type DriveReclaimOutcome string

DriveReclaimOutcome is the closed set of answers a DriveReclaimer gives for one object it was asked to destroy. The second is not an error: the object being gone already is the same END STATE the caller asked for. Telling the two apart matters only to the audit row.

const (
	DriveReclaimDeleted DriveReclaimOutcome = "deleted" // this call issued the delete and the substrate accepted it
	// DriveReclaimAlreadyAbsent: no object answered to that name. Not an
	// error, but never reported as "deleted" — an operator must be able to
	// trust that row.
	DriveReclaimAlreadyAbsent DriveReclaimOutcome = "already_absent"
)

type DriveReclaimer added in v0.8.0

type DriveReclaimer interface {
	// ReclaimDrive destroys the storage object mount names, bounded by ctx.
	// MUST refuse rather than destroy when the object is not this drive's
	// (ErrDriveNotReclaimable) or is still held by a sandbox (ErrDriveInUse),
	// and MUST answer DriveReclaimAlreadyAbsent, not an error, when nothing
	// answers to the name.
	ReclaimDrive(ctx context.Context, mount types.DriveMount) (DriveReclaimOutcome, error)
}

DriveReclaimer is an OPTIONAL Runner capability, modelled on ImageRemover: a substrate that can DESTROY the per-person storage object a user drive allocated. The one irreversible verb in this file; a drive OUTLIVES every run that mounts it, so no automatic reclaim path may exist. On Kubernetes the chart's Role gains `delete` only under `drives.reclaim.enabled`.

type ExecSession added in v0.5.0

type ExecSession struct {
	// Stdin writes to the exec's stdin. Close HALF-closes the write side only
	// — implementations MUST NOT tear down Stdout/Stderr when Stdin.Close is
	// called.
	Stdin io.WriteCloser
	// Stdout carries standard output; with ExecSpec.TTY=true it ALSO carries
	// stderr — see Stderr.
	Stdout io.Reader
	// Stderr carries standard error as a SEPARATE stream when TTY is false.
	// STREAMING CONTRACT: Stdout and Stderr are UNBUFFERED io.Pipes fed by one
	// demux goroutine off ONE connection — an undrained stderr byte blocks the
	// demux, Stdout, AND Wait. Callers MUST drain Stderr concurrently.
	Stderr io.Reader
	Resize func(cols, rows uint16) error // no-op returning nil with no TTY
	Wait   func() (int, error)
	Close  func() error // tears down ONLY this exec stream — never the sandbox, agent, or any sidecar
}

ExecSession is a live, bidirectional stream into ONE exec started by Runner.ExecStream. A PLAIN STRUCT of streams/closures, deliberately NEVER an interface: a fake can construct a partial ExecSession by filling in fields directly. Resize, Wait, and Close take NO context: an implementation binds them to whatever context it created the exec with.

type ExecSpec added in v0.5.0

type ExecSpec struct {
	Argv []string
	Env  []string // additional exec-scoped environment, on top of the sandbox's own
	// TTY requests a pseudo-terminal, which MERGES stdout and stderr onto
	// ExecSession.Stdout — a binary protocol on stdout (SFTP, socat) would be
	// corrupted by interleaved stderr without one.
	TTY        bool
	Cols, Rows uint16 // initial PTY window size (TTY only); zero => implementation default
}

ExecSpec describes one exec launched via Runner.ExecStream: the argv to run, exec-scoped environment, and whether it runs under a PTY.

type Freezer added in v0.8.0

type Freezer interface {
	FreezeSandbox(ctx context.Context, ref string) error
	ThawSandbox(ctx context.Context, ref string) error
}

Freezer is an OPTIONAL Runner capability: pause and resume the AGENT container in place. FreezeSandbox pauses the agent process — memory, disk and any established TCP connection keep their state — and the daemon refuses a new exec until ThawSandbox resumes it. Both idempotent.

The PROXY SIDECAR IS NEVER FROZEN — only the ref this is called with.

A substrate with no pause primitive returns ErrFreezeUnsupported. Implementing this is NOT itself proof the pause is safe for every runtime the substrate carries: runsc/Kata pause is UNVERIFIED (Capabilities.Freeze[class] is false) though Docker implements Freezer. Callers MUST check that field.

type ImageChecker added in v0.5.0

type ImageChecker interface {
	ImagePresent(ctx context.Context, ref string) (bool, error) // present in the substrate's local image store right now
}

ImageChecker is an OPTIONAL Runner capability: a substrate whose local image cache can go stale (the docker driver) implements this so a stale cache can be detected and fallen through to a rebuild. A substrate that pulls fresh per launch (k8s) doesn't implement this — callers treat "doesn't implement" as unknown, so trust the cache (fail-open).

type ImageRemover added in v0.5.0

type ImageRemover interface {
	ImageRemove(ctx context.Context, ref string) error // a ref already absent is NOT an error
}

ImageRemover is an OPTIONAL Runner capability: a substrate with a local image store (docker) implements this so a superseded workspace-built image can be reclaimed instead of leaking forever. Best-effort, same posture as ImageChecker; a substrate that pulls fresh per launch (k8s) doesn't implement this.

type InjectionGrant

type InjectionGrant struct {
	GrantID uuid.UUID            `json:"grant_id"`
	Rule    egress.InjectionRule `json:"rule"`
}

InjectionGrant pairs an api_key grant with its proxy-side injection rule (never the secret value).

type ManagedFile added in v0.8.0

type ManagedFile struct {
	// Path is the absolute in-sandbox path, already cleaned. See
	// ValidateManagedFiles for the shape both substrates can honour.
	Path string
	// Mode is the file's permission bits. Zero means DefaultManagedFileMode.
	// Group- and other-writable modes are refused: the file would be writable
	// by the agent's own supplementary groups, which defeats the field.
	Mode fs.FileMode
	// Content is the exact file body. Delivered byte for byte; no trailing
	// newline is added.
	Content []byte
}

ManagedFile is one operator-authored file placed inside the sandbox that the AGENT CANNOT MODIFY. SECURITY: a driver delivers it root-owned, in a directory the agent can neither write nor replace, before the agent's main process can run — never materialised from inside the image (agent runs as uid 1000) or via a post-start root exec (races the main process). On Kubernetes it travels in the per-run Secret like SecretEnv.

func (ManagedFile) FileMode added in v0.8.0

func (f ManagedFile) FileMode() fs.FileMode

FileMode is Mode with the zero value resolved to DefaultManagedFileMode. Drivers use THIS, never the field, so "unset" means the same thing on both.

type Mount

type Mount struct {
	Source   string `json:"source"`
	Target   string `json:"target"`
	ReadOnly bool   `json:"read_only"`
	// MemberAuthored marks a bind whose SOURCE a MEMBER chose. ONLY these are
	// re-checked against SandboxSpec.UserMountRoots at bind time.
	MemberAuthored bool `json:"member_authored,omitempty"`
	// DriveAuthored marks the ONE bind a driver synthesizes from
	// SandboxSpec.Drive. A LABEL, NEVER A GATE: WARDYN_USER_DRIVE_HOST_ROOTS
	// runs on EVERY host_path drive regardless of this flag.
	DriveAuthored bool `json:"drive_authored,omitempty"`
}

Mount is one operator/policy-controlled host bind mount into the sandbox. Source is a host path; Target is the in-container path. ReadOnly defaults to true. See SandboxSpec.Mounts for the security model.

type ProxyConfig

type ProxyConfig struct {
	RunToken          string // authenticates the sidecars to the control plane (NOT usable outside the platform)
	ControlPlaneURL   string // where sidecars stream decisions/recordings
	ControlPlaneCAPEM string // wardynd's internal CA; empty with a loopback http URL only
	// Policy is the run's egress policy, handed verbatim to the wardyn-proxy
	// sidecar. Drivers MUST deliver it at launch: a proxy without a policy
	// fails closed and the sandbox has no working egress at all.
	Policy    types.RunPolicySpec
	Injection []InjectionGrant // auto-mintable api_key grants the proxy resolves at startup (values live only in proxy memory)
	// MITMCACertPEM / MITMCAKeyPEM are the OPTIONAL per-run TLS-MITM CA. The
	// private key reaches ONLY the proxy, never the sandbox. Empty => opaque
	// CONNECT passthrough.
	MITMCACertPEM string
	MITMCAKeyPEM  string
	MITMHosts     []string                  // OPERATOR-CONFIGURED corp hosts the proxy may TLS-MITM; NEVER attacker-controlled
	MITMLLM       bool                      // TLS-MITM of the built-in LLM hosts intended for this run; see proxy.Config.MITMLLM
	GitGrants     map[string]uuid.UUID      // git-broker per-repo allowlist; token stays proxy-side
	PATGrants     map[string]proxy.PATGrant // git_pat broker's per-HOST allowlist; PAT minted proxy-side, never enters the sandbox
	ADOGrant      *proxy.ADOGrantConfig     // per-person Azure DevOps grant for the proxy's REST gate
	// UpstreamProxyURL is the OPTIONAL corporate parent proxy the sidecar
	// chains egress through; control-plane calls bypass it. Empty => direct dial.
	UpstreamProxyURL string
	// TrustedCAPEM is the operator's corporate CA bundle, forwarded so the
	// sidecar's egress TLS additionally trusts it. Empty => system roots only.
	TrustedCAPEM  string
	InternalHosts []types.InternalHost // operator-declared internal hostnames eligible for the proxy's private-IP-guard lift
	// UpstreamProxyNoProxy: hosts/CIDRs whose dials skip the corporate
	// upstream proxy — still faces the private-IP guard and the run's policy.
	UpstreamProxyNoProxy []string
	LLMUpstreams         map[string]string // public vendor host -> operator model gateway base URL
	LLMUnavailableDetail string            // reason for the brokered-LLM 404 when no credential backs it
	Unattended           bool              // a run nobody is driving: a held push is refused instead
}

type ProxyReviver added in v0.8.0

type ProxyReviver interface {
	CanReplaceProxy(ctx context.Context, ref string) error
	ReplaceProxy(ctx context.Context, ref string, cfgJSON []byte) error
	// EnsureProxyImage pulls the proxy sidecar image if not already present.
	// Called BEFORE the revive claim so a slow pull doesn't widen the window
	// a watcher sweep must leave the run alone in.
	EnsureProxyImage(ctx context.Context) error
}

ProxyReviver is an OPTIONAL Runner capability: replace a sandbox's proxy sidecar, running or stopped, with a new one while the agent keeps running. The control plane reads the run's stored config (#1176), rewrites its token and denies, and hands it to ReplaceProxy.

CanReplaceProxy answers ErrReviveUnsupported when ref's substrate cannot replace a proxy in place. ReplaceProxy removes the old proxy, then starts the new one at the address the agent's hosts entry pins. An error wrapping ErrProxyReplaceFailed means the old proxy is, or may be, gone; any other error left it untouched.

type ProxyStopper added in v0.8.0

type ProxyStopper interface {
	StopProxy(ctx context.Context, ref string) error
}

ProxyStopper is an OPTIONAL Runner capability: stop a sandbox's proxy sidecar and leave its agent running (a run lost to a control-plane outage). The stopped proxy is removed, not kept: the config lives in the run's stored config (#1176). Idempotent. A graceful stop that fails escalates to a kill; a proxy that survives both is an error, with the agent stopped too so no work runs while its egress is unconfirmed (#1060).

type Resources

type Resources struct {
	CPUMillis int64
	MemoryMiB int64
	PidsLimit int64 // fork-bomb guard; zero => driver default
	// DiskMiB caps writable storage; WHAT BINDS IT DIFFERS BY SUBSTRATE (see
	// Capabilities.EphemeralDiskEnforcement): docker quotas only when
	// supported, else uncapped; k8s enforces via kubelet EVICTION.
	DiskMiB int64
	// DiskMiBFilled says DiskMiB was FILLED IN from the org default rather
	// than policy-authored: the desktop tier fails closed only for a
	// POLICY-AUTHORED value; a FILLED default degrades to uncapped instead.
	DiskMiBFilled bool
}

Resources are the hard sandbox caps the driver applies as cgroup/storage limits. A ZERO field means "use the driver's conservative platform default".

type Runner

type Runner interface {
	Name() string
	Capabilities(ctx context.Context) (Capabilities, error)
	CreateSandbox(ctx context.Context, spec SandboxSpec) (Sandbox, error) // L0 confinement: no default route, egress only via the proxy
	// Exec starts the agent process; returns once started, not finished.
	// Persist agentExecID for restart-safe liveness via AgentStatus.
	Exec(ctx context.Context, ref string, argv []string) (agentExecID string, err error)
	Wait(ctx context.Context, ref string) (exitCode int, err error) // blocks until Exec's process exits; valid only after Exec on the same ref
	// Attach opens a NEW interactive shell, SEPARATE from the agent process.
	// Bounded by the SAME L0 egress envelope (invariant 3); callers MUST
	// record the human principal (invariant 4).
	Attach(ctx context.Context, ref string, opts AttachOptions) (Session, error)
	// ExecStream launches spec.Argv as a fresh, streamable exec. UNLIKE Exec,
	// MUST be repeatable against the SAME ref. SECURITY: opens no new network
	// path (invariant 3); callers MUST record the human principal (invariant 4).
	ExecStream(ctx context.Context, ref string, spec ExecSpec) (*ExecSession, error)
	Status(ctx context.Context, ref string) (Status, error)
	AgentStatus(ctx context.Context, ref, agentExecID string) (Status, error) // restart-safe; falls back to Status when agentExecID is ""
	StopSandbox(ctx context.Context, ref string) error                        // graceful path (lifecycle auto-stop)
	// KillSandbox is the kill-switch path: immediate teardown. The control
	// plane cascades identity + credential revocation around this call.
	KillSandbox(ctx context.Context, ref string) error
}

Runner is the lifecycle contract. Implementations must be safe for concurrent use. Every method must be idempotent where the verb implies it (Stop/Kill on a gone sandbox return nil).

type Sandbox

type Sandbox struct {
	Ref           string // container ID / pod name
	Driver        string
	EnforcedClass types.ConfinementClass // what the driver actually applied (>= requested or error)
}

Sandbox is a handle to a created sandbox.

type SandboxEnder added in v0.8.0

type SandboxEnder interface {
	EndSandbox(ctx context.Context, ref string) error
}

SandboxEnder is an OPTIONAL Runner capability: stop a sandbox and KEEP it. EndSandbox stops the agent without removing it, so files survive, and stops the proxy sidecar so nothing the agent could restart has a network path. Idempotent. A substrate that cannot keep a stopped sandbox (Kubernetes: stopping a pod deletes it) returns ErrEndUnsupported through its router.

type SandboxSpec

type SandboxSpec struct {
	RunID            uuid.UUID
	Image            string // resolved agent/workspace OCI image
	ConfinementClass types.ConfinementClass
	// Env is non-secret environment. Credential material NEVER passes through
	// here; it rides SecretEnv.
	Env map[string]string
	// SecretEnv is CREDENTIAL-BEARING environment, DISJOINT from Env. The k8s
	// driver MUST route these through the per-run Secret via
	// ValueFrom.SecretKeyRef (an inline EnvVar.Value is readable by anyone
	// holding pods/get); docker passes them as container env, readable only
	// through the daemon socket — the same trust boundary as the run token.
	SecretEnv map[string]string
	// ProxyConfig wires the L0 path: the sandbox's only egress is the
	// wardyn-proxy sidecar identified here.
	ProxyConfig ProxyConfig
	Resources   Resources         // hard caps (cgroups / ResourceQuota)
	Labels      map[string]string // attached for attestation selectors and audit
	// Mounts are operator/policy-controlled host bind mounts. SECURITY:
	// POLICY-controlled, NEVER attacker-controlled. Drivers bind-mount these
	// AND enforce a deny-list defense-in-depth. Default ReadOnly.
	Mounts []Mount
	// UserMountRoots, non-nil, marks a run whose mounts were MEMBER-authored
	// and carries the roots those mounts must resolve inside, re-checked at
	// BIND time. Nil elsewhere — never narrows an operator mount.
	UserMountRoots []string
	// Drive is the acting principal's USER DRIVE, already resolved by the
	// control plane — nil if none was asked for. NOT a Mount: the request
	// carried a flag rather than a path.
	Drive *types.DriveMount
	// ManagedFiles are operator-authored files the AGENT CANNOT MODIFY —
	// root-owned, unwritable, present BEFORE the main process runs.
	// POLICY-controlled. A driver that doesn't advertise
	// Capabilities.ManagedFiles MUST refuse rather than start without them.
	ManagedFiles []ManagedFile
	// OnWaiting, non-nil, reports WHY the sandbox isn't up yet, each time the
	// reason CHANGES. Called SYNCHRONOUSLY on CreateSandbox's own goroutine
	// (must not block). Call NotifyWaiting, never the field.
	OnWaiting func(detail string) `json:"-"`
	// Interactive marks a run that comes up idle for `wardyn run attach`; a
	// driver prepares the workspace on the idle main process for it.
	Interactive bool
}

SandboxSpec is everything a driver needs to create one governed sandbox.

func (SandboxSpec) NotifyWaiting added in v0.7.6

func (s SandboxSpec) NotifyWaiting(detail string)

NotifyWaiting delivers one OnWaiting report, or does nothing when this spec carries no callback. Drivers call THIS, never the field.

type SandboxStarter added in v0.8.0

type SandboxStarter interface {
	StartSandbox(ctx context.Context, ref string) error
}

SandboxStarter is an OPTIONAL Runner capability: start a kept sandbox's stopped agent again (revive after a reboot). Files survive; the main process re-runs from the start. Never starts the proxy itself: the caller replaces that first (ProxyReviver). Idempotent. Kubernetes (a stopped pod is gone) returns ErrReviveUnsupported.

type Session

type Session interface {
	Read(p []byte) (int, error)                          // io.EOF when the shell exits or closes
	Write(p []byte) (int, error)                         // sends keystrokes
	Resize(ctx context.Context, cols, rows uint16) error // new PTY window size
	// Close tears down ONLY the interactive exec stream — not the sandbox,
	// the agent process, or any sidecar.
	Close() error
}

Session is a live, bidirectional interactive PTY stream into a RUNNING sandbox, opened by Runner.Attach.

SECURITY (invariant 3): runs INSIDE the existing sandbox, bounded by the same L0 confinement envelope as the agent; opens NO new network path. Attach grants a terminal, not a new egress route.

Not safe for concurrent Read/Write from multiple goroutines on the same direction; Read and Write each in their own goroutine is supported.

type Status

type Status struct {
	State    types.RunState
	ExitCode *int
	Message  string
}

Status reports observed sandbox state.

type UserMountPolicy added in v0.8.0

type UserMountPolicy struct {
	// Roots is the shared allowlist (WARDYN_USER_WORKSPACE_ROOTS): absolute,
	// cleaned host prefixes any member's local_dir source must resolve into.
	Roots []string
	// RootsByPrincipal (WARDYN_USER_WORKSPACE_ROOTS_MAP) is the per-member
	// override. A principal with an entry uses ONLY that entry — per-member
	// REPLACES shared, since it exists to be the more restrictive control,
	// and a union would make adding a row widen rather than narrow. Keys are
	// lowercased principals (OIDC sub or email).
	RootsByPrincipal map[string][]string
	// WritableRoots (WARDYN_USER_WRITABLE_ROOTS) is where a member may mark
	// their own mount writable. Empty = nowhere.
	WritableRoots []string
	// WritableDeny (WARDYN_USER_WRITABLE_DENY) carves holes in WritableRoots.
	// Deny WINS over allow, and is checked first.
	WritableDeny []string
}

UserMountPolicy is the operator/MDM-set posture for MEMBER-authored local_dir binds. The zero value means "no member local_dir mounts at all", the fail-closed default.

Every field comes from operator/MDM-set ENV (cmd/wardynd), never SiteConfig — site config is a full-replace row a single bad PUT can blank, and this is a security ceiling.

func ParseUserMountPolicy added in v0.8.0

func ParseUserMountPolicy(roots, rootsMap, writable, writableDeny string) (p UserMountPolicy, warnings []string, err error)

ParseUserMountPolicy builds the policy from its four raw operator/MDM env values (see UserMountPolicy for what each one means). roots, writable and writableDeny are comma-separated absolute paths; rootsMap is JSON {"<principal>": ["/abs/root", ...]}.

FAIL CLOSED on a malformed value — a typo'd root that silently matched nothing (or, worse, prefix-matched something else) is exactly the config bug this ceiling cannot afford — so boot stops rather than starting with a half-understood allowlist.

warnings are the posture notices the caller logs at boot: a root of "/" or of $HOME is permitted but leaves the dotfile-deny residual unbounded, so it must never be silent.

func (UserMountPolicy) Configured added in v0.8.0

func (p UserMountPolicy) Configured() bool

Configured reports whether any member root is set at all — i.e. whether member local_dir onboarding is available on this deployment.

func (UserMountPolicy) RootsFor added in v0.8.0

func (p UserMountPolicy) RootsFor(principal string) []string

RootsFor returns the roots that bound one member's mounts: their own map entry when they have one (REPLACING the shared list), otherwise the shared list. An empty result means this member may not mount a host dir at all.

func (UserMountPolicy) ValidateUserMount added in v0.8.0

func (p UserMountPolicy) ValidateUserMount(principal, src string, writable bool) error

ValidateUserMount is the AUTHORING-time gate (workspace onboarding and run-create): the full source check for principal, plus the writable allowlist when the source asks to be mounted read-write. A nil error means this member may bind this source with this writability.

Directories

Path Synopsis
Package orchestrator is the build-tag-free runner.Runner the control plane talks to.
Package orchestrator is the build-tag-free runner.Runner the control plane talks to.
Package substrate defines the confinement-substrate sub-interface: the seam beneath runner.Runner that lets a non-OCI microVM VMM back a Confinement Class alongside the OCI/Docker substrate.
Package substrate defines the confinement-substrate sub-interface: the seam beneath runner.Runner that lets a non-OCI microVM VMM back a Confinement Class alongside the OCI/Docker substrate.

Jump to

Keyboard shortcuts

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