runner

package
v0.7.12 Latest Latest
Warning

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

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

Documentation

Overview

Package runner defines the target-agnostic sandbox lifecycle contract. The control plane ONLY talks to this interface — it must contain zero Docker- or Kubernetes-specific code. Drivers live in subpackages (currently runner/docker) 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).

Why a drive needs one at all, when the drive row is admin-authored: a host_path drive names a tree the OPERATOR mounted host-side, and Wardyn then binds a subdirectory of it into OTHER PEOPLE's sandboxes. That is the same shape member_mount.go's own doc argues about — a path typed into the product deciding what a sandbox can reach — and it takes the same answer: the security ceiling is operator/MDM-set in the environment, never a row in a table the product itself writes. A console compromise then widens nothing, because the widening it would need is not in the database.

UNSET FAILS CLOSED. With no roots configured, NO host_path drive may be authored at all — byte-for-byte the WARDYN_MEMBER_WORKSPACE_ROOTS posture, and for the same reason: the safe default for "the operator has not said where" is "nowhere", not "anywhere".

Index

Constants

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. They exist 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 (the basic multi-tenant safety controls — see types.ResourceLimits). A policy value always overrides.

View Source
const (
	// DriveSourceRefused is every way the composed source check can refuse the
	// path this deployment resolved for the drive: the deployment ceiling, the
	// host bind deny-list, the credential-dotfile list, a source that resolved
	// to a configured root, an unresolvable path. WHICH of them is a fact about
	// the OPERATOR's filesystem and their configuration, so it goes to the log
	// and the member is told who can act on it.
	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 diagnosis needs no
	// path to be complete, and 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 = `` /* 978-byte string literal not displayed */

AgentIdleScript is the agent sandbox's main (idle) process for NON- interactive runs: it installs the per-run TLS-MITM CA (when delivered) and then idles while the task Exec does the real work. (Interactive runs use `agent-run --idle` as their main process instead — each substrate's CreateSandbox wires this — which performs this same CA install plus workspace prep; the human then drives claude in the attach shell.) The CA install is REQUIRED either way: without it, NODE_EXTRA_CA_CERTS points at a CA file that was never written, so claude cannot trust the proxy's TLS termination of api.anthropic.com (breaking subscription proxy-side injection). It writes the EXACT paths internal/api pins (/tmp/wardyn — any- uid-writable, so it works regardless of the image's USER/HOME; this Cmd may run as root while agent-run later re-runs as the image user, hence the sticky-bit dir and the ||true rewrites: within one run the content is identical, so a failed rewrite over a correct file is harmless). It also assembles the COMBINED bundle (system roots + per-run CA) that SSL_CERT_FILE/REQUESTS_CA_BUNDLE/CURL_CA_BUNDLE point at — those vars REPLACE the client trust store, so the bare CA there would break non-MITM'd CONNECT-tunneled hosts. Keep in lockstep with install_mitm_ca in deploy/images/common/agent-run-lib.sh. No-op when the run did not opt into TLS-MITM (WARDYN_MITM_CA_PEM unset).

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

DriveTarget is the RESERVED in-container path a user drive mounts at. It sits under /home/agent, so it is already an allowed target prefix — reserving it is what stops an authored workspace mount, workspace repo or workspace source from landing on the same path and quietly shadowing (or being shadowed by) the member's own persistent storage.

Reserved rather than merely first-come: the two are authored by different people at different times (an admin writes a policy, an admin allocates a drive, a member ticks a checkbox at run time), so a collision would surface as one of them silently disappearing inside a running sandbox rather than as a refusal anybody could act on.

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 ErrExecNeverStarted = errors.New("runner: agent exec never started")

ErrExecNeverStarted is the sentinel a Runner's Wait returns when it can prove the agent exec will NEVER reach a terminal state on its own — e.g. the k8s driver's ephemeral "wardyn-agent" container stuck Waiting on a hard-failure Reason (ImagePullBackOff, CreateContainerConfigError, ...) that will not resolve without intervention. Distinct from every other Wait error (a transient probe error, ctx cancellation): those mean "the agent might still be running, we just couldn't observe it right now" and the caller retries/hands off to the reconciler; this one means "the task never ran and never will", so the caller (startCompletionWatcher, runs_lifecycle.go) must fail the run immediately rather than treat it as a transient hiccup to hand off.

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

ErrExecStreamUnsupported is the sentinel a Runner/Substrate returns from ExecStream when it has no implementation for the primitive (a stub, a fake, or a driver that has not wired it up yet). Callers — notably the conformance suite's testExecStream — use errors.Is against this exact sentinel to skip cleanly on "not implemented" while still FAILING on any other error, so an implemented-but-broken ExecStream cannot skip green by returning some other error.

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, and only a person (a fixed image reference, a registry credential, a working entrypoint) does.

It lives HERE, in the tagless runner package, rather than in the k8s driver that reads it, because three places have to agree on the same six strings and two of them cannot see a `k8s`-tagged symbol: the driver's own fast-fail poll (k8s/canary.go's terminalWaitingReasons, which is this map), the control plane's read projection (a terminal reason is the one startup detail that survives a FAILED run, api/runs_status_detail.go), and the console's mirror (ui/.../run-status-detail.ts's TERMINAL_STATUS_REASONS, which a vitest case pins against this list by hand). Not exhaustive — every other reason is covered by the wait's own timeout — this is the fast path for the cases that are decidable the moment the kubelet says them.

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 as WARDYN_PROXY_CONFIG_JSON — the proxy fails closed without it (no policy => no working egress). Pure data transform (field mapping + json.Marshal); hoisted out of the docker driver so a k8s substrate builds byte-identical sidecar config without duplicating the mapping. 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 "" when no prefix does (including for every OTHER reason ValidateMountSource refuses a path: empty, relative, uncleaned, the host root, a runtime socket).

It exists for the one surface with its own FROZEN vocabulary for this refusal — a user drive's host_root, whose editor has no "mount source" field to send an admin looking for (UserDriveHostRootCheck). Naming the prefix is what lets that surface say WHICH prefix bit without keeping a second copy of the list, and a second copy is how the deny-list and the sentence explaining it drift apart.

It answers on the same TWO paths deniedSource is run against — the lexical value and the symlink-resolved one — so a root that resolves INTO a denied tree names the prefix that actually 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 (an older control plane, an in-process caller that built the spec itself) names the directory alone — never the object, which on a share IS the operator's absolute path.

Exported because the DRIVER composes the same sentence for its own refusals (RefuseUserDriveBind, and internal/runner/docker's drive arm). One definition of "what a member may be told about a drive" is the point: two would drift, and the direction they drift in is disclosure.

Nil-safe: a refusal about a mount that carries no drive at all still has to be a sentence, and "the drive" is the least it can say.

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. Note "krun" is NOT matched by the "crun" family (different leading byte), so libkrun (crun + 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 MountCeilingOverlapWarnings added in v0.7.0

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

MountCeilingOverlapWarnings returns the boot WARNINGS earned when the two operator-set mount ceilings name overlapping trees.

THEY ARE ONE LEVEL APART BY DESIGN, and nothing enforced it. A drive's host_root is the mount point of a SHARE, and Wardyn binds only ONE PERSON's subdirectory of it into a sandbox — the whole per-person isolation model, and the reason UserDriveMountSourceCheck refuses a source that resolved TO a root. WARDYN_MEMBER_WORKSPACE_ROOTS bounds a different thing entirely: directories a MEMBER may name as a workspace and bind WHOLE, writable where WARDYN_MEMBER_WRITABLE_ROOTS allows it.

Point the two at one tree and the second undoes the first. With WARDYN_MEMBER_WORKSPACE_ROOTS=/srv/shares and a drive rooted at /srv/shares, a member onboards /srv/shares as a workspace and mounts EVERY person's home, through a surface that never consults a drive allocation at all. Both ceilings individually accept it; nothing compared them, so no boot line said so and no gate refused it.

A WARNING, NOT A REFUSAL, the same allow-and-warn posture the surrounding ceiling parsing takes (see the three-case doc above and MemberMountPolicy.bootWarnings): an operator may have deliberately opened a tree to both, and a refusal at boot would take a running deployment down on upgrade over a posture it already has. What was missing was the operator ever being told.

LEXICAL, on the values as configured, for the same reason the deny-list check above is: boot is not the place to touch a share that may not be mounted yet. Every member ceiling is compared, the shared list and each per-principal override alike — a per-member override REPLACES the shared list, so it is a ceiling in its own right and can overlap on its own.

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 the boot WARNINGS a dangerously wide root earns.

It reuses parseRootList verbatim, so a root here can no more carry a traversal segment than a member root can, and a malformed value REFUSES BOOT exactly as WARDYN_MEMBER_WORKSPACE_ROOTS does: a ceiling the operator mistyped must not silently become a ceiling that bounds a different tree.

THREE root values are permitted but WARNED about, the same allow-and-warn posture MemberMountPolicy.bootWarnings takes — and they are warned about for OPPOSITE reasons, which is why they do not share a sentence:

  • the daemon's own $HOME is far too WIDE: every dotfile tree that home holds becomes a place a drive may be authored over, and its per-person subdirectories are bound into other people's sandboxes.
  • "/" is DEAD, not wide. withinAnyRoot matches `real == root` or `real` under `root + "/"`, and for the root "/" that second form is the prefix "//", which no cleaned absolute path has — so a ceiling of "/" matches nothing at all and refuses EVERY host_path drive. It reads like "allow anywhere" and behaves like "allow nothing", so the warning has to say which.
  • A root UNDER A DENIED BIND PREFIX is dead in exactly the same way, and was the silent one (F13 H2). UserDriveHostRootCheck runs ValidateMountSource BEFORE it ever compares against the roots, so a ceiling of /dev/shm, a share mounted under /var/run, or a relocated Docker data-root under /var/lib/docker parses clean here and then refuses every drive authored inside it — the operator learning from a 422 on a form they believed was right rather than from the boot line that could have told them. Same "matches NOTHING" wording as "/", because it is the same outcome; the deny-list's own sentence is carried through so the operator reads WHICH prefix bit.

The deny-list runs LEXICALLY here, on the value as configured, and does not resolve symlinks: boot is not the place to touch a share that may not be mounted yet, and a root that resolves INTO a denied tree is still caught — by UserDriveHostRootCheck, which re-runs the same list on the real path at authoring and at bind time. This warning is about the value the operator can read back out of their own unit file.

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 (re-review R-12).

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, and only for the ones wardynd actually has set — for a substrate to copy into the sidecar it creates.

The sidecar does not inherit wardynd's environment on ANY substrate: the docker driver builds an Env slice, the k8s driver an Env array on the pod spec. So a knob a substrate forgets is not "off", it is UNREACHABLE — the operator sets it, nothing refuses it, and the control it names silently never applies on that substrate. That is how the git_pat branch-namespace switch shipped dead on Kubernetes in review, which is why the LIST lives here, beside BuildProxyConfig, rather than once per driver: one list, every substrate.

Values are passed through verbatim; each knob's own reader does the parsing and the fail-closed decision (BranchNSEnforced, PATBranchNSEnforced, the WARDYN_LLM_SCAN switch in cmd/wardyn-proxy). Host-run and custom-image proxies read their own env directly and need none of this.

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 inside the agent image) so the GPL recorder (asciinema) is exec'd as a subprocess, never linked into Wardyn (license hygiene).

Layout:

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

wardyn-rec decides at runtime whether asciinema is present; the caller does not need to know. uploadURL is the DEFAULT delivery path: the proxy's brokered recording route (PUT /wardyn/v1/recordings/{run}), which injects the run token and lets the control plane MASK secrets before persisting the cast (secret masking lives control-plane-side; the registry of secret values is never in the sandbox). outDir is the optional shared-mount fallback and carries TWO documented limitations: (1) every agent sandbox sharing the mount runs under the same identity, so it has NO cross-run isolation; and (2) it bypasses the control plane, so the cast it writes is UNMASKED — secret masking is structurally impossible here (wardyn-rec holds no secret values, by design). Use the brokered upload path where recordings are viewer-exposed.

HIGH-finding hardening: the masked upload path and the unmasked shared-mount -out-dir are MUTUALLY EXCLUSIVE. When an uploadURL is configured we drop the shared-mount -out-dir entirely so wardyn-rec can NEVER also drop an UNMASKED <runID>.cast into the API-served replay store (cross-run-writable, viewer- exposed). The shared mount is only ever used as the reduced-isolation FALLBACK when no control-plane upload path exists.

Callers should only invoke this when recording is enabled — there is no passthrough case; an unwrapped argv is simply argv itself.

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 somebody reads, and it splits the audience in two — the shape k8s.refuseForbiddenDriveClaim already uses, for the same reason and on the same evidence.

The OPERATOR gets the paths, in a log line: the source the resolver derived (on a share that IS the operator's absolute share path), what it resolved to, and the underlying check's own sentence — which names the deployment's configured roots, or the denied prefix, or the sibling home. Nothing has been taken away from them.

The MEMBER gets the drive, the directory and `reason`, and `reason` carries no path. A driver refusal becomes the run's failure_hint VERBATIM (internal/api's dispatch), read by whoever launched the run — the same reader driveShareIsBindable already refuses to hand the resolved path, applyUserDriveEnv refuses to hand the object name, and driveAuditTarget masks the audit row for. Two of the Docker driver's three host_path refusal sites handed them all of it: the deployment's configured roots on one, and on the other the absolute path of the home a sibling symlink actually landed on.

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.

─── WHY THE DEPLOYMENT CEILING IS NOT ENOUGH ──────────────────────────────

UserDriveMountSourceCheck bounds the bind to the union of every root in WARDYN_USER_DRIVE_HOST_ROOTS, which is the OPERATOR's outer bound and stays exactly that. It cannot say which of those roots this particular drive was authored against, because it is not given the drive. So on a deployment with two share drives — ceiling `/srv/a,/srv/b`, drive A rooted at /srv/a, drive B at /srv/b — a home under A replaced host-side by a link to the SAME-NAMED home under B satisfies every check the driver had: it is inside a configured root, it is not a root, it traverses no denied prefix, and its base name is still this principal's home. And it binds drive B's tree.

The per-drive root closes it, and the two bounds are kept BOTH rather than collapsed into one: the ceiling is the operator's (env/MDM-set, a console compromise cannot widen it) and the root is the row's (admin-authored, and therefore not allowed to be the outer bound). A drive must satisfy both.

─── FAIL CLOSED ON AN ABSENT ROOT ─────────────────────────────────────────

An empty hostRoot is a REFUSAL, not a skip. types.DriveMount.HostRoot is set from the resolved row for every host_path drive, so "" means the mount was built by something that does not know about this field — an older control plane, or an in-process caller that assembled a runner.SandboxSpec itself — and the one thing that must not happen then is the pre-fix behaviour silently returning. NOT the standalone runner's -spec JSON: cmd/wardyn-runner's fileSpec decodes no drive field and loadSpec never sets SandboxSpec.Drive, so a hand-written spec cannot produce a drive at all (TestLoadSpec_CannotProduceADrive pins that).

STRICT, so a source that resolved TO the root is refused here as well as by UserDriveMountSourceCheck: binding a share's root hands one member every other member's home, and a check that is only stated once is a check a refactor can drop.

─── THE REFUSAL NAMES THE DRIVE, THE LOG NAMES THE PATHS ───────────────────

Every driver refusal on this path becomes the run's failure_hint, which the run's CREATOR reads — the same reader driveShareIsBindable already refuses to hand the resolved path, applyUserDriveEnv refuses to hand the object name, and driveAuditTarget masks the audit row for. So the returned error names the DRIVE and the DIRECTORY and nothing else (DriveSubject), the shape driveVolumeAdoptable already uses when it declines to reproduce a subject digest, and the host_root, the resolved real path and the underlying resolve error go to slog, where the operator reads them. Field NAMES are kept (host_root) — a field name is not a value.

A mount that cannot even name its drive falls back to the directory alone: the fallback for "I cannot name the drive" must not be "then disclose the path".

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. It satisfies types.UserDriveHostRootCheck.

THE EMPTY-ROOTS ARM IS THE FEATURE, not a degenerate case: no roots means every host_path drive is refused, so a deployment that has not opted in cannot acquire one by an admin filling in a form.

It resolves symlinks and matches on the REAL path (withinAnyRoot, shared with the member rule), and it FAILS CLOSED on a resolve error — including "does not exist". That last one is stricter than ValidateMountSource, which falls through to a lexical check so a remote dockerd's paths still validate, and the difference is deliberate: a member's mount source is a path they are looking at, while a drive's host root is a share the operator is asserting is mounted HERE. Authoring a drive over a path that is not there yet would defer the failure to somebody else's run.

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 host_root an admin authored, which UserDriveHostRootCheck decides about.

It is that check plus ONE rule that only makes sense for a bind: the resolved source must be a STRICT SUBDIRECTORY of a root, never a root itself. An authored host_root legitimately IS a root (the ordinary shape: the ceiling names the share's mount point and so does the drive), so the equality arm cannot live inside the shared check without refusing every correct drive. But a MOUNT whose source resolved to the root would bind the whole share — everybody's home directory — into one member's sandbox, which is the single outcome the per-person subdirectory model exists to prevent. That can only arrive through a bug (a home name that resolved to "." or "", a symlink from a home back to its parent), and a bug is precisely what a last-thing-before- ContainerCreate check is for.

Composed rather than restated so the driver and the API write boundary cannot drift on what the deny-list, the unset-roots refusal, or "inside a root" mean.

IT RETURNS THE RESOLVED REAL PATH, not just nil/error, and the caller is expected to keep asserting about it. This function's whole job is symlink resolution, so handing back the answer is what stops the ONE caller that has to say more about it — the Docker driver, which knows whose home the bind is supposed to be — from resolving the same path a second time and reasoning about a value this one never saw. On an error the string is "": there is no resolved path to speak of, and a caller that ignored the error must not find a plausible-looking one in its place.

func ValidateAuthoredTarget added in v0.7.0

func ValidateAuthoredTarget(tgt string) error

ValidateAuthoredTarget is ValidateTarget PLUS the reserved-target rule, and it is what every AUTHORED in-container target goes through — a policy's workspace_mounts/workspace_repos entry, a workspace source's target, and the re-validation of a stored source's target when a run seeds it.

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

func ValidateMemberMountSource added in v0.6.0

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

ValidateMemberMountSource is the BIND-time gate: the source half only, run against the roots already resolved for the run (SandboxSpec.MemberMountRoots) 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.

func ValidateMount

func ValidateMount(m Mount) error

ValidateMount enforces the host bind-mount deny-list. It returns nil for an allowed mount and a descriptive error (suitable for an HTTP 400 or a fail-closed CreateSandbox error) for any denied one. It is the 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 (workspace onboarding, the driver's recording-mount probe) 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 one of allowedTargetPrefixes. It is the target half of ValidateMount, extracted so another authoring surface that places something at an in-container path WITHOUT a host bind-mount source (e.g. a git-cloned WorkspaceRepo target) can enforce the same invariant without duplicating the prefix list.

Types

type AttachOptions

type AttachOptions struct {
	Cols uint16
	Rows uint16
}

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

type Capabilities

type Capabilities struct {
	Driver string `json:"driver"` // e.g. "docker"
	// ConfinementClasses available on this host/cluster, strongest last.
	ConfinementClasses []types.ConfinementClass `json:"confinement_classes"`
	// Resolved maps each available ConfinementClass to the concrete substrate
	// label that enforces it (e.g. "oci/runc", "oci/runsc", "oci/kata-qemu"), so
	// /healthz can advertise WHICH runtime backs each class — the seam that makes
	// CC3 substrate-pluggability visible to operators. Nil when a driver does not
	// report substrate detail.
	Resolved map[types.ConfinementClass]string `json:"confinement_substrates,omitempty"`
	// StructuralEgress reports L0 support: sandbox has no default route and
	// its only egress path is the wardyn-proxy sidecar.
	StructuralEgress bool `json:"structural_egress"`
	// NetworkPolicy reports L1 support (nftables / NetworkPolicy default-deny).
	NetworkPolicy bool `json:"network_policy"`
	// NetworkPolicyAcknowledged (B1): an operator has accepted an
	// ambient-default-deny-shaped canary failure as expected rather than
	// proven — see substrate.ClassSupport.NetworkPolicyAcknowledged's doc.
	// omitempty: absent on every driver that predates B1 reads the same as
	// false.
	NetworkPolicyAcknowledged bool `json:"network_policy_acknowledged,omitempty"`
	// SessionRecording reports wardyn-rec sidecar support.
	SessionRecording bool `json:"session_recording"`
	// UserDrives reports whether this driver can BIND a member's user drive
	// (migration 0054) into the sandbox — SandboxSpec.Drive.
	//
	// FALSE IS THE FAIL-CLOSED DEFAULT AND IT IS LOAD-BEARING. A driver that
	// says nothing declares no drive support, so the control plane refuses a
	// drive-carrying request rather than admitting one this substrate would
	// reject at CreateSandbox — the same rule ConfinementClasses states one
	// field up: never claim a control that is not structurally enforced, and
	// never schedule a run demanding more than the driver declares.
	//
	// omitempty: absent on a driver that predates the field reads the same as
	// false, which is the safe half.
	UserDrives bool `json:"user_drives,omitempty"`
	// EphemeralDiskEnforcement names WHAT ACTUALLY BINDS a run's Resources.DiskMiB
	// on this deployment: `filesystem` (docker, on a storage driver that can
	// enforce a per-container size quota), `eviction` (kubernetes — the kubelet
	// measures periodically and kills the pod over the limit; it never refuses the
	// write) or `none` (nothing binds it at all).
	//
	// EMPTY READS AS `none`, the same fail-closed direction UserDrives takes: a
	// driver that says nothing claims no enforcement. The orchestrator aggregates
	// it as the WEAKEST across substrates, not the strongest — a deployment must
	// not promise an enforcement one of its substrates cannot deliver.
	//
	// Surfaced on the admin setup status (internal/api's SetupRunner), so an admin
	// setting a disk number can see whether anything will hold it. NOT on the
	// anonymous /healthz.
	EphemeralDiskEnforcement types.StorageEnforcement `json:"ephemeral_disk_enforcement,omitempty"`
}

Capabilities declares what a driver (on this host/cluster) can actually enforce. The control plane uses this to honor Confinement Class policy: it must refuse to schedule a run whose policy demands more than the driver declares. Never claim a control that is not structurally enforced.

type ExecSession added in v0.5.0

type ExecSession struct {
	// Stdin writes to the exec's standard input. Close HALF-closes the write
	// side only (the exec observes EOF on stdin) — implementations MUST NOT
	// tear down Stdout/Stderr when Stdin.Close is called; those streams may
	// still be flowing.
	Stdin io.WriteCloser
	// Stdout carries standard output. With ExecSpec.TTY=true it ALSO carries
	// stderr (PTY semantics merge the two onto one stream); with TTY=false it
	// carries ONLY stdout — see Stderr.
	Stdout io.Reader
	// Stderr carries standard error as a SEPARATE stream when ExecSpec.TTY is
	// false. When TTY is true there is no separate stderr channel to read
	// (PTY semantics already merged it onto Stdout), so Stderr reads io.EOF
	// immediately.
	//
	// STREAMING CONTRACT (TTY=false): Stdout and Stderr are UNBUFFERED
	// io.Pipes fed by a single background demux goroutine off ONE underlying
	// connection: a single undrained stderr byte blocks the demux goroutine,
	// Stdout, AND Wait (which observes the same exec). Callers MUST start
	// draining Stderr BEFORE (or concurrently with) the first Stdout read.
	// This mirrors how every demultiplexed docker/moby attach stream must be
	// consumed; it is not implementation-specific.
	Stderr io.Reader
	// Resize changes the PTY window size. A no-op returning nil when the exec
	// has no TTY.
	Resize func(cols, rows uint16) error
	// Wait blocks until the exec exits and returns its exit code.
	Wait func() (int, error)
	// Close tears down ONLY this exec stream — never the sandbox, the agent
	// process, or any sidecar.
	Close func() error
}

ExecSession is a live, bidirectional stream into ONE exec started by Runner.ExecStream — the streaming-primitive analogue of Session (which is shaped for an interactive attach shell). It is a PLAIN STRUCT of streams/closures, deliberately NEVER an interface: ExecStream has exactly one production implementation per substrate plus test fakes, and a struct lets a fake construct a partial ExecSession (a canned Wait, a strings.Reader for Stdout, a nil Stdin) by filling in fields directly — there is no method set to satisfy, so no fake can ever "implement this wrong."

Resize, Wait, and Close take NO context parameter: an implementation binds them to whatever context it created the exec with. Callers that need a fresh deadline per operation should scope the ctx passed to ExecStream itself accordingly.

type ExecSpec added in v0.5.0

type ExecSpec struct {
	Argv []string
	// Env is additional exec-scoped environment (on top of the sandbox's own).
	Env []string
	// TTY requests a pseudo-terminal. PTY semantics MERGE stdout and stderr
	// onto ExecSession.Stdout (ExecSession.Stderr then reads io.EOF
	// immediately — there is no separate channel to read). Without a TTY,
	// stdout and stderr are delivered as SEPARATE streams: a binary protocol
	// riding stdout (SFTP, socat) would be corrupted by interleaved stderr
	// bytes, so a merged stream is only acceptable under PTY semantics.
	TTY bool
	// Cols, Rows seed the initial PTY window size (TTY only); zero lets the
	// implementation pick a default.
	Cols, Rows uint16
}

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

type ImageChecker added in v0.5.0

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

ImageChecker is an OPTIONAL Runner capability (W20-W20-record-image-5): a substrate whose local image cache can go stale out from under a workspace's cached image_ref (the docker driver — a pruned/removed local image; the daemon that built it is gone) implements this so a stale cache can be detected and fallen through to a rebuild, instead of the cached ref being trusted forever and every launch failing at "no such image" until an operator finds and clears the row by hand. A substrate that pulls fresh per launch (k8s: the kubelet pulls, there is no local cache to go stale) has nothing to report and simply doesn't implement this — callers type-assert and treat "doesn't implement" the same as a check error: unknown, so trust the cache (fail-open, the same posture resolveWorkspaceImage already takes everywhere else).

type ImageRemover added in v0.5.0

type ImageRemover interface {
	// ImageRemove deletes ref from the substrate's local image store. A ref
	// already absent (raced by a manual prune, a prior partial cleanup) is
	// NOT an error — same idempotent-teardown contract as StopSandbox.
	ImageRemove(ctx context.Context, ref string) error
}

ImageRemover is an OPTIONAL Runner capability (bug-workspace-1): a substrate with a local image store (the docker driver) implements this so a superseded workspace-built image (a rescan, an image-choice edit, or a workspace delete) can be reclaimed instead of leaking a full docker image forever — every resolveWorkspaceImage build lane mints a fresh, uniquely- named local tag on every cache miss and nothing removed the tag it replaced. Best-effort by design (callers log-and-continue on error, the same posture as ImageChecker): a substrate that pulls fresh per launch (k8s) has nothing local to reclaim and simply 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 credential grant with its proxy-side injection rule (host/header/format/secret name — never the secret value).

type MemberMountPolicy added in v0.6.0

type MemberMountPolicy struct {
	// Roots is the shared allowlist (WARDYN_MEMBER_WORKSPACE_ROOTS): absolute,
	// cleaned host prefixes any member's local_dir source must resolve into.
	Roots []string
	// RootsByPrincipal (WARDYN_MEMBER_WORKSPACE_ROOTS_MAP) is the per-member
	// override. A principal with an entry uses ONLY that entry — per-member
	// REPLACES shared, because per-member 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, the same dual-key
	// identity a capability_grants `user` subject carries).
	RootsByPrincipal map[string][]string
	// WritableRoots (WARDYN_MEMBER_WRITABLE_ROOTS) is where a member may mark
	// their own mount writable. Empty = nowhere.
	WritableRoots []string
	// WritableDeny (WARDYN_MEMBER_WRITABLE_DENY) carves holes in WritableRoots.
	// Deny WINS over allow, and is checked first.
	WritableDeny []string
}

MemberMountPolicy is the operator/MDM-set posture for MEMBER-authored local_dir binds. The zero value means "no member local_dir mounts at all", which is the fail-closed default: a deployment that configures none of these vars behaves exactly as it does today, minus the ability for a member to onboard a host directory.

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 (the 0042 hazard), and this is a security ceiling.

func ParseMemberMountPolicy added in v0.6.0

func ParseMemberMountPolicy(roots, rootsMap, writable, writableDeny string) (p MemberMountPolicy, warnings []string, err error)

ParseMemberMountPolicy builds the policy from its four raw operator/MDM env values (see MemberMountPolicy 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 O4 posture notices the caller logs at boot: a root of "/" or of $HOME is permitted (matching the LocalMode unspecified-bind WARN precedent documented on humanOrAdminAuth) but leaves the section-(c) residual unbounded, so it must never be silent.

func (MemberMountPolicy) Configured added in v0.6.0

func (p MemberMountPolicy) 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 (MemberMountPolicy) RootsFor added in v0.6.0

func (p MemberMountPolicy) 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 (MemberMountPolicy) ValidateMemberMount added in v0.6.0

func (p MemberMountPolicy) ValidateMemberMount(principal, src string, writable bool) error

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

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 — a member-owned
	// workspace's local_dir. ONLY these are re-checked against
	// SandboxSpec.MemberMountRoots at bind time, because the roots bound what a
	// MEMBER may name and nothing else: the same spec also carries binds WARDYN
	// ITSELF authored (the subscription ~/.claude credential staging, the Bedrock
	// ~/.aws dir) and an operator-owned workspace's dirs, none of which live
	// under any member root. Checking those too refused the credential mounts
	// EVERY model run needs, so no member-owned workspace could run at all on a
	// subscription or Bedrock deployment.
	//
	// Set by internal/api dispatch from the run's member-owned workspaces
	// (memberMountPosture); false — the operator default — everywhere else.
	MemberAuthored bool `json:"member_authored,omitempty"`
	// DriveAuthored marks the ONE bind a driver synthesizes from
	// SandboxSpec.Drive: the host_path user drive's per-person subdirectory.
	//
	// IT IS A LABEL, NEVER A GATE — unlike MemberAuthored, which selects which
	// of a mixed slice of binds the member roots apply to. A drive arrives on
	// its OWN field (SandboxSpec.Drive), so the driver already knows it is
	// looking at a drive; the deployment's WARDYN_USER_DRIVE_HOST_ROOTS ceiling
	// (UserDriveHostRootCheck) therefore runs on EVERY host_path drive
	// unconditionally, and no check anywhere may be written as `if
	// m.DriveAuthored`. One was, and it was fail-OPEN by shape: the flag's only
	// false state is a refactor that stops stamping it, so the check would
	// vanish exactly when the code around it changed.
	//
	// IT NEVER LEAVES THE DRIVER, and never reaches a wire. SandboxSpec.Drive is
	// a types.DriveMount rather than a Mount precisely so the composer clamp, the
	// workspace-source allow-list and the k8s blanket host-bind refusal never see
	// a drive — which also means dispatch puts no Mount carrying this flag into
	// spec.Mounts, and nothing that serializes a SandboxSpec ever observes it.
	// The Docker driver builds a local Mount from the DriveMount so the bind
	// passes through the same value shape every other bind does, stamps this on
	// it, and converts it to the runtime's own mount type in the next statement.
	// The json tag is the struct's shape, not a claim that this field is
	// transmitted; `omitempty` plus "never set outside the driver" means it is
	// absent from every spec that is.
	//
	// So it is DOCUMENTATION IN THE TYPE, kept deliberately (user-drives DESIGN
	// §3.1(5) and §11 Q3, owner default: a bool, converted to a Kind enum at the
	// third authoring class). `git grep -n DriveAuthored` is the whole audit: the
	// declaration, the one assignment, and prose. If a gate ever reads it, that
	// grep is where the fail-open shows up.
	//
	// TWO FLAGS, NOT A Kind ENUM, and this comment is the trigger to change
	// that: N=2 is below the consolidation threshold and MemberAuthored is
	// security-critical code, so a THIRD authoring class — anything that adds a
	// `*Authored bool` beside these two — is the point at which both become one
	// `Kind` field with a closed set of values, rather than three booleans whose
	// illegal combinations are only prevented by everyone remembering.
	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 (drivers restrict it to an allowed prefix, e.g. under /home/agent or /work). ReadOnly defaults to true (RW only when the policy explicitly opts in). See SandboxSpec.Mounts for the security model: mounts are operator/policy-controlled, never request-set.

type ProxyConfig

type ProxyConfig struct {
	// RunToken authenticates the sidecars to the control plane (identity
	// provider verifies; it is NOT a secret usable outside the platform).
	RunToken string
	// ControlPlaneURL is where sidecars stream decisions/recordings.
	ControlPlaneURL string
	// ControlPlaneCAPEM is wardynd's internal CA (internal/hoptls): the only
	// root the sidecar trusts for ControlPlaneURL. Public; empty with a
	// loopback http URL only.
	ControlPlaneCAPEM string
	// Policy is the run's egress policy, handed verbatim to the wardyn-proxy
	// sidecar (default-deny domain allowlist, method rules, first-use flag).
	// Drivers MUST deliver it to the sidecar at launch: a proxy without a
	// policy fails closed and the sandbox has no working egress at all.
	Policy types.RunPolicySpec
	// Injection lists the run's auto-mintable api_key grants the proxy
	// resolves at startup (secret values live only in proxy memory — never in
	// the sandbox). Approval-gated api_key grants are NOT included: they would
	// block proxy startup, which fails closed if any injection mint fails.
	Injection []InjectionGrant
	// MITMCACertPEM / MITMCAKeyPEM are the OPTIONAL per-run TLS-MITM CA (PEM)
	// delivered to the proxy sidecar when the policy opts into intercept_tls. The
	// CA private key reaches ONLY the proxy (never the sandbox); the sandbox
	// trusts the public cert (delivered separately via the agent env). Empty =>
	// opaque CONNECT passthrough (no MITM).
	MITMCACertPEM string
	MITMCAKeyPEM  string
	// MITMHosts are OPERATOR-CONFIGURED corp artifact hosts the proxy may TLS-MITM
	// in addition to the built-in LLM hosts, so a corporate registry token can be
	// injected on the wire (the sandbox never holds it). A tight per-host operator
	// allowlist sourced from site-config's artifact overrides — NEVER a blanket and
	// NEVER attacker-controlled (dispatch populates it, the sandbox cannot).
	MITMHosts []string
	// MITMLLM reports whether TLS-MITM of the built-in LLM hosts (Anthropic/OpenAI)
	// is intended for this run (subscription injection or intercept_tls) — as opposed
	// to a CA minted only for artifact-token injection. See proxy.Config.MITMLLM.
	MITMLLM bool
	// GitGrants is the git-broker per-repo allowlist ("<org>/<repo>" -> github_token
	// grant id) delivered to the proxy sidecar's /wardyn/gh/ route so the sandbox
	// reaches only its granted repos (never all of github.com) and the token stays
	// proxy-side. Populated at dispatch from the run's github grants; empty => no
	// repo brokered. See proxy.Config.GitGrants.
	GitGrants map[string]uuid.UUID
	// PATGrants is the git_pat broker's per-HOST allowlist ("host" -> the grant
	// to mint from), backing the proxy's /wardyn/git/ route so a non-GitHub
	// forge's PAT is minted proxy-side and never enters the sandbox. Empty => no
	// host brokered. See proxy.Config.PATGrants.
	PATGrants map[string]proxy.PATGrant
	// ADOGrants is the run's per-person Azure DevOps grant for the proxy's REST
	// gate. See proxy.Config.ADOGrants.
	ADOGrants []proxy.ADOGrantConfig
	// UpstreamProxyURL is the OPTIONAL corporate parent proxy the sidecar chains
	// egress through (http://[user:pass@]host[:port] — https-to-proxy is rejected
	// by the sidecar's own config validation, parseUpstreamProxy). Threaded
	// verbatim to the proxy sidecar; control-plane calls bypass it. Empty =>
	// direct dial.
	//
	// Sourced operator-wide at dispatch (internal/api/runs.go dispatchWithVerify)
	// from the persisted site-config's UpstreamProxySecretRef, resolved to the
	// secret's value. Any resolution failure (unset ref, missing secret, non-http
	// URL) leaves this "" rather than failing the run — see
	// resolveUpstreamProxyURL and its audit event run.upstream_proxy.resolve.
	UpstreamProxyURL string
	// TrustedCAPEM is the operator's corporate CA bundle (WARDYN_TRUSTED_CA_FILE,
	// api.Config.TrustedCAPEM), forwarded verbatim so the sidecar's egress TLS
	// additionally trusts it (never its control-plane calls). Threaded to the proxy via proxy.Config's
	// identically-named field (WARDYN_PROXY_CONFIG_JSON, BuildProxyConfig below).
	// Control-plane-authored, same trust boundary as MITMCACertPEM/MITMCAKeyPEM
	// above; empty => system roots only, byte-identical to today.
	TrustedCAPEM string
	// InternalHosts are the operator-declared internal hostnames
	// (SiteConfig.InternalHosts, forwarded verbatim) eligible for the proxy's
	// private-IP-guard lift. Control-plane-authored (the sandbox cannot set
	// this); empty => no lift, byte-identical to today. Threaded to the proxy
	// via proxy.Config's identically-named field (BuildProxyConfig below).
	InternalHosts []types.InternalHost
	// UpstreamProxyNoProxy is SiteConfig.UpstreamProxyNoProxy, forwarded
	// verbatim: the hosts, .domain suffixes and CIDRs whose dials skip the
	// corporate upstream proxy and are made directly. It is a ROUTING decision
	// only — a bypassed dial still faces the private-IP guard and the run's
	// policy, which is why reaching a private endpoint also needs an
	// InternalHosts declaration.
	UpstreamProxyNoProxy []string
	// LLMUpstreams maps a public vendor host to an operator-configured internal
	// model gateway base URL (api.Config.LLMGateways, forwarded verbatim;
	// WARDYN_ANTHROPIC_BASE_URL/WARDYN_OPENAI_BASE_URL). Control-plane-authored
	// (the sandbox cannot set this); empty => every brokered LLM route dials
	// the vendor host, byte-identical to today. Threaded to the proxy via
	// proxy.Config's identically-named field (BuildProxyConfig below).
	LLMUpstreams map[string]string
	// LLMUnavailableDetail is the control-plane-composed reason the proxy's
	// brokered-LLM 404 gives when this run has no LLM credential behind that
	// route (internal/api's llmUnavailableDetail). Empty => the route's own
	// generic detail. Threaded to the proxy via proxy.Config's identically-named
	// field (BuildProxyConfig below).
	LLMUnavailableDetail string
}

type Resources

type Resources struct {
	CPUMillis int64
	MemoryMiB int64
	// PidsLimit caps the number of processes/threads in the sandbox — the
	// fork-bomb guard for the host PID space. Zero => driver default.
	PidsLimit int64
	// DiskMiB caps writable storage. Best-effort, and WHAT BINDS IT DIFFERS BY
	// SUBSTRATE — Capabilities.EphemeralDiskEnforcement names which: the docker
	// driver applies it as a storage-driver quota only when the daemon supports a
	// per-container one (overlay2 on xfs+pquota, or btrfs/zfs), otherwise it warns
	// and runs uncapped rather than hard-failing the run; the k8s substrate sets
	// the agent container's resources.limits[ephemeral-storage], where the kubelet
	// enforces it by EVICTING the pod (periodically measured; the write is never
	// refused).
	DiskMiB int64
	// DiskMiBFilled says DiskMiB was FILLED IN for this run from the org's
	// storage.ephemeral.default_disk_mib rather than authored on its policy —
	// the one bit that tells a driver whose host cannot enforce a cap whether
	// refusing the run is the honest answer or the destructive one.
	//
	// An org default arrives by MDM (site-config on every desktop boot), and the
	// desktop tier is overlay2 over ext4, where the docker driver's disk quota
	// FAILS THE CREATE CLOSED. Failing closed is right for a cap a policy asked
	// for and this host cannot keep; applied to a fleet-wide default it would
	// brick every request-less run on every laptop the moment an admin typed a
	// number for the Kubernetes half of the estate. So a FILLED value degrades
	// to uncapped-with-a-warning there (enforcement `none`), while a
	// POLICY-AUTHORED DiskMiB keeps today's fail-closed arm exactly.
	//
	// The k8s substrate ignores the bit on purpose: an ephemeral-storage limit
	// is set and the kubelet evicts either way, so there is no unenforceable
	// case for it to degrade.
	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" (the docker driver fills CPU/memory/PIDs unconditionally so EVERY sandbox is capped even when policy sets nothing). The control plane copies these from a policy's types.ResourceLimits at dispatch.

type Runner

type Runner interface {
	Name() string
	Capabilities(ctx context.Context) (Capabilities, error)
	// CreateSandbox provisions the sandbox AND its sidecars (proxy, recorder)
	// with L0 confinement: no default route, egress only via the proxy.
	CreateSandbox(ctx context.Context, spec SandboxSpec) (Sandbox, error)
	// Exec starts the agent process inside the sandbox (PTY attached when
	// recording). Returns when the process has been started, not finished. The
	// returned agentExecID identifies the started process for exec-based substrates
	// (the docker idle-container + `docker exec` path); it is "" for exec-less /
	// main-process substrates (krun), where the container IS the agent. Persist it
	// so the crash reconciler can observe agent liveness across a wardynd restart
	// via AgentStatus.
	Exec(ctx context.Context, ref string, argv []string) (agentExecID string, err error)
	// Wait blocks until the agent process started by Exec for this sandbox ref
	// has exited, returning its exit code. It is ONLY valid after a successful
	// Exec on the same ref (it observes the agent exec Exec created). Wait
	// honours ctx cancellation/deadline and returns an error if no agent exec
	// is tracked for ref (e.g. Exec was never called, or the ref is unknown).
	Wait(ctx context.Context, ref string) (exitCode int, err error)
	// Attach opens a NEW interactive exec (an interactive shell) inside the
	// already-RUNNING sandbox ref and returns a live PTY Session. This is the
	// foundation of interactive session mode: a human attaches to a live PTY in
	// a running sandbox. The exec is SEPARATE from the agent process Wait tracks
	// — it is a fresh shell, so attaching/detaching never affects the agent.
	//
	// Session.Close tears down ONLY the exec stream, NOT the sandbox: detaching
	// leaves the run and its sidecars exactly as they were. The interactive
	// shell is bounded by the SAME L0 egress + confinement envelope as the agent
	// (it runs inside the existing sandbox); Attach opens no new network path
	// (invariant 3). Callers MUST record the human principal for attribution
	// (invariant 4) at the call site (the runner is identity-agnostic).
	Attach(ctx context.Context, ref string, opts AttachOptions) (Session, error)
	// ExecStream launches spec.Argv inside the already-RUNNING sandbox ref as a
	// fresh, streamable exec, distinct from both the agent process Exec starts
	// (Wait/AgentStatus observe THAT one, not this) and from Attach's
	// interactive shell (no PTY tmux session, no shell wrapping — argv runs
	// directly). See ExecSpec/ExecSession for the streaming and TTY-merge
	// contract.
	//
	// UNLIKE Exec, ExecStream MUST be repeatable against the SAME ref: a
	// long-lived consumer (an SSH/SFTP bridge, a multiplexed shell) opens ONE
	// ExecStream per channel against ONE long-lived sandbox ref for the life
	// of the connection, so a substrate that errored or no-op'd on a second
	// call would break that consumer outright. A k8s substrate implements
	// ExecStream on the streaming exec subresource (pods/<name>/exec —
	// repeatable, leaves no pod-spec residue), NOT an ephemeral container
	// (add-only; that add-only limit is what makes Exec one-shot, not
	// ExecStream — see Substrate.Exec's doc).
	//
	// SECURITY: ExecStream opens no new network path (invariant 3). Callers
	// MUST record the human principal for attribution (invariant 4) at the
	// call site (the runner is identity-agnostic) — a raw argv makes this the
	// MORE dangerous of the two streaming primitives, not less.
	ExecStream(ctx context.Context, ref string, spec ExecSpec) (*ExecSession, error)
	Status(ctx context.Context, ref string) (Status, error)
	// AgentStatus reports the AGENT's observed state in a restart-safe way, given
	// the agentExecID Exec returned (persisted on the run row). For exec-based
	// substrates it inspects that exec, so a run whose agent has exited reports a
	// terminal State + ExitCode even while the idle container is still up — the
	// distinction container-level Status cannot make after a restart lost the
	// in-memory exec map. When agentExecID is "" (exec-less/main-process, or Exec
	// never ran) it falls back to Status, where the container IS the agent.
	AgentStatus(ctx context.Context, ref, agentExecID string) (Status, error)
	// StopSandbox is the graceful path (lifecycle auto-stop).
	StopSandbox(ctx context.Context, ref string) error
	// 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 is what the driver actually applied (>= requested or error).
	EnforcedClass types.ConfinementClass
}

Sandbox is a handle to a created sandbox.

type SandboxSpec

type SandboxSpec struct {
	RunID            uuid.UUID
	Image            string // resolved agent/workspace OCI image
	ConfinementClass types.ConfinementClass
	// Env is non-secret environment: every value here is safe to read straight
	// off the substrate's own object model (a docker container config, a k8s
	// Pod spec — which any principal with pods/get can read). Credential
	// material NEVER passes through here; it rides SecretEnv.
	Env map[string]string
	// SecretEnv is CREDENTIAL-BEARING environment — the same name=value shape as
	// Env, delivered to the agent under the same names — split out because a
	// substrate must be able to deliver it WITHOUT writing the value anywhere an
	// ordinary reader of that substrate can see it. Two dispatch lanes populate
	// it (internal/api's resolveEnvSecretGrants, for env_secret grant VALUES,
	// and applyBedrockTransport, for the resident AWS SigV4 keys / the captured
	// AWS SSO blob); every other variable a run gets is platform configuration
	// and stays in Env.
	//
	// DISJOINT from Env by construction — dispatch's splitSecretEnv MOVES a key
	// from one map to the other, never copies — so a driver may concatenate the
	// two without deduplicating, and no value is ever delivered twice.
	//
	// Driver obligations differ because the exposure differs. 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 in the runs
	// namespace, which is the entire reason this field exists (the same reason
	// the proxy config already travels as a Secret rather than inline). The
	// docker driver passes them as ordinary container env: a container's config
	// is readable only through the daemon socket, i.e. by a principal already
	// root-equivalent on that host — the trust boundary docker's proxyEnv
	// already documents for 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 are hard caps (cgroups / ResourceQuota).
	Resources Resources
	// Labels are attached to the sandbox for attestation selectors and audit.
	Labels map[string]string
	// Mounts are operator/policy-controlled host bind mounts into the sandbox
	// (e.g. a host repo at ~/work for the WSL-migration substrate / a persistent
	// workspace). SECURITY: these are POLICY-controlled, NEVER attacker-controlled.
	// The ONLY population path is internal/api dispatch copying a policy's
	// RunPolicySpec.WorkspaceMounts here; the create-run HTTP request body has no
	// mounts field, so a prompt-injected agent or a malicious run requester can
	// never choose a host mount. Drivers apply these as bind mounts AND enforce a
	// deny-list defense-in-depth (see runner/docker/driver.go) even though the
	// values came from policy. Default ReadOnly.
	Mounts []Mount
	// MemberMountRoots, when non-nil, marks this run as one whose mounts were
	// authored by a MEMBER (a member-owned workspace's local_dir) and carries
	// the operator/MDM-set roots those mounts must resolve inside. Resolved at
	// create-run from the owning member's principal (MemberMountPolicy.RootsFor)
	// and re-checked by the driver at BIND time — the last moment this process
	// can resolve the real path — via ValidateMemberMountSource.
	//
	// NIL for every operator/non-member run, and nil means the driver does
	// EXACTLY what it does today: the member gate is purely additive and can
	// never narrow an operator mount. See member_mount.go for the threat model.
	MemberMountRoots []string
	// Drive is the acting principal's USER DRIVE (migration 0054), already
	// resolved, folded and narrowed by the control plane — nil for every run
	// that did not ask for one. It is NOT a Mount: it never rides
	// RunPolicySpec.WorkspaceMounts, the composer clamp never sees it, and the
	// request that produced it carried a flag rather than a path (the operator-
	// controlled-only mounts guardrail). Drivers mount ObjectName at Target
	// (DriveTarget) with the given mode; the Docker driver still converts it to
	// a Mount internally so the deny matrix runs on the host path.
	Drive *types.DriveMount
	// OnWaiting, when non-nil, reports WHY this sandbox is not up yet, in the
	// substrate's own `<component>: <Reason>[: <message>]` words ("agent:
	// ImagePullBackOff: …", "pod: Unschedulable: …", "image: Pulling: <ref>"),
	// each time that reason CHANGES. It exists because the whole STARTING window
	// is spent INSIDE CreateSandbox — there is no sandbox ref yet, so nothing
	// outside the driver can ask a substrate what it is waiting on — and the
	// answer is already in the driver's hand, read and discarded once per poll.
	//
	// CONTRACT, and the driver relies on every clause of it: called
	// SYNCHRONOUSLY on CreateSandbox's own goroutine, so an implementation must
	// not block (wardynd's does one scoped UPDATE under a 500ms deadline and
	// drops an overdue one); called only while CreateSandbox is still running,
	// never after it returns; only on a CHANGE, so a 200ms poll costs one call
	// per distinct reason rather than five a second; DIAGNOSTIC, so a driver
	// never fails a create because a report could not be delivered. Nil for every
	// driver-level caller (the conformance suite, cmd/wardyn-runner), which is
	// why NotifyWaiting rather than the field is what drivers call.
	OnWaiting func(detail string) `json:"-"`
	// Interactive marks a run that comes up idle for `wardyn attach` (no task is
	// exec'd). Drivers use it to prepare the workspace on the idle main process —
	// e.g. clone the repo into ~/work — so the attach shell isn't empty. A non-
	// interactive run ignores it (its task exec does the preparation).
	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: a nil check at each of the four report sites is four chances to forget one.

type Session

type Session interface {
	// Read copies terminal output (PTY bytes) into p. It returns io.EOF when the
	// shell exits or the stream is closed.
	Read(p []byte) (int, error)
	// Write sends keystrokes (PTY bytes) into the shell.
	Write(p []byte) (int, error)
	// Resize informs the PTY of a new window size (e.g. on a browser resize).
	Resize(ctx context.Context, cols, rows uint16) error
	// Close tears down ONLY the interactive exec stream. It does NOT stop the
	// sandbox, the agent process, or any sidecar — detaching a human leaves the
	// run exactly as it was.
	Close() error
}

Session is a live, bidirectional interactive PTY stream into a RUNNING sandbox, opened by Runner.Attach. It is the human-facing analogue of the agent exec: a person types into Write and reads the terminal back from Read.

SECURITY (invariant 3): the interactive shell runs INSIDE the existing sandbox, so it is bounded by exactly the same L0 structural-egress and confinement envelope as the agent process. Attach opens NO new network path — the stream flows control-plane -> dockerd -> container, never through the sandbox's HTTP_PROXY egress path, and egress/mint enforcement stays at the proxy/broker. Attach therefore grants a terminal, not a new egress route.

A Session is NOT safe for concurrent Read/Write from multiple goroutines on the same direction, but the typical pump runs Read in one goroutine and Write in another, which is supported.

type Status

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

Status reports observed sandbox state.

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 the runner.Runner surface that lets a non-OCI microVM VMM (SmolVM, Firecracker, …) back a Confinement Class alongside the OCI/Docker substrate, without the control plane (or each substrate) re-implementing the runner contract.
Package substrate defines the confinement-substrate sub-interface: the seam beneath the runner.Runner surface that lets a non-OCI microVM VMM (SmolVM, Firecracker, …) back a Confinement Class alongside the OCI/Docker substrate, without the control plane (or each substrate) re-implementing the runner contract.

Jump to

Keyboard shortcuts

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