Stage 1 Runtime Images¶
AgentGuard issue #310 Phase 1 prepares reviewed runtime-image inputs for the future Stage 1 live-study gateway and Codex CLI agent. Phase 1 does not approve or publish an image, create a live authorization, use a real credential, contact an inference API, or start the #294 live study.
Boundary¶
The gateway image is an application-level live-study egress component. It is not an absolute hostile-code sandbox and does not replace the Docker daemon, host kernel, or maintainer operational boundary. The agent container remains on an isolated study network and can leave only through the gateway path configured by #302/#303/#308.
Both images target Linux amd64 for Stage 1. Runtime execution is expected to
drop capabilities, set no-new-privileges, use a read-only root filesystem,
use bounded tmpfs writable paths, apply PID/memory/CPU limits, avoid Docker
sockets, avoid host namespaces, and avoid devices.
Build Inputs¶
Gateway:
- Dockerfile:
runtime-images/gateway/Dockerfile - Entrypoint:
/usr/bin/python3 /agentguard-live-egress-gateway - Base image:
gcr.io/distroless/python3-debian13:nonroot@sha256:339ecee37afe554f5237b9993250bc390ac9f1483a08c9b5e77ac4c016d6d39a - Base index digest:
sha256:774595d652a294b54c9bd575b2d9fdd1a4b47547dc17b8bfa4c0e953c64855b3 - UID/GID:
65532:65532
Codex agent:
- Dockerfile:
runtime-images/codex-agent/Dockerfile - Entrypoint:
/usr/local/bin/agentguard-codex-entrypoint - Final base image:
gcr.io/distroless/static-debian12:nonroot@sha256:52dcfbabb7457ea47c82f6e13af8c8a4a1d9f7b0145142b3ecab20f2b888411d - Final base index digest:
sha256:afa5c872c891853ca7fcf1f12c3edb23f7eeef36189728842dd51042ff57f7ab - Builder base image:
node:22.20.0-bookworm-slim@sha256:c385ec44d77c785e2364ac0c9b150809a0fdc17fde3dbf061e3dad07242c6a85 - Builder base index digest:
sha256:b21fe589dfbe5cc39365d0544b9be3f1f33f55f3c86c87a76ff65a02f8f5848e - UID/GID:
10001:10001 - Package:
@openai/codex - Version:
0.159.2 - Package integrity:
sha512-SE13C3nZCYoVL569BdegoOl6vwjb7o2sXOo7ivwVzaVoY0cswwi0/6pIE0TyO/C0vIkQh3jslExitET7PBTfIg== - Linux x64 package:
@openai/codex@0.159.2-linux-x64 - Linux x64 integrity:
sha512-RrCZ1X52wpa1lOsXtCtSyhjOFdQPh7LH5Ccv8HsKmd/2UXbUwxXFqWXFK3JzatquUNGtW/TLox5Y7qVOGkV0/Q== - Credential environment variable name:
CODEX_API_KEY
The official npm metadata for the package and Linux x64 alias must be re-verified during build review. Runtime npm/package installation and runtime update checks are not allowed. The final agent image must carry the verified Linux x64 Codex musl binary and required Codex helper binaries only; Node, npm, npm caches, the optional voice library tree, package managers, shells, and debug utilities are excluded from the final runtime image.
Mock-Only Validation¶
Phase 1 validation uses repository-owned mock endpoints and fake canaries only. It must not use a public inference endpoint, a real provider credential, or a billable call. Hosted Docker validation must prove the images build from the reviewed inputs, run as non-root, execute by local digest, and do not push.
The gateway evidence remains bounded and sanitized. Evidence must not contain URL queries, authorization headers, cookies, request bodies, prompts, model output, credentials, private paths, or unbounded logs. Missing, malformed, truncated, contradictory, or incomplete gateway evidence fails closed.
Usage Evidence¶
Usage evidence is accepted only when it is attributable to the exact trial and contains bounded measured token fields. Missing, malformed, conflicting, incomplete, or unattributable usage prevents the next trial. Estimated cost is not silently substituted for measured evidence; cost calculation later requires an explicitly frozen pricing-policy input bound by authorization.
If the exact CLI/protocol cannot provide reliable usage in hosted mock
validation, the status is USAGE_EVIDENCE_BLOCKED, images must not be
published, and #294 remains blocked.
Supply Chain Evidence¶
Phase 1 tooling records or validates:
- OCI image/layout identity;
- image config and manifest digest placeholders for local/CI artifacts;
- immutable base-image digests;
- source commit identity;
- Dockerfile/build-context digests;
- Codex package integrity evidence;
- SBOM, provenance, license, vulnerability, and secret-scan artifact digests;
- deterministic or explainably reproducible build metadata;
- vulnerability policy status; Phase 1 validation must not claim zero high/critical findings unless an actual reviewed scanner artifact produced that result.
Scanner failures must not be suppressed. Raw machine-readable scan artifacts are retained as bounded CI artifacts. Hosted scan summaries also retain the Trivy version, vulnerability database schema/version, database update or download timestamp, and a digest of the retained database metadata when the scanner cache provides it. Advisory-set drift must fail closed rather than pin an obsolete database.
Temporary Gateway Vulnerability Exceptions¶
The reviewed Phase 1 gateway image carries 31 HIGH Trivy occurrences that
deduplicate to 15 CVEs. The Codex agent image remains at zero HIGH and zero
CRITICAL findings. The gateway findings are inherited from the reviewed
gcr.io/distroless/python3-debian13:nonroot base image, and the reviewed
Debian package records did not report fixed package versions for those
findings at approval time.
The only accepted exceptions are recorded in
runtime-images/vulnerability-exceptions.json. They are temporary, exact
digest-bound records for gateway canonical identity
sha256:b30a59331f1b111b6620203d2e25ab8c1cd20a046559ee98d1018d0a5157ac4a
and expire on 2026-11-06. They do not apply to the agent image, any other
gateway canonical identity, any other base-image digest, fixable findings, CRITICAL
findings, new CVEs, changed package versions, or stale findings that no longer
appear in raw Trivy JSON. Any image, base, import, dependency, or runtime
behavior change requires reevaluation.
CVE-2026-19445 is accepted only for the exact observed Python 3.13 binary
packages at 3.13.5-2+deb13u5. Its reviewed vulnerable operation is
server-side ssl.SSLContext.sni_callback context switching; the gateway acts
only as an upstream TLS client with ssl.create_default_context().wrap_socket
and does not terminate TLS or install an SNI callback.
CVE-2026-77214 is accepted only for libexpat1 at
2.8.3-1~deb13u1. Its reviewed vulnerable path is Expat XML_ParseBuffer
after XML parser-buffer use; the gateway imports no XML parser, performs no XML
or content-type parsing, exposes no XML callback or buffer API, and forwards
provider payloads as bytes after destination authorization.
A narrower Python runtime or a compiled gateway remains the preferred longer-term remediation. Docker containment, even with non-root execution, read-only-root compatibility, dropped capabilities, and no-new-privileges, is application-level containment and must not be treated as an absolute hostile-code sandbox.
Phase 1 hosted validation also records a canonical runtime identity for each image. The canonical identity hashes the normalized exported root filesystem and runtime configuration that affects process execution, while excluding Docker creation timestamps, image history serialization, layer serialization digests, and non-runtime provenance labels that can change across otherwise equivalent builds. Hosted validation builds each image twice from the same inputs and fails closed unless both canonical identities, root filesystem digests, runtime configuration digests, SBOMs, and vulnerability findings match. The Docker local image ID and Docker-reported layer list remain recorded as diagnostic evidence but are not treated as the exception identity.
Freeze Manifest¶
agentguard.evaluation.runtime_images defines the strict versioned
agentguard.stage1-runtime-image-freeze manifest. Phase 1 may record local and
CI OCI digests plus publication placeholders. It must not claim that images are
published or registry-verifiable.
Final published references and registry manifest digests are filled and independently verified only in the separately approved Phase 2 publication.
Build-Once Publication Architecture¶
The prepared workflow is .github/workflows/stage1-runtime-images-publish.yml.
It is workflow_dispatch only. It has two jobs:
preparebuilds each image exactly once forlinux/amd64, validates the exact built images, exports the exact validated Docker archives, generates SBOM and raw Trivy evidence from those archives, applies the reviewed vulnerability exception policy, runs mock-only checks, and uploads the validated archives plus bounded evidence.publishdepends onprepare, downloads those exact archives, verifies their manifest/checksum/computed digests before registry authentication, loads the archives, pushes the already validated image subjects to GHCR, records immutable GHCR digests, rejects any digest recorded in the reviewed rejected-publication ledger, pulls by digest, recomputes canonical identities, regenerates SBOM and scan evidence, compares canonical SBOM package rows to the prepared evidence, reapplies the exception policy with explicit pulled-scan role bindings, runs the full local mock egress integration using digest references, and only then attests the approved published digest subjects.
The publish job must not run a Docker build, install packages into an image,
reconstruct an image from source, retag to a moving alias, or publish an
unreviewed platform. The checkout in the protected job is for validation code
only; the image payloads come from the downloaded validated archives.
Approval Stages And Activation¶
The workflow remains unusable for publication until a separate explicitly authorized settings task completes all repository settings. Dispatch requires:
- exact reviewed source commit input;
- approval phrase
APPROVE_STAGE1_RUNTIME_IMAGE_PUBLICATION; - trusted
mainbranch ref; required_source_commit == github.sha;- repository variable
STAGE1_RUNTIME_PUBLICATION_ENABLEDset exactly totrue; - successful
preparejob; - protected environment approval for environment
stage1-runtime-images.
Do not create or modify that environment or repository variable as part of
runtime-image code review. Before any dispatch, a separate settings task must
create/configure stage1-runtime-images, add the required reviewer, disable
administrator bypass if supported by repository policy, restrict deployment to
the trusted main branch, and enable STAGE1_RUNTIME_PUBLICATION_ENABLED
only for the approved publication window.
If GitHub repository settings cannot guarantee that a missing environment is protected before first use, operators must verify the environment and branch deployment protection out of band before dispatch. The activation variable is an additional fail-closed guard; it is not a substitute for environment protection.
As of this corrective prerequisite, the protected environment is configured and two publication attempts have run and failed post-push verification. The pushed digests from both attempts are recorded as rejected and are not approved for study use. The activation variable is disabled or absent, and any further publication attempt requires a new readiness review and a separate explicit authorization.
Image Coordinates And External State¶
Phase 2 publication creates externally visible GHCR package state only after
the protected publish job is approved. The intended coordinates are:
ghcr.io/richinmrudul/agentguard/stage1-gateway:<full-source-commit>ghcr.io/richinmrudul/agentguard/stage1-codex-agent:<full-source-commit>
The full source commit is the final selected trusted main commit at dispatch
time, not a shortened SHA and not a hardcoded historical merge commit. The
workflow must not create latest, release-channel, shortened-SHA, moving, or
unreviewed platform tags. Future Stage 1 study profiles must use immutable
@sha256: GHCR references resolved after publication, not tags.
The external state created by an authorized Phase 2 run is limited to the two GHCR image packages/tags, their immutable registry digests, registry-bound attestations for those digests, and retained GitHub Actions evidence artifacts. No provider credential, live authorization, paid call, public inference API contact, release, PyPI/TestPyPI publication, or live study is involved.
Provenance Subjects¶
Publication evidence records the source repository, exact commit, workflow file, run ID and attempt, target platform, Dockerfile and build-context digests, base-image references, Codex package version and integrity, archive digests, canonical identities, rootfs and runtime configuration digests, SBOM digests, normalized SBOM package-row digests, package counts, Trivy version and database metadata, raw scan digests and counts, exception-manifest digest and expiry, and mock integration status.
SBOM package-row evidence is normalized before hashing. Rows include stable package identity fields: package type/ecosystem, name, version, PURL when present, and normalized license identifiers when Syft provides them. Generated document IDs, timestamps, source paths, location metadata, and cataloger order are excluded. Rows are sorted deterministically and hashed as canonical JSON. Post-publication SBOM regeneration must produce the same row count and row digest for both images; equal counts alone are not sufficient.
GitHub attestations must bind the published GHCR subject names and immutable registry digests:
ghcr.io/richinmrudul/agentguard/stage1-gateway@sha256:...ghcr.io/richinmrudul/agentguard/stage1-codex-agent@sha256:...
The local Docker image ID, canonical identity, tag, archive digest, and SBOM digest are supporting evidence only. They are not the attestation subject. Attestations are created only after digest pullback, canonical identity verification, SBOM row comparison, role-bound Trivy policy enforcement, and the full pulled-by-digest mock integration have all passed. Provenance attestations for a failed digest prove where that digest came from; they do not mean AgentGuard approved the digest for study use.
Pulled-Scan Role Binding¶
Post-publication Trivy scans must not infer the gateway or agent security role
from a free-form scanner target string. The publish job writes a
machine-readable scan-bindings.json file after pullback scans complete. For
each image it binds:
- explicit role,
gatewayoragent; - exact GHCR repository;
- immutable published digest;
- pulled
repository@sha256:...reference; - prepared archive role and archive name;
- canonical identity from the publication manifest;
- exact pulled SBOM and Trivy evidence filenames.
The post-publication validator cross-checks those bindings against the
publication manifest and the resolved digest outputs before applying
role-specific vulnerability policy. Gateway findings may use only reviewed
gateway exceptions. Agent findings can never use gateway exceptions. Missing,
unknown, duplicated, swapped, tag-only, repository-mismatched, digest-mismatched,
or basename/substring-only bindings fail closed. Trivy ArtifactName and
Target formatting differences are tolerated only when they do not assert a
conflicting repository or digest.
Post-Publication Verification And Failure Behavior¶
After registry push, verification must pull both images by immutable digest, recompute canonical identities, require the reviewed gateway and agent canonical identities, regenerate SBOMs with the reviewed package counts and canonical package-row digests, rescan with the current Trivy database, reapply the exact exception policy, and run local-only Docker egress checks using the pulled digest references.
Before any GHCR login, image load, or push, the publish job performs a triple
archive check: the publication manifest must list exactly
gateway-image.tar and agent-image.tar; image-archives.SHA256SUMS must
list exactly those same two basenames; the manifest digests, checksum-file
digests, and freshly computed archive digests must all agree. Missing,
additional, duplicated, absolute, traversal, symlinked, malformed, oversized,
or role-swapped archive evidence fails closed.
The pulled-by-digest integration uses repository-controlled local Docker
networking only. It requires immutable @sha256: gateway and agent
references, verifies non-root users and read-only-root compatibility, runs an
approved local mock destination through the gateway, proves a disallowed
destination and metadata/private destination are denied, verifies a direct
bypass attempt cannot reach the mock provider, checks gateway crash detection,
cleanup, liveness, bounded sanitized evidence, and confirms a fake credential
canary is not retained. The local mock provider is on an internal Docker
network with a documentation-prefix subnet so public internet access and real
provider contact are not required.
Gateway evidence must be generated by the real gateway process during the
current pulled-by-digest test. The integration prepares the host evidence
directory for the gateway UID/GID 65532:65532 before starting the non-root,
read-only-root gateway container, without making the host path world-writable.
The resulting evidence file must exist, be owned by the gateway UID/GID, be
bounded, be newer than the current test start, contain valid gateway evidence,
include the approved and denied request decisions, exclude the fake credential
canary and private host paths, and persist after container cleanup. Stale,
empty, missing, synthetic, or harness-created evidence fails closed.
Safe failure diagnostics retain enough metadata to distinguish directory existence, ownership, mode, expected filename, bounded directory entries, container state, log digests, request completion, and cleanup state. They must not retain credential values, auth headers, cookies, request bodies, provider output, unrestricted environment data, or private host paths.
Any failure after push fails the workflow, retains evidence, prints the rejected immutable digest, and requires corrective review. Operators must not silently overwrite tags, rebuild and retry as if equivalent, delete evidence, reuse a rejected digest, begin #294 with the failed digest, or claim #310 complete. Automatic package deletion, tag rewriting, and destructive rollback are intentionally not implemented.
runtime-images/rejected-publications.json records immutable digests that were
pushed but rejected by post-publication verification. The first publication
attempt for source commit 26f9c74117185154f81438cdb33922067f92f10d created
and rejected:
ghcr.io/richinmrudul/agentguard/stage1-gateway@sha256:fbe729bf8768fba3c68e4f8ed7eff45eb34ab814745dd7a78a9a65e7e27000a0ghcr.io/richinmrudul/agentguard/stage1-codex-agent@sha256:07266c6012e9b5d36c295a0ea0d1a7570c97ccbe6332ff8e9d1da1911c496d94
Those digests remain eligible_for_study: false even if a later publication
succeeds. The second publication attempt for source commit
037aa62c0926e3210d8897b19d75819f5f73f692 created and rejected:
ghcr.io/richinmrudul/agentguard/stage1-gateway@sha256:8d8b30618ec0b9b9458f5f54269432e3b48ccf29dc005676a4f53e01bef0cbf4ghcr.io/richinmrudul/agentguard/stage1-codex-agent@sha256:337cf49d6283639ff6fdcd2839c4b7cc77e3a83e96c710567e1d1baf30f389da
That attempt passed canonical identity, SBOM row, explicit scan-role binding,
and vulnerability policy checks, then failed the full pulled-by-digest mock
integration with gateway evidence file missing. The root cause was the
integration harness creating the bind-mounted evidence directory with host
ownership and default 0755 permissions while running the gateway as
65532:65532 on a read-only root. The gateway could read policy.json and
serve requests, but it could not create gateway-evidence.json on shutdown.
The correction prepares the evidence directory for the exact gateway UID/GID
before container start and strengthens the evidence lifecycle assertion rather
than weakening it.
Future publication and study-freeze tooling must reject a candidate whose immutable digest appears in this file. No third publication attempt is allowed until this correction merges, a new read-only readiness review passes, and a separate explicit publication authorization is issued. This corrective prerequisite does not dispatch the publication workflow, mutate GHCR, or approve any digest.