Documentation
¶
Overview ¶
Package container provides the Backend seam — the interface the engine's Dispatcher uses to run commands inside long-lived containers (a single digest-pinned image or a compose project, per awf-workflow(5), CONTAINERS).
Phase 2 ships only the interface and an in-memory fake (fake.go); Phase 4 adds the Docker impl behind the same interface. Cross-impl conformance is the backendtest sub-package's job (RunBasicContract).
Per runtime-design §3, container depends on no other AWF package. CaptureFiles therefore returns content bytes (CapturedFile{Path, Content}), NOT CAS refs (which would require Backend to Put to Blobs, violating the CLAUDE.md invariant "the interpreter is the only writer to state"). The slice 2.4 dispatcher passes the bytes up; the interpreter Puts at the commit boundary.
Index ¶
- Constants
- Variables
- func BuildTreeTar(files map[string][]byte) ([]byte, error)
- func ExtractTreeTar(tarGz []byte, maxBytes int64, maxEntries int) (map[string][]byte, error)
- type Backend
- type Caps
- type CapturedFile
- type Cmd
- type ContainerResources
- type ContainerSpec
- type ExecResult
- type Fake
- func (f *Fake) BlockExec() chan struct{}
- func (f *Fake) Capabilities() Caps
- func (f *Fake) CaptureFiles(ctx context.Context, h Handle, paths []string) ([]CapturedFile, error)
- func (f *Fake) ClearFault()
- func (f *Fake) CopyTo(ctx context.Context, h Handle, files []InputFile) error
- func (f *Fake) Create(_ context.Context, spec ContainerSpec) (Handle, error)
- func (f *Fake) Destroy(_ context.Context, h Handle) error
- func (f *Fake) Exec(ctx context.Context, h Handle, cmd Cmd) (<-chan IOChunk, <-chan ExecResult, error)
- func (f *Fake) FailCaptureAfterN(n int)
- func (f *Fake) FailCreateConfigForImage(image string)
- func (f *Fake) FailCreateForImage(image string)
- func (f *Fake) FailExecAfterN(n int)
- func (f *Fake) ProgramExec(run string, result ExecResult, chunks []IOChunk)
- func (f *Fake) ProgramExecAny(result ExecResult, chunks []IOChunk)
- func (f *Fake) ProgramExecWithFiles(run string, result ExecResult, chunks []IOChunk, files map[string][]byte)
- func (f *Fake) ProgramImageDigest(image, digest string)
- func (f *Fake) ReadFileAt(ctx context.Context, h Handle, path string) ([]byte, error)
- func (f *Fake) ReadTreeAt(ctx context.Context, h Handle, dir string) ([]byte, error)
- func (f *Fake) ReleaseBlockedExec()
- func (f *Fake) Restore(_ context.Context, ref SnapshotRef, name string) (Handle, error)
- func (f *Fake) Snapshot(_ context.Context, h Handle) (SnapshotRef, error)
- func (f *Fake) WithBlobs(b blobStore) *Fake
- func (f *Fake) WriteFile(h Handle, path string, content []byte) error
- func (f *Fake) WriteFileAt(ctx context.Context, h Handle, path string, content []byte) error
- func (f *Fake) WriteTreeAt(ctx context.Context, h Handle, dir string, tarGz []byte) error
- type Handle
- type IOChunk
- type ImageUnavailableError
- type InputFile
- type RestoreCall
- type SnapshotMode
- type SnapshotRef
- type WorkdirResolver
- type WriteFileAtCall
- type WriteTreeAtCall
Constants ¶
const AWFOutputDir = "/tmp/awf"
AWFOutputDir is the base directory the engine dispatcher derives AWF_OUTPUT tempfile paths under — AWFOutputDir/<sanitized-step>.json (see engine/awf_output.go) — and that the native Backend pre-creates at New() so the author's `> $AWF_OUTPUT` redirect can write the file. Docker containers get a fresh /tmp and don't need it. One source so the engine's derivation and the native bootstrap cannot drift.
const TeardownGrace = 30 * time.Second
TeardownGrace is how long Backend.Destroy / Backend.Down gets after the run's ctx has been cancelled (Ctrl-C / SIGTERM). The signal cancels in-flight work via ctx, but the deferred teardown needs a non-cancelled ctx so containers actually come down. 30s is generous for the instant Fake and for Docker (`docker stop --time=10s` + cleanup typically <30s). One source so every teardown site shares one deadline.
const TreeTarMaxBytes int64 = 4 << 30
TreeTarMaxBytes is the default cumulative decompressed-bytes cap for ExtractTreeTar — mirrors native's snapshotDefaultMaxRestoreBytes (4 GiB).
const TreeTarMaxEntries int = 1_000_000
TreeTarMaxEntries is the default entry-count cap for ExtractTreeTar — mirrors native's snapshotMaxEntries (1 000 000).
Variables ¶
var ErrSnapshotTooLarge = errors.New("snapshot diff exceeds the backend size limit")
ErrSnapshotTooLarge is the sentinel for a deterministic snapshot-capacity failure (the workspace diff exceeds the backend's size cap). A backend's concrete too-large error wraps this so callers can errors.Is without importing the concrete backend package. Phase-4 decision 11: this is a permanent_failure (retrying re-runs the step to fail identically).
var ErrUnsupported = errors.New("container: operation not supported by this backend")
ErrUnsupported is the sentinel returned by Backend methods this backend doesn't implement. Phase 2 fake returns it from Snapshot / Restore. Callers route with errors.Is(err, container.ErrUnsupported).
Functions ¶
func BuildTreeTar ¶ added in v0.1.1
BuildTreeTar encodes files as a deterministic gzip-tar whose entries are sorted by path. Intermediate directory entries are synthesised for every parent component. All paths in the map are treated as relative (no leading slash). Headers carry zeroed mtime, uid, gid, uname, gname — identical content always produces identical bytes (the blob = content-hash invariant).
BuildTreeTar does NOT validate that the caller's paths are free of ".." or absolute components; ExtractTreeTar enforces those guards on read.
func ExtractTreeTar ¶ added in v0.1.1
ExtractTreeTar decompresses and decodes a gzip-tar produced by BuildTreeTar (or compatible tools) and returns a map of relative path → content for every regular file in the archive.
Two zip-bomb / path-escape guards are enforced:
- maxBytes: cumulative decompressed bytes limit; triggers when exceeded.
- maxEntries: total entry count (dirs + files); triggers when exceeded.
Any entry whose cleaned path starts with ".." or is absolute is rejected as a path-escape attempt. Only regular-file entries contribute to the returned map; directory entries are accepted (for tree structure) but not returned.
Types ¶
type Backend ¶
type Backend interface {
// Capabilities reports what this backend supports. Must be callable on a
// zero-state backend (no Create needed); the engine queries it at startup
// to decide whether snapshot:workspace containers can be served.
Capabilities() Caps
// Create brings a container to readiness per the spec. The returned Handle
// is opaque to the caller and must be passed back to Exec / CaptureFiles /
// Snapshot / Destroy. Phase 2 fake: allocates an in-mem fs map keyed by a
// monotonic ID. Phase 4 Docker: image-pinned create or compose `up --wait`.
Create(ctx context.Context, spec ContainerSpec) (Handle, error)
// Exec runs a command synchronously inside the handle. Returns two
// channels and an error.
//
// chunks: STREAMING channel of IOChunks. Emits each stdout/stderr chunk
// as it arrives from the underlying pipe — callers ranging over it
// observe the process's output in real time. Closed by the Backend
// implementation when both stdout and stderr pipes drain (process
// either exited or ctx-cancelled). MUST be drained by the caller; an
// unread chunk back-pressures the reader goroutines (chan buffer is
// unspecified but bounded).
//
// result: SINGLE-VALUE channel of ExecResult. Receives exactly one
// value AFTER the chunks channel closes AND the process has been
// waited-on (ExitCode + accumulated Stdout + Err available). Then
// closes. Receiving from it before chunks closes is permitted (blocks
// until the close) but unusual — the natural order is
// `for range chunks { ... }; r := <-result`. ExecResult.Err surfaces
// transport-class errors (stdcopy mid-stream failure on docker;
// ctx.Canceled on native pipe close mid-read) WITHOUT swallowing them
// silently.
//
// On non-nil error return, BOTH channels are nil; callers must check
// err before ranging or receiving. The "launch / transport" error
// class — couldn't start the command at all — surfaces here. If the
// command ran and merely exited nonzero, err is nil and result's
// ExecResult.ExitCode carries the code.
//
// ctx cancellation: aborts the command; chunks channel closes; result
// emits an ExecResult with Err set to ctx.Err() (and likely
// process-killed ExitCode). The function's error return remains nil —
// the cancellation surfaces via the result value's Err field, NOT via
// err. Callers that used `errors.Is(err, context.Canceled)` must
// update to check `result.Err` instead.
//
// Slice 5.3: STREAMING refactor of the original Phase 2
// "(ExecResult, <-chan IOChunk, error)" contract. The Claude Code
// adapter (agent/claude.Launch) needs progressive event arrival;
// refactoring Backend.Exec across all three backends is the
// load-bearing prerequisite.
Exec(ctx context.Context, h Handle, cmd Cmd) (<-chan IOChunk, <-chan ExecResult, error)
// CaptureFiles reads the named in-container paths and returns their content,
// one CapturedFile per path in the input order. Missing-path is a hard error
// (no partial returns) — output_files are declared by the author and absence
// at commit time is a step bug, not graceful degradation. The slice 2.4
// dispatcher passes the bytes up; the interpreter Puts each into Blobs at
// the commit boundary (this method never touches state.Blobs; see pkg doc).
CaptureFiles(ctx context.Context, h Handle, paths []string) ([]CapturedFile, error)
// CopyTo writes each InputFile's bytes to its in-container path BEFORE the
// step's Exec/Launch — the symmetric inverse of CaptureFiles. Overwrites an
// existing path. len(files)==0 is a no-op returning nil. Unknown handle is a
// hard error. Never touches state.Blobs.
CopyTo(ctx context.Context, h Handle, files []InputFile) error
// ReadFileAt reads a single in-container path and returns its content.
// Missing path or unknown handle is a hard error. Mirrors CaptureFiles but
// for one path. NEVER touches state.Blobs — the engine Puts at commit.
ReadFileAt(ctx context.Context, h Handle, path string) ([]byte, error)
// WriteFileAt writes content to a single in-container path, overwriting.
// Unknown handle is a hard error. Symmetric inverse of ReadFileAt. NEVER
// touches state.Blobs. Real impls confine the path; the fake is in-memory.
WriteFileAt(ctx context.Context, h Handle, path string, content []byte) error
// ReadTreeAt returns the subtree rooted at dir inside the container as a
// gzip-tar whose entries are relative to dir (no leading dir prefix).
// Missing or empty dir is a hard error. Unknown handle is a hard error.
// NEVER touches state.Blobs — the engine Puts the archive at commit.
ReadTreeAt(ctx context.Context, h Handle, dir string) ([]byte, error)
// WriteTreeAt extracts a gzip-tar (as produced by ReadTreeAt or
// BuildTreeTar) into the container under dir, creating dir and all
// parent directories. Archive entries must be relative to dir (no
// leading slash, no ".." escape); implementations reject escape
// attempts. Overwrites existing files. Unknown handle is a hard error.
// NEVER touches state.Blobs.
WriteTreeAt(ctx context.Context, h Handle, dir string, tarGz []byte) error
// Snapshot captures the handle's filesystem as a CoW diff. Only meaningful
// for snapshot:workspace containers (spec §3). Phase 2 fake: returns
// ("", ErrUnsupported). Phase 4 Docker (slice 4.4): streaming gzip-tar
// diff via ContainerDiff + per-path CopyFromContainer through
// state.Blobs.Put. Returns a 3-segment SnapshotRef:
// <state-blobs-ref>@<image-ref>@<base64-json-of-cmd-entrypoint>
// so Restore can ContainerCreate against the source image AND faithfully
// re-apply the effective Cmd/Entrypoint (a Restore→Snapshot loop
// preserves runtime config across the round-trip).
//
// Peak memory: bounded by snapshotMaxBlobBytes at state.Blobs.Put time
// (the compressed blob lives in memory for the Put call; the streaming
// gzip+tar intermediate buffers stay at ~64 KiB throughout the build).
//
// If the gzip-compressed diff exceeds the configured cap (default 256
// MiB), returns *ErrSnapshotTooLarge — the engine routes this as
// permanent_failure.
Snapshot(ctx context.Context, h Handle) (SnapshotRef, error)
// Restore rebuilds a handle from a prior Snapshot. The name argument is the
// IR-declared container name (spec §3); the returned Handle.Name is set to
// it so the dispatcher's d.Handles[name] keys consistently across
// pre-snapshot Create and post-resume Restore.
//
// Phase 2 fake: returns ErrUnsupported.
//
// Phase 4 Docker (slice 4.4): parses the 3-segment SnapshotRef, Blobs.Get
// the diff-tar, ContainerCreate against the embedded image + Cmd +
// Entrypoint, stream-CopyToContainer the data entries via io.Pipe (peak
// memory ~64 KiB + the diff blob bytes already in RAM from Get),
// Exec "rm -rf -- '<path>'" for each .awf-deletes entry. waitReady runs
// on the restored container so the spec §3 readiness contract holds.
// The embedded image is NOT auto-pulled; callers responsible for prior
// ImagePull (same as Backend.Create's image-mode path).
Restore(ctx context.Context, ref SnapshotRef, name string) (Handle, error)
// Destroy releases the handle's resources. The caller must call Destroy
// exactly once per Create; a second call returns an error (the handle is
// gone). Matches os.File.Close and Docker's ContainerRemove behavior;
// io.Closer itself leaves post-first-call behavior undefined, but the
// engine relies on the stricter "error on double" guarantee to surface
// dispatcher bugs.
Destroy(ctx context.Context, h Handle) error
}
Backend executes commands inside long-lived containers. One concrete impl per phase: in-memory Fake (Phase 2, fake.go); Docker (Phase 4, docker.go).
Lifecycle: Create on first reference for a declared container name, Exec / CaptureFiles many times across the run, Snapshot / Restore only for snapshot:workspace containers (Phase 4+; Phase 2 fake errors with ErrUnsupported), Destroy at run end or cancellation. The engine treats Handles as opaque; only the Backend that produced one may consume it.
Concurrency: single-writer per instance in Phase 2 (matches state.InMemoryLog/InMemoryBlobs precedent; the interpreter is single- threaded for commits — runtime-design §5). Phase 3's `parallel` will need per-method synchronization on the fake — added when that slice lands, not pre-emptively here (CLAUDE.md rule 2: simplest solution first).
type Caps ¶
type Caps struct {
Snapshot SnapshotMode
// RuntimeImage reports whether the backend can honor a map's runtime-
// resolved per-element image: (P6a) — i.e. actually boot the rendered
// image and report its content digest. Zero-value false FAILS CLOSED:
// a backend that ignores image: (native) advertises false, and the CLI
// guard rejects a runtime-image workflow on it rather than silently
// running bodies on the host.
RuntimeImage bool
// RuntimeCompose reports whether the backend can promote compose bytes
// generated during a workflow run into a managed project with normal
// lifecycle ownership. Zero-value false FAILS CLOSED: native cannot honor
// compose projects and must be rejected before execution.
RuntimeCompose bool
// StagingRoot is the directory under which the engine stages reduce
// manifests, branch artifacts, and react tool-call args files inside
// the step's container (or workdir, for native). Docker value:
// "/work/.awf" (an absolute in-container path). Native value: ".awf"
// (workdir-relative — native.CopyTo joins relative paths to the
// container's workdir). The engine exposes this to the reducer's shell
// via the AWF_STAGING_ROOT env var so the author's run: can reference
// staged files portably.
StagingRoot string
}
Caps reports a backend's capabilities. Backends MUST explicitly construct (e.g. Caps{Snapshot: SnapshotNone}) — zero-value Caps has Snapshot == "" so a missing assignment is observable in tests rather than silently advertising "no snapshot."
type CapturedFile ¶
CapturedFile is one path + its contents, as returned by CaptureFiles. The slice 2.4 dispatcher passes them up; the interpreter Puts each Content into Blobs at the commit boundary and records Path → ref in engine.NodeCompletedData.Files.
type Cmd ¶
Cmd describes a command to Exec. Run is the shell command (from CodeStep.Run after template substitution). Env carries the dispatcher-injected env vars (slice 2.4: AWF_IDEMPOTENCY_KEY; slice 4.2: AWF_OUTPUT when output_schema is declared); the Phase 2 fake accepts Env but does not use it (its scripted result table is keyed on Run alone — slice 2.4 verifies dispatcher env injection by inspecting what it passes to Backend.Exec, not by what the fake does with the env it receives).
Shell-quoting note: Run is interpreted as a single string passed to `sh -c` (POSIX baseline) — slice 4.2 implementation in container/docker/exec.go. Authors needing bash-specific features (`<(...)`, `[[ ]]`, double-bracket regex) ship bash in their image and write `bash -c '...'` as the inner script. An author who templates an untrusted agent-controlled value into Run MUST quote the substitution: `./scan.sh "{{ step.x.url }}"` — an unquoted `$(...)` / backtick / `;` in the typed value becomes an unintended command. Phase 4 may add a parallel `Argv []string` field for shell-free exec if a workload requires it; Phase 2 takes the simpler path and documents the contract.
type ContainerResources ¶
ContainerResources mirrors the IR Container.Resources fields (spec §3) for transport to the Backend. Image-mode only — for compose-mode, per-service resources are declared in the compose file.
type ContainerSpec ¶
type ContainerSpec struct {
Name string
// Image-mode fields.
Image string
Resources *ContainerResources
// Cmd is an optional override for the image's CMD instruction. When
// nil or empty, the image's default Cmd applies. Slice 4.4 adds this
// field so test fixtures can inject a long-running entrypoint into
// short-CMD images (e.g., alpine's /bin/sh → sleep infinity) without
// bypassing Backend.Create. Populated from ir.Container.Cmd by
// engine.ContainerSpecFor (image-mode arm).
Cmd []string
// DisableKeepalive opts out of keepalive injection for this container.
// Zero value (false) means "inject keepalive if needed" — the safe
// default that preserves existing behaviour for all callers (tests,
// map P6a, etc.) that construct ContainerSpec without setting this
// field. Set to true only when the IR Container explicitly sets
// keepalive: false. The docker backend (Task 3) reads !DisableKeepalive
// to decide whether to inject a long-running CMD.
DisableKeepalive bool
// PullIfAbsent requests that Create ensure Image is present in the local
// cache before starting it — image-mode only (P6a). The engine sets it
// solely for a map's runtime-resolved per-element image: (engine/map.go),
// where the image is learned at dispatch and is generally not pre-pulled.
// Statically-declared containers leave it false: they are pre-provisioned
// from a validator-pinned (digest) image, so the run-start pre-pull suffices.
//
// Because a runtime-resolved image cannot be validator-pinned, a backend
// honoring this flag MUST require Image to be digest-pinned (name@sha256:…)
// so the booted bytes are content-addressed and resume-reproducible, and
// MUST report the booted digest on Handle.ResolvedImageDigest. The Docker
// backend implements this; backends that ignore image: (native) advertise
// Caps.RuntimeImage=false and are rejected by the CLI guard before dispatch.
PullIfAbsent bool
// Compose-mode fields (slice 4.3).
Compose []byte
ComposePath string
Service string
}
ContainerSpec describes a container the engine wants Created. The Backend dispatches on which mode-specific fields are populated:
- Image-mode (slice 4.1): Image required, Resources optional, Cmd optional, Compose nil.
- Compose-mode (slice 4.3): Compose+ComposePath+Service required, Image empty, Resources/Cmd nil (per-service config lives in the compose file).
The Phase 2 fake ignores every field except Name.
type ExecResult ¶
type ExecResult struct {
ExitCode int
AWFOutput []byte
Stdout []byte
// Err (slice 5.3) surfaces transport-class errors that occurred AFTER
// Backend.Exec successfully launched the process — stdcopy mid-stream
// failure (docker), pipe-read failure (native), ctx-cancel during
// in-flight read. nil on the happy path. Pre-slice-5.3 these came back
// via Backend.Exec's err return; the streaming refactor moved them
// here so the result channel can carry them without the function
// returning early before chunks drain.
Err error
}
ExecResult is the streamed result of an Exec call (slice 5.3: delivered via Backend.Exec's result channel). Slice 2.4's outcome classifier reads ExitCode (per §6 — nonzero outside non_retryable_exit_codes is retryable_failure; in the list is permanent_failure); the dispatcher parses AWFOutput against the step's output_schema for typed outputs; Stdout is exposed via {{ step.<id>.stdout }} substitution.
Stderr is intentionally NOT a field — awf-workflow(5) (Code step) lists only exit_code and stdout as implicit outputs; the templating section never references stderr. The live tap (slice 2.5) and node.failed events (slice 2.4) get stderr via IOChunk{Stream: "stderr"} on the chunk channel.
type Fake ¶
type Fake struct {
// Calls is the defensive-copied history of every Cmd this fake's Exec
// received. Slice 2.4 (and later) tests inspect this to verify the
// dispatcher's env-injection contract (AWF_IDEMPOTENCY_KEY per AWF §10).
// The fake's doc-comment anticipated this need at slice-2.2 time but
// shipped no mechanism; this is the recording slot.
Calls []Cmd
// CreateSpecs records every ContainerSpec passed to Create, in order
// (test assertion aid — mirrors Calls for Exec). The P6a wiring test reads
// PullIfAbsent / Image off it to verify the engine flags a map's
// runtime-resolved image spec for the backend to pull. Shallow copies: the
// recorded scalars (Name/Image/PullIfAbsent) are what tests assert on.
CreateSpecs []ContainerSpec
// ExecHandles records the Handle value passed to each Exec call. Tests use
// this to assert compose service overrides (`container: lab:api`) reached
// the backend as a handle-level Service override.
ExecHandles []Handle
// DestroyCalls records every Handle passed to Destroy, in order.
DestroyCalls []Handle
// RestoreCalls records every Restore invocation, in order (test assertion
// aid — mirrors Calls for Exec).
RestoreCalls []RestoreCall
// WriteFileAtCalls records every WriteFileAt invocation, in order (test
// assertion aid — mirrors RestoreCalls).
WriteFileAtCalls []WriteFileAtCall
// WriteTreeAtCalls records every WriteTreeAt invocation, in order (test
// assertion aid — mirrors WriteFileAtCalls; used to observe session-subtree
// restores).
WriteTreeAtCalls []WriteTreeAtCall
// contains filtered or unexported fields
}
Fake is the in-memory Backend used by Phase 2 engine tests and the conformance suite (slice 2.6). Deterministic: monotonic-counter handle IDs, no time.Now, no OS-level process spawning, no goroutines.
Thread-safe (Phase 3 slice 3.2): per-method mutex protects handles / execTable / streamTable / Calls / counters so parallel branch goroutines dispatching to distinct containers (all backed by the same *Fake via the harness's Handles map) can race-cleanly.
Phase 4 Docker impl will live alongside this in container/docker.go; backendtest.RunBasicContract runs against both unchanged.
func NewFake ¶
func NewFake() *Fake
NewFake mints an empty Fake. Returns *Fake (not Backend) so test callers can reach helpers like ProgramExec / WriteFile / FailExecAfterN that the Backend interface deliberately doesn't expose.
func (*Fake) BlockExec ¶
func (f *Fake) BlockExec() chan struct{}
BlockExec arms the block gate: the NEXT Exec call (and all subsequent calls until ReleaseBlockedExec is called) will block inside Exec until released. Returns the gate channel — callers may also select on it directly if needed. Used by signal conformance tests (Bucket 8) to hold the engine at its first step dispatch so the pollControls goroutine can detect a pre-written pause/cancel file before any step completes.
Call ReleaseBlockedExec to unblock all waiting Exec calls. If the engine's ctx is cancelled while blocked (e.g. by the poller detecting pause/cancel), Exec returns ctx.Err — the caller does NOT need to release the gate.
func (*Fake) Capabilities ¶
func (*Fake) CaptureFiles ¶
CaptureFiles reads each path from the handle's in-mem fs and returns the content in input order. Missing-path errors the whole call; unknown handle is a hard error. Defensive-copies content (matches state.InMemoryBlobs.Get discipline — callers mutating their slice must not corrupt our store).
func (*Fake) ClearFault ¶
func (f *Fake) ClearFault()
ClearFault resets BOTH fault hooks (Exec + CaptureFiles). The conformance harness uses this between bucket runs that share a fake instance — though the standard pattern is to factory() a fresh fake per run/resume (matching the "infra rebuilt from recipe" spec §8 semantic), ClearFault is the in-place reset used when the same fake survives a crash-resume boundary in the conformance harness's atomic-commit bucket.
func (*Fake) CopyTo ¶
CopyTo writes each InputFile into the handle's in-mem fs, defensive-copying content. Unknown handle is a hard error; len==0 is a no-op. Thread-safe.
func (*Fake) Exec ¶
func (f *Fake) Exec(ctx context.Context, h Handle, cmd Cmd) (<-chan IOChunk, <-chan ExecResult, error)
Exec returns the ExecResult programmed for cmd.Run (via ProgramExec). The streaming contract (slice 5.3): returns two channels. chunks is buffered with every programmed IOChunk and pre-closed; result is 1-buffered with the programmed ExecResult and pre-closed. chunks closes BEFORE result emits (the Fake's deterministic-burst semantic — every chunk is materialized before the result is observable).
Unprogrammed cmd.Run is a hard error — silent zero-value would mask dispatcher bugs in slice 2.4. Unknown handle is a hard error. Cmd.Env is accepted but not used (slice 2.4 verifies dispatcher env injection by inspecting what it passes to Exec, not by what the fake does with it).
On any error return, both channels are nil — the new Backend contract requires callers to check err before ranging or receiving.
func (*Fake) FailCaptureAfterN ¶
FailCaptureAfterN is the CaptureFiles analogue — same one-shot semantic, same guarantees. Crashes BEFORE any blob is written (CaptureFiles runs before Blobs.Put), so resume simply re-runs the step. Useful in slice 2.6 bucket-2 (replay) to simulate a crash mid-execution before state is committed. Bucket-3 (atomic commit) is the job of FailAppendAfterN on state.InMemoryLog, which crashes between Blobs.Put and Log.Append(node.completed).
func (*Fake) FailCreateConfigForImage ¶
FailCreateConfigForImage makes Create return a PLAIN (non-ImageUnavailable) error for a spec.Image — models a deterministic DEFINITION fault (e.g. a malformed resources: the backend rejects) that the engine must surface by failing the WHOLE map as permanent_failure, NOT tolerate under min_success (contrast FailCreateForImage, the tolerated availability failure). Lazily allocates the set.
func (*Fake) FailCreateForImage ¶
FailCreateForImage makes Create return a *container.ImageUnavailableError for a spec.Image — models a valid runtime image that can't be pulled/booted (the SOLE tolerated per-element Create failure, P6a). Lazily allocates the set.
func (*Fake) FailExecAfterN ¶
FailExecAfterN configures the fake so the first n Exec calls succeed and the (n+1)-th fails with an "induced fault" error. FailExecAfterN(0) fails the very first call. One-shot: call #(n+1) and beyond succeed normally — matches slice 2.6's bucket-3 use (a single crash per test process; the process exits and resumes fresh).
The check sits in Exec itself, so calling this method has no retroactive effect on already-completed calls — the count is the live counter at the next call.
func (*Fake) ProgramExec ¶
func (f *Fake) ProgramExec(run string, result ExecResult, chunks []IOChunk)
ProgramExec queues a result + optional stream chunks for a Cmd.Run. Calling Exec with that Cmd.Run returns the queued result and a closed channel pre-filled with the chunks. Test helper, NOT on the Backend interface.
Defensive-copies every byte slice (result.AWFOutput, result.Stdout, each chunk's Data) so a caller mutating its slices after ProgramExec returns cannot corrupt the fake. Matches state.InMemoryBlobs.Put's discipline.
func (*Fake) ProgramExecAny ¶
func (f *Fake) ProgramExecAny(result ExecResult, chunks []IOChunk)
ProgramExecAny is the "match any Cmd.Run" variant of ProgramExec, used by tests where the Cmd.Run is built by the caller and the test only cares about the response (e.g., agent/claude.Launch_test, where the assembled command line is the test SUBJECT, not the lookup key).
The fake keeps a single "any" programmed entry; subsequent calls overwrite. Use ProgramExec when you want exact-match lookup. The fall-through in Exec consults anyExec only AFTER execTable[cmd.Run] misses, so ProgramExec entries still win when both are programmed.
Defensive-copies bytes per the same discipline as ProgramExec.
func (*Fake) ProgramExecWithFiles ¶
func (f *Fake) ProgramExecWithFiles(run string, result ExecResult, chunks []IOChunk, files map[string][]byte)
ProgramExecWithFiles is ProgramExec plus the files the programmed command WRITES into the executing handle's fs when it runs — simulating output_files production on the scripted fake (SP1 Task 8a). The conformance harness creates handles internally with no seed hook, so a producer step makes its own artifact via this affordance (the written files are then captured by the dispatcher's post-Exec CaptureFiles). Defensive-copies the map and every byte slice. Existing ProgramExec callers are unaffected (this is a new method).
func (*Fake) ProgramImageDigest ¶
ProgramImageDigest maps a spec.Image value to the resolved content digest the next Create against it returns on Handle.ResolvedImageDigest (P6a test helper; not on the Backend interface). Lazily allocates the table.
func (*Fake) ReadFileAt ¶ added in v0.1.1
ReadFileAt implements container.Backend.
func (*Fake) ReadTreeAt ¶ added in v0.1.1
ReadTreeAt implements container.Backend. Returns the subtree rooted at dir as a gzip-tar (built via BuildTreeTar). Entry paths in the archive are relative to dir. Returns an error when no files exist under dir (the caller cannot distinguish an empty directory from a missing one in the flat in-memory fs, so both are treated as absent).
func (*Fake) ReleaseBlockedExec ¶
func (f *Fake) ReleaseBlockedExec()
ReleaseBlockedExec unblocks all Exec calls currently waiting on the gate channel and disarms the block hook (future Exec calls proceed normally). Idempotent — safe to call even if BlockExec was not called or was already released.
func (*Fake) Restore ¶
Restore reads the serialized files from the injected Blobs store and creates a fresh handle preloaded with them; records the call in RestoreCalls. Without an injected store returns ErrUnsupported — the Phase 2 default. The restored handle is built exactly as Create builds one (monotonic-ID key into handles), so CaptureFiles works on it unchanged.
func (*Fake) Snapshot ¶
Snapshot serializes the handle's in-mem files to JSON and Puts them to the injected Blobs store, returning the CAS ref. Without an injected store (WithBlobs not called) returns ErrUnsupported — the Phase 2 default.
The real Docker backend captures a CoW *diff*; the fake serializes the whole file map. This exercises the capture→CAS→ref WIRING (slice 7.1's snapshot:workspace round-trip), not Docker's diff fidelity, which the Docker tests own.
func (*Fake) WithBlobs ¶
WithBlobs wires a CAS store so the fake can serialize a container's in-mem filesystem into Blobs on Snapshot and read it back on Restore — the durable path that survives the run→resume fake recreation (the conformance harness mints a fresh Fake for run and another for resume, but the same state.Blobs survives). Without it the fake advertises SnapshotNone and Snapshot/Restore return ErrUnsupported (preserving the Phase 2 default). Returns f for chaining.
func (*Fake) WriteFile ¶
WriteFile preloads a file into the handle's in-mem fs. Test helper for setting up CaptureFiles scenarios; not on the Backend interface (Phase 4 Docker would docker cp instead). Defensive-copies content on the way in.
func (*Fake) WriteFileAt ¶ added in v0.1.1
WriteFileAt implements container.Backend.
func (*Fake) WriteTreeAt ¶ added in v0.1.1
WriteTreeAt implements container.Backend. Extracts a gzip-tar (as produced by BuildTreeTar or ReadTreeAt) into the handle's in-memory fs under dir. Relative paths from the archive are joined to dir; existing files are overwritten. Enforces the standard TreeTarMaxBytes / TreeTarMaxEntries caps to guard against zip-bomb payloads.
type Handle ¶
type Handle struct {
Name string
ID string
Service string
// ResolvedImageDigest is the content digest of the image that actually
// booted (P6a) — set by Create when the image was runtime-resolved (a map's
// per-element image:). Empty for statically-pinned containers and backends
// that don't resolve a digest (native). The engine records it on the
// element's map.item commit so resume has a durable record of what booted.
ResolvedImageDigest string
}
Handle identifies a Created container. Treated as opaque by the engine; only the Backend that produced it may consume it.
Service is non-empty iff the handle is compose-shaped — it's the service the next Exec call will route to. The engine dispatcher (engine/local_dispatcher.go runCode) MAY override Service by cloning the Handle value before passing to Exec — this is the spec §3 `container: lab:db` cross-service form.
type IOChunk ¶
IOChunk is one stream slice produced during Exec. Phase 2 emits the chunks queued by the fake's ProgramExec; Phase 4 Docker forwards real stream frames. The Stream field is "stdout" or "stderr" — typed loosely so future streams (e.g. agent.event) can reuse the type without churn.
type ImageUnavailableError ¶
type ImageUnavailableError struct {
}
ImageUnavailableError, returned by Backend.Create, marks the SOLE Create failure a map TOLERATES per element (P6a): a valid, non-empty image reference that could not be made runnable — its pull failed, or its container would not start / become ready — plus a rendered reference that is not a `@sha256:` content digest (untrusted worklist run-data the format contract rejects per element, not the author's pinned definition). The engine routes it to a tolerated item_failed + ReasonImageUnavailable, counted against min_success.
Every OTHER Create error is a deterministic DEFINITION error — an invalid per-element spec the author wrote: a malformed `resources:` (e.g. mem: 4Gi the daemon/parse rejects), or a host config the daemon refuses — and fails the WHOLE map as permanent_failure. Backends MUST wrap ONLY genuine image- availability / boot faults in this type; an UNWRAPPED Create error is treated as a hard, map-failing config error. That fail-loud default is deliberate: it stops a broken definition from being silently laundered into a tolerated per-item skip (the bug that hid `mem: 4Gi` behind image_unavailable). Callers route with errors.As.
func (*ImageUnavailableError) Error ¶
func (e *ImageUnavailableError) Error() string
func (*ImageUnavailableError) Unwrap ¶
func (e *ImageUnavailableError) Unwrap() error
type InputFile ¶
InputFile is one in-container destination path + the bytes to write there, the symmetric input to CopyTo. The engine (the only Blobs reader) resolves an input_files ref to a CAS blob, Blobs.Get's the bytes, and passes them here. This method never touches state.Blobs (see pkg doc).
type RestoreCall ¶
type RestoreCall struct {
Name string
Ref SnapshotRef
}
RestoreCall records one Restore invocation (test assertion aid).
type SnapshotMode ¶
type SnapshotMode string
SnapshotMode is the snapshot capability a backend supports.
const ( // SnapshotNone — the backend has no snapshot facility. Phase 2 fake; any // non-snapshot:workspace Docker container in Phase 4. SnapshotNone SnapshotMode = "none" // SnapshotFSCoW — the backend can capture and restore a CoW filesystem // diff. Phase 4 Docker for snapshot:workspace containers. SnapshotFSCoW SnapshotMode = "fs-cow" // SnapshotFSArchive — the backend captures and restores a FULL gzip-tar // archive of the container workspace (no base image to diff against). The // native backend (host workdir). Caps is a self-description: this is NOT a // CoW diff, so it must not advertise SnapshotFSCoW. Behaviorally the engine // treats any non-SnapshotNone mode identically; this value exists to keep // the Caps honest, not to drive a code branch (YAGNI: do not add FSCoW-vs- // FSArchive switches in engine logic). SnapshotFSArchive SnapshotMode = "fs-archive" )
type SnapshotRef ¶
type SnapshotRef string
SnapshotRef is the opaque reference returned by Snapshot and consumed by Restore. Phase 2 fake never produces one (Snapshot returns ErrUnsupported); Phase 4 sets it to the CAS ref of the CoW diff.
type WorkdirResolver ¶ added in v0.1.1
type WorkdirResolver interface {
// ResolveWorkdirPath returns the absolute host path for rel (a
// workdir-relative staging path) under the handle's working directory.
// If the handle is unknown (defensive), rel is returned unchanged — callers
// must not rely on an error return; the method never panics.
ResolveWorkdirPath(h Handle, rel string) string
}
WorkdirResolver is an OPTIONAL interface implemented by backends whose StagingRoot is workdir-relative (native), so callers can resolve it to an absolute path for a live handle — e.g. to set an absolute CLAUDE_CONFIG_DIR.
Backends with an absolute StagingRoot (docker: "/work/.awf", fake: "/work/.awf") do NOT implement this interface; callers use the StagingRoot path as-is without any host-path resolution.
Usage: if wr, ok := b.(container.WorkdirResolver); ok { abs = wr.ResolveWorkdirPath(h, rel) }
type WriteFileAtCall ¶ added in v0.1.1
WriteFileAtCall records a WriteFileAt for test assertions (mirrors RestoreCall).
type WriteTreeAtCall ¶ added in v0.1.1
WriteTreeAtCall records a WriteTreeAt for test assertions (mirrors WriteFileAtCall). Content is the gzip-tar passed to WriteTreeAt.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package backendtest is the parameterized interface-conformance test for container.Backend.
|
Package backendtest is the parameterized interface-conformance test for container.Backend. |
|
Package docker implements container.Backend against the Docker Engine SDK.
|
Package docker implements container.Backend against the Docker Engine SDK. |
|
Package native implements container.Backend by running commands directly on the host via os/exec.
|
Package native implements container.Backend by running commands directly on the host via os/exec. |