Local runner container reference
Source-build foundation for v0.40.0, not a published image.
This reference uses the existing public runner contract
and terminal envelope. The default reference target
remains a scratch image for inline.mock-text only. The explicit
native-reference target adds a digest-pinned Debian/glibc runtime and supports
the exact-unit native Copilot text profile. It is tested with the actual bundled
CLI and an isolated synthetic provider; no live provider, paid request or
external network is used by that test. This is native execution evidence, not
inference-quality or provider assurance.
Issue #762 remains open until the
public image and pinned published-image acceptance run are verified.
There is no published v0.40.0 runner image or registry digest to copy today.
A manual-only GHCR publisher is defined in source, but has not been dispatched;
package creation permission and public visibility remain unverified. A separate
manual verifier is also prepared; it must anonymously pull the exact digest and
pass mock and bundled-native production-caller controls before that image is
accepted. Neither workflow has been dispatched.
No Lab dashboard change is required: this adds no new evidence or status surface.
Host and build requirements
Section titled “Host and build requirements”Use a reviewed source checkout with hydrated Git LFS objects, Docker Engine with BuildKit and Linux containers, and a POSIX shell. The source-built targets have been exercised on matching native Linux architectures; the CI gate specifically requires native Linux AMD64. Local ARM64 evidence is not AMD64 evidence. Windows-native containers, cross-platform emulation and alternate container engines are not established by this reference.
Builds need network access for the digest-pinned Node/Go/runtime base images,
Debian packages, locked npm dependencies and checksummed Go modules. The
native-reference build uses Debian Bookworm glibc and installs Bash, CA
certificates and Git for the bundled CLI’s runtime features. It embeds the real
dashboard and native CLI payloads, not compile stubs. The mock profile does not
need runtime extraction. Tests deny runtime networking and use only a local
synthetic HTTP provider; they do not supply credentials or contact a paid
provider. A real native provider may require network access and caller-managed
authentication when you run it.
If your network requires a public npm mirror, pass
--build-arg NPM_CONFIG_REGISTRY=https://your-mirror.example/npm/ to the build.
For the test command below, set NPM_CONFIG_REGISTRY to that same URL.
Do not pass credentials in build arguments; no private registry authentication
mechanism is provided.
From the repository root:
work=$(mktemp -d)cp examples/runner/unit.json "$work/request.json"docker build --file examples/runner/container/Dockerfile \ --iidfile "$work/image.id" .image=$(docker image inspect --format '{{.Id}}' "$(cat "$work/image.id")")docker image inspect "$image"
# Build the native-capable reference separately; the default remains scratch.docker build --file examples/runner/container/Dockerfile \ --target native-reference --iidfile "$work/native-image.id" .native_image=$(docker image inspect --format '{{.Id}}' "$(cat "$work/native-image.id")")docker image inspect "$native_image"The inspected image ID is sha256: followed by 64 hex digits. It is an
immutable local image identifier, not evidence of publication or a pullable
registry reference. Its meaning depends on Docker’s image store: Docker 29’s
containerd store can report an OCI index digest rather than a configuration
digest. Neither --iidfile nor .Id is a published-image acceptance result.
The build does not push, tag a release or publish anything. Keep the work
directory until you have inspected its outputs.
The native-reference image is configured as non-root UID/GID 65532:65532
with HOME=/home/waza, COPILOT_HOME=/home/waza/.copilot and TMPDIR=/tmp.
The bundled runtime extracts executable files under COPILOT_HOME, so that
directory and temporary storage must be writable and executable. With a
read-only root filesystem, provide them explicitly:
docker run --rm --pull=never --network=none --read-only \ --cap-drop=ALL --security-opt=no-new-privileges --pids-limit=64 \ --memory=2g --cpus=1 \ --tmpfs /tmp:rw,exec,nosuid,nodev,size=1g,mode=1777 \ --tmpfs /home/waza/.copilot:rw,exec,nosuid,nodev,size=1g,uid=65532,gid=65532,mode=700 \ "$native_image" runner capabilitiesOne GiB was exercised for the current bundled CLI; it is not a measured minimum.
Use a persistent writable executable mount at COPILOT_HOME if retaining the
downloaded bundle is desired. These settings provide runtime storage, not a
container sandbox or hermetic build.
Execute and inspect local outputs
Section titled “Execute and inspect local outputs”docker run --rm --pull=never --network=none --read-only \ --cap-drop=ALL --security-opt=no-new-privileges --pids-limit=64 \ --memory=256m --cpus=1 \ --tmpfs /tmp:rw,noexec,nosuid,size=32m,mode=1777 \ -i "$image" runner run \ < "$work/request.json" > "$work/terminal.json" 2> "$work/stderr.log"code=$?printf 'Container exit: %s\n' "$code"cat "$work/terminal.json"The clean request selects exactly
{"shard":"shard-a","task":"selected","trial":2,"attempt":2}.
Its eval permits three trials and attempts, but these are selectors, not loops.
The captured adjacent task glob is never expanded. The only task is the inline
captured task; no neighboring source files are supplied to the container.
Expect exit 0, empty stderr, and exactly one JSON envelope with
kind: "waza.runner-terminal", schema_version: "1.0", outcome: "success",
exit_code: 0, engine_calls: 1, validation: "valid" and
failure_boundary: "none". Both selection and observation.selection equal
the tuple above; observation has execution_mode: "mock", engine_calls: 1,
run_number: 2, attempts: 2, status: "passed" and one passed validation.
identity is input-derived lineage, not an isolation or delivery attestation.
The producer validates the terminal schema before its single emission.
The host shell writes terminal.json and stderr.log; the container writes
only ephemeral temporary workspace data. There is no mounted output directory,
archive, upload, ingestion service or scheduler. Docker’s own launch errors can
produce stderr and no envelope (typically exit 125); do not interpret those as
Waza verdicts. Redirection is neither exclusive creation nor atomic/durable
publication: use a fresh work directory, never overwrite valuable outputs, and
retain partial bytes on failure for diagnosis. A successful write is not proof
that another process received the result.
Smoke and production-boundary controls
Section titled “Smoke and production-boundary controls”With Node.js 24 available on the host, run:
node --test examples/runner/container/test.mjsThis builds the image, resolves its local ID, starts actual Linux containers and uses the production CLI, admission, mock engine and terminal producer. It checks capability discovery, zero-call offline validation, exact 2/2 and alternative 3/3 selection, one failed text check (exit 1), wrong shard/task, out-of-range trial/attempt, external prompt-file and malformed JSON rejection (exit 6), and unavailable native capability even with paid consent (exit 5). Rejected requests have no observation or invented execution identity. Fresh host input/output files round-trip the real envelope; no host files or credentials are mounted. The test prints the actual local image ID and cleans only its own temporary directory. It leaves the built image in Docker’s local store.
Offline SDK subprocess and bundled-runtime controls
Section titled “Offline SDK subprocess and bundled-runtime controls”node --test examples/runner/container/native-test.mjsThe same Dockerfile has explicit test-only targets. synthetic-test adds a
deterministic stdio peer to the scratch image and selects it using the supported
COPILOT_CLI_PATH override. The real built waza runner run consumes derivatives
of examples/runner/copilot-unit.json, including its explicit paid-consent field.
Every invocation nevertheless has --network=none, fresh tmpfs configuration
directories and no credentials/provider settings. Only an empty, invocation-owned
RPC output directory is mounted; no input/configuration or credentials are mounted. The peer does
not perform inference. Its text, authentication and usage responses are synthetic.
The host reads only its test RPC log after the container stops.
The controls compare emitted identities with offline admission, exact 2/2
selection, one create/send, text verdicts, complete RPC inventories, startup
rejection, protocol mismatch, execution errors, timeout and unsupported-capability
rejection. They also require explicit context-discovery/file-hook/store/git/memory
opt-outs and session.options.update before sending. An update rejection must
produce infrastructure failure without a prompt. Declared eval metrics remain
uncomputed identity metadata; success does not assert aggregate thresholds.
The default scratch reference is separately required to report a
native startup failure with zero engine calls; synthetic success does not make
that image native-ready.
native-smoke uses a digest-pinned Alpine base and the existing bundled-runtime
test. It checks actual native ping/status/session create/get-messages/detach/delete
with a local rejecting provider and zero provider requests, without sending
a prompt. This establishes only that bounded lifecycle on the executed Linux
architecture. It does not prove inference, credentials, billing, provider quality
or general native workflow support.
The scratch image supplies no dynamic loader or runtime dependencies. The bundle
can extract successfully yet fail fork/exec with “no such file or directory”
even when its executable exists. Alpine supplies the musl loader and ldd,
which the SDK uses to select the musl bundle. Native extraction additionally
needs a writable, executable cache/temp area: 256 MiB proved insufficient for
CLI 1.0.101 assets; the smoke control uses 1 GiB, not a measured minimum. The
mock walkthrough’s 32 MiB noexec tmpfs is intentionally not reused.
Native-capable reference controls
Section titled “Native-capable reference controls”node --test examples/runner/container/native-bundle-test.mjsThis builds the native-reference target and a separate test-only
native-bundle-test target. It verifies the native reference’s mock path, then
executes /waza runner run from the Debian-based target with the actual bundled
Copilot CLI. A loopback OpenAI-compatible provider runs inside the same
--network=none container and returns deterministic synthetic text and usage.
The controls assert one exact selected unit and one provider request, deny
ambient instruction/configuration discovery and hooks on the runner path, and
prove those canaries activate in a separate positive SDK control. They also
exercise timeout, a stopped native child, SIGTERM cancellation, terminal exits,
native-process reaping and cleanup. The bind mount contains only two
synthetic-evidence files; they are written by the non-root container and checked
for cross-UID host readability. No checkout, credentials or host environment is
mounted into the test container.
The script requires the Docker daemon architecture to match the image. Docker CI additionally requires Linux AMD64 and rejects cross-architecture execution; the architecture printed by a local run describes only that local run. These controls demonstrate current-bundle startup and a synthetic native text turn, not paid inference, real-provider behavior, billing assurance or model quality. The native targets and their local image IDs are not published; they do not complete #762’s immutable public-manifest acceptance gate.
Verify an authorized public digest
Section titled “Verify an authorized public digest”After a separately authorized publisher run, confirm the package is actually
public in GHCR; the first push may create a private package. Then run the
manual-only Verify Published Runner Reference workflow with the source
revision and sha256:... digest from the publisher summary. Its isolated
provenance job has contents: read, attestations: read and actions: read; the production
container-test job has only contents: read. Neither has package permission or
logs in to GHCR. It uses a fresh, empty Docker configuration for an anonymous
pull, so a private package fails the gate rather than silently using credentials.
The workflow must be dispatched from the current main tip, and the image
source revision must still be reachable from current main. On a native Linux
AMD64 runner, it checks that the immutable registry reference resolves to the
expected source/revision labels and only the claimed runtime
platform, and verifies the signed provenance for that digest and publisher
workflow. It also checks the exact publisher run and attempt recorded in the
verified provenance and requires successful publication/finalization steps.
A signed digest left by a failed, cancelled or incomplete publisher is rejected.
It then runs the production CLI’s mock exact-unit profile and the
actual bundled Copilot CLI against an in-container synthetic provider, with
network isolation and all six native lifecycle/discovery controls. The probe
binaries and fixture come from the validated source checkout and are mounted
read-only; /waza remains the binary from the pulled digest. Only a successful
verifier run proves anonymous access and caller behavior for that published
digest. A failed publisher finalization or verifier leaves the candidate
unaccepted; no rollback is implied.
Troubleshooting and limits
Section titled “Troubleshooting and limits”- Cannot connect to Docker: start the daemon/Desktop and select Linux containers.
- Build fails fetching dependencies: check network, registry access and LFS hydration; no offline build is claimed.
- Exit 6: compare the exact shard/task and one-based bounds; retain the inline mock/text profile rather than dropping unsupported semantics.
- Exit 5: negotiate currently advertised capabilities; native consent cannot activate an absent capability.
- Exit 1: inspect the actual false verdict, not an infrastructure retry.
- Empty/partial terminal output: inspect stderr and Docker’s exit; process loss, output failure and SIGKILL cannot guarantee an envelope.
The non-root user, read-only filesystem, network denial, resource bounds and dropped capabilities reduce exposure but do not establish strong isolation. The image contains the full Waza binary and embedded assets, not a sandboxed mock-only binary. Do not mount secrets or run untrusted native workloads. There is no scheduler integration, automatic retry or exactly-once delivery. See the maintainer procedure for pinning and update limitations.