diff --git a/.github/actions/deploy-core/tests/check-image-pin.test.sh b/.github/actions/deploy-core/tests/check-image-pin.test.sh new file mode 100755 index 00000000..fac43bbc --- /dev/null +++ b/.github/actions/deploy-core/tests/check-image-pin.test.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Unit tests for the build-container digest-pin validator (spec §3.6/§5). The +# validator must accept a sha256-digest-pinned image.json and FAIL CLOSED on a +# tag, a missing digest, or malformed JSON. +set -euo pipefail + +DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +CHECK="$DIR/../../../docker/build-app-cli/check-image-pin.sh" +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT + +pass=0 +fail=0 +ok() { + printf ' \033[32mok\033[0m %s\n' "$1" + pass=$((pass + 1)) +} +no() { + printf ' \033[31mFAIL\033[0m %s\n' "$1" + fail=$((fail + 1)) +} +run() { bash "$CHECK" "$1" >/dev/null 2>&1; } + +echo "== build container image.json digest-pin validator ==" + +printf '{"repository":"ghcr.io/stackpop/edgezero-build-app-cli","tag":"v1","digest":"sha256:%064d"}\n' 0 >"$WORK/ok.json" +if run "$WORK/ok.json"; then ok "a digest-pinned reference passes"; else no "a digest-pinned reference passes"; fi + +printf '{"repository":"ghcr.io/stackpop/edgezero-build-app-cli","tag":"v1","digest":"v1"}\n' >"$WORK/tag.json" +if run "$WORK/tag.json"; then no "a non-digest (tag) reference is rejected"; else ok "a non-digest (tag) reference is rejected"; fi + +printf '{"repository":"ghcr.io/stackpop/edgezero-build-app-cli","tag":"v1","digest":"sha256:deadbeef"}\n' >"$WORK/short.json" +if run "$WORK/short.json"; then no "a short/invalid digest is rejected"; else ok "a short/invalid digest is rejected"; fi + +printf '{"repository":"ghcr.io/stackpop/edgezero-build-app-cli","tag":"v1"}\n' >"$WORK/nodigest.json" +if run "$WORK/nodigest.json"; then no "a missing digest is rejected"; else ok "a missing digest is rejected"; fi + +printf '{"tag":"v1","digest":"sha256:%064d"}\n' 0 >"$WORK/norepo.json" +if run "$WORK/norepo.json"; then no "a missing repository is rejected"; else ok "a missing repository is rejected"; fi + +printf '{"repository":"ghcr.io/attacker/edgezero-build-app-cli","tag":"v1","digest":"sha256:%064d"}\n' 0 >"$WORK/foreign.json" +if run "$WORK/foreign.json"; then no "a foreign repository is rejected"; else ok "a foreign repository is rejected"; fi + +printf '{"repository":123,"tag":1,"digest":"sha256:%064d"}\n' 0 >"$WORK/numeric.json" +if run "$WORK/numeric.json"; then no "numeric (non-string) repository/tag is rejected"; else ok "numeric (non-string) repository/tag is rejected"; fi + +printf 'not json\n' >"$WORK/bad.json" +if run "$WORK/bad.json"; then no "malformed JSON fails closed"; else ok "malformed JSON fails closed"; fi + +printf 'Passed: %d Failed: %d\n' "$pass" "$fail" +[ "$fail" -eq 0 ] diff --git a/.github/docker/build-app-cli/check-image-pin.sh b/.github/docker/build-app-cli/check-image-pin.sh new file mode 100755 index 00000000..3aacbb1d --- /dev/null +++ b/.github/docker/build-app-cli/check-image-pin.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Fail-closed: the build container reference must be the canonical EdgeZero GHCR +# repository, pinned by a sha256 manifest digest, never a mutable tag (spec +# docs/superpowers/specs/2026-08-20-edgezero-deploy-build-caching-design.md +# §3.6/§5). image.json records the canonical repository, tag, and pinned digest; +# the rest of the build-caching feature keys `platform-id` on that digest, so a +# non-digest, malformed, or foreign-repository pin must never pass. +# +# Usage: check-image-pin.sh +set -euo pipefail + +# The one repository the build-caching feature trusts; a pin naming any other +# repository is rejected so a foreign image can never become `platform-id`. +EXPECTED_REPO="ghcr.io/stackpop/edgezero-build-app-cli" + +file="${1:?usage: check-image-pin.sh }" + +if ! command -v jq >/dev/null 2>&1; then + echo "::error::check-image-pin.sh requires jq" >&2 + exit 2 +fi + +# FAIL CLOSED on unreadable JSON: a file jq cannot parse must be rejected, never +# silently passed. +if ! json=$(jq -e . "$file" 2>/dev/null); then + echo "::error::$file is not valid JSON — refusing to pass an unreadable image pin" >&2 + exit 1 +fi + +# Require string TYPES: `jq -r` would coerce a numeric repository/tag/digest to a +# string, so a `"repository": 123` would otherwise slip through. Check the JSON type. +if [[ "$(jq -r '.repository | type' <<<"$json")" != "string" || + "$(jq -r '.tag | type' <<<"$json")" != "string" || + "$(jq -r '.digest | type' <<<"$json")" != "string" ]]; then + echo "::error::$file 'repository', 'tag', and 'digest' must all be JSON strings" >&2 + exit 1 +fi + +repo=$(jq -r '.repository' <<<"$json") +tag=$(jq -r '.tag' <<<"$json") +digest=$(jq -r '.digest' <<<"$json") + +if [[ -z "$repo" || -z "$tag" ]]; then + echo "::error::$file must set a non-empty 'repository' and 'tag'" >&2 + exit 1 +fi + +# The repository must be the canonical EdgeZero build container, not merely +# non-empty: `platform-id` is trusted, so a foreign repository must never pass. +if [[ "$repo" != "$EXPECTED_REPO" ]]; then + echo "::error::$file 'repository' must be '$EXPECTED_REPO', not '$repo'" >&2 + exit 1 +fi + +# A sha256 manifest digest, never a tag. +if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then + echo "::error::$file 'digest' must be a sha256 manifest digest (sha256:<64-hex>), not a tag: '$digest'" >&2 + exit 1 +fi + +echo "build container reference is pinned: $repo@$digest" diff --git a/docs/specs/edgezero-deploy-build-caching.md b/docs/specs/edgezero-deploy-build-caching.md deleted file mode 100644 index 6dee7a89..00000000 --- a/docs/specs/edgezero-deploy-build-caching.md +++ /dev/null @@ -1,244 +0,0 @@ -# EdgeZero Deploy Actions — Build Caching Spec - -**Status:** Design (proposed) — v6.14 (sccache pivot) - -**Related:** `docs/specs/edgezero-deploy-github-action.md`, -`docs/specs/edgezero-deploy-action-implementation-plan.md`, -`docs/specs/edgezero-deploy-adoption-guide.md`, `docs/guide/deploy-github-actions.md` - -## 1. Problem - -`build-app-cli` compiles the application's CLI (native) with **no caching**, so every deploy -recompiles the whole dependency graph (~10 min for `stackpop/trusted-server-deployer`, which -checks out a **separate** application repo and builds its CLI). Caching must work for that -**cross-repository deployer** topology **and for real EdgeZero apps, whose crates are unpublished -git dependencies** (so a crates.io-only rule is unusable). - -## 2. Trust model and v1 shape - -- The build compiles **trusted code** (the deploy target); `build.rs` is trusted. -- Caching runs **only for authorized deployer events/refs (fail before compiling otherwise)**; - the runtime credential and the narrow app-checkout PAT are explicitly trusted. -- **The deployer owns and writes its repo-scoped cache**; every writer that can write the - deployer's **current-/default-branch** cache is trusted; the deployer's protected workflow - allowlists `app-repository`/`app-ref`. -- **The reusable workflow is the only SUPPORTED producer** (build + deploy in one **pinned - container**, §3.6). Provenance is a **consistency check, not producer authentication** (an - other-job archive can self-assert; attestation is §7). The direct composite is an internal `$/` - step only. -- **GitHub-hosted `linux/amd64` runners only** (no reliable ephemeral self-hosted predicate). - -## 3. Design - -### 3.1 Cache mechanism: fresh target + action-owned sccache (no `target/` pruner) - -Rather than caching and pruning `target/` (whose unit graph and intermediate layout Cargo treats -as **internal and unstable**), v1 uses **`sccache`** — the compiler cache Cargo itself recommends -for shared dependency acceleration: - -- **`CARGO_TARGET_DIR` is FRESH every run** (an action-owned path under `RUNNER_TEMP`, never - cached, never inside the checkout) — so there is no stale-`target/`, no source-in-target, no - workspace-crate-output, and no unit-graph classification problem. -- **`RUSTC_WRAPPER` is set (action-owned) to a pinned `sccache`** baked into the container. - `sccache` stores compiled rustc outputs in `SCCACHE_DIR` (an action-owned path), **keyed by the - content of the preprocessed source + compiler + flags**. Correctness is content-addressed: - restoring an older `SCCACHE_DIR` is always safe (a cached object is used only when its inputs - match), so there is no immutable-cache staleness and **no custom pruning**. `sccache` bounds its - own size (`SCCACHE_CACHE_SIZE`, LRU) — the cached directory is self-managing. -- **Cache contents = `SCCACHE_DIR` only** (compiled objects + sccache's index). **No `.crate` - sources, no `registry/src`, no `git/*`, no `CARGO_HOME/bin`, no config, no credentials** are - cached — so a cold build's `registry/src` extraction is irrelevant to the audit, and **no - dependency source is ever cached** (only compiled objects). Re-downloading crates each run is the - small remaining cost; caching `.crate` archives is §7. -- **Any dependency source is supported** (crates.io, the public **EdgeZero git repo** the generator - emits, other git deps) — sccache caches their compilation regardless of source. The old - crates.io-only restriction is **removed**; `cache: false` and `cache: true` resolve dependencies - identically (caching never changes resolution). - -### 3.2 Own restore + save, coarse rolling key - -`actions/cache/restore` + `save` over **`SCCACHE_DIR` only**: - -- **Key** = `edgezero-sccache-v1---`, restore-keys prefix - `edgezero-sccache-v1---`. `` is `github.run_id` (unique per - run), so each run **saves a fresh generation** (never colliding with an immutable prior entry) - and **restores the newest matching prefix**. `platform-id` = the container digest (which encodes - toolchain + ABI); `suffix-hash` = the validated `cache-key-suffix`. No lockfile/manifest hashing - is needed — sccache content-addresses internally. -- **Restore → audit → build → best-effort save.** After restore, **audit** that the restored path - is exactly `SCCACHE_DIR` and contains only sccache's blob/index layout (fail closed / **discard - and build cold once** on a corrupt or unexpected restore). After the build, `actions/cache/save` - under the run's `` key is **best-effort** (its failures are warnings). Bump the - `-v1-` namespace whenever the mechanism changes. - -### 3.3 Action-owned Cargo/sccache environment - -The build runs under a **constructed minimal environment** (`env -i` + an explicit allowlist), -not scrub-then-reject, so there is nothing to miss: only the action-owned variables and an -allowlist of benign ones exist. Action-owned (fixed, exact): `CARGO_HOME`, `CARGO_TARGET_DIR` -(fresh), `HOME`, `TMPDIR`, `SCCACHE_DIR`, `SCCACHE_CACHE_SIZE`, `RUSTC_WRAPPER=sccache`, -`RUSTUP_TOOLCHAIN`, `CARGO_ENCODED_RUSTFLAGS=""`, `CARGO_INCREMENTAL=0`. A **caller-supplied** -`RUSTC`/`RUSTC_WRAPPER`/`RUSTC_WORKSPACE_WRAPPER`/`RUSTDOC`/`RUSTFLAGS`/native-tool var simply is -**not present** in the constructed env (never inherited). The effective **Cargo config** over the -full chain (cwd → `/`, incl. the working directory, plus `CARGO_HOME`) must contain only benign -allowlisted keys (registry index URLs, `net.retry`, `http.timeout`/`check-revoke`); anything else -fails closed. Default-features-only; `Cargo.lock` must be a tracked, regular file. External path -deps outside the workspace root are rejected. Fixed internal container paths (`CARGO_HOME`, -`CARGO_TARGET_DIR`, `HOME`, writable `/tmp`). - -### 3.4 Identity - -`git-root` (path, confinement); `app-repo` (`owner/repo`); **`app-repo-id`** (canonical decimal -**string**, always required, **verified via the GitHub REST API to belong to `app-repository`**). -`workspace-root` canonicalized, confined beneath `git-root`, `working-directory` beneath it, -asserted `== cargo metadata.workspace_root`. `workspace-id` = hash(`app-repo-id`, workspace-root -rel `git-root`). `platform-id` = the container digest, **read inside every action from `image.json` -at the same EdgeZero SHA — never caller-supplied**; `container-ref` = `@`. - -### 3.5 Writer fidelity vs. source authorization - -Cache runs only on `push`/`workflow_dispatch`/`schedule` on a **protected deployer ref** with -`HEAD == resolved app SHA` (action fidelity); the deployer's protected workflow allowlists the app -identity, and every writer of the deployer's **current-/default-branch** cache scope is trusted -(deployer authorization). Normative in the guide. - -### 3.6 Container, runner, launcher - -- **Image:** EdgeZero-published, **public** (anonymous pull) + retained, single-manifest - `linux/amd64`, pinned by **manifest digest**, from a versioned in-repo Dockerfile baking the - pinned Rust toolchain, `wasm32-wasip1`, the pinned **`sccache`**, the pinned **Fastly CLI** - (`versions.json`), and `git jq tar curl cc`. Run **`--read-only`, non-root**, explicit writable - mounts only. `platform-id` = its digest. -- **Runner: GitHub-hosted `linux/amd64` only** (fail closed on self-hosted). Host-level job, local - Docker daemon. -- **One launcher `run-app-cli-in-container`** with **enumerated mounts** (never `RUNNER_TEMP` - wholesale): - - **Writable working COPY of the checkout.** The CLI runs arbitrary manifest commands via - `sh -c` in the manifest root and may create `dist/`, `node_modules/`, generated manifests, - etc. — so the working directory is a **disposable writable copy (or overlay)** of the app - checkout, not read-only source. The **read-only original** is used for the before/after source - checks (§3.7). (v1 alternative: prohibit manifest-command overrides; the writable overlay is - preferred.) - - **Other writable (specific):** `CARGO_TARGET_DIR`, `CARGO_HOME`, `SCCACHE_DIR`, a Fastly/ - provider `HOME`, a package/output dir. **Read-only:** the validated CLI binary, and — for - config-push — the **specific inline-config temp file** (by exact path). UID/GID mapping so the - non-root container user owns the mounts. - - **env:** only the required provider token + `EDGEZERO_*`; no GitHub file-command channels - inside the container. - - **signals/outputs:** host↔container readiness handshake; **`mutation-attempted` published - host-side to `$GITHUB_OUTPUT` before launching the mutating CLI**; named container + host-side - signal forwarding (`docker stop -t ` → `docker rm`) - - post-cancel reconciliation. - -### 3.7 Source freezing, provenance, disclosure, actions - -- **Source freezing:** on the **read-only original** checkout, assert the initial `HEAD` SHA - unchanged + tree clean (tracked + untracked + recursive submodules) **before and after** all - app-controlled commands (commands run in the writable copy); reject escaping symlinks. Consumers - additionally **verify their mounted checkout's repository id, `HEAD`, and workspace against the - artifact before and after commands**. -- **`ExpectedIdentity`:** `app-repo-id` (decimal string), `source-revision` (full SHA, explicit), - `app-cli-package`, `app-cli-bin`, `workspace-id` — **caller-supplied and checkout-verified**; - `platform-id`/`container-ref` are **derived inside every action from same-SHA `image.json`, not - accepted from the caller**. -- **Schema/canonicalization (normative, with golden vectors):** `app-cli-meta.json` is - **canonical JSON** — UTF-8, keys **lexicographically sorted at every level**, no duplicate keys - (a duplicate-key-rejecting parser is required; JSON Schema cannot do this), minimal number/string - forms — validated by a committed **JSON Schema 2020-12** file **plus** the procedural - canonical/dup-key pass. Numeric caps: meta ≤ **64 KiB**. Fields = `ExpectedIdentity` + - `app-cli-version` (informational) + `binary-sha256` + `binary-size` + `abi` - (`{ machine, interp, needed: [sorted str] }`). -- **Archive contract (normative):** a **`ustar`/`pax` tar** with **exactly two** regular members, - `app-cli-meta.json` then the `app-cli-bin` binary — **any extra/duplicate/renamed member, - symlink, hardlink, device, or path-traversal header is rejected**; total logical size ≤ **512 - MiB**, binary ≤ `binary-size`, meta ≤ 64 KiB; the extracted binary's sha256/size re-verified. -- **`validate-app-cli-provenance`** (fresh pinned container, minimal env): enforce the archive - contract; canonical-JSON + JSON-Schema validate; re-verify binary digest/size; **ABI loadability - proof** — recompute `PT_INTERP`, `DT_NEEDED`, and search paths from the binary, **resolve every - required library inside the immutable image**, then run a **credential-free, network-disabled - `--help` smoke**; compare every caller `ExpectedIdentity` field. Output `app-cli-path`. -- **`active-version-fastly`** — inputs: `artifact-tar`, `ExpectedIdentity`, `fastly-service-id`, - `fastly-api-token`; validates, runs `active-version` via the launcher; output `version` (empty on - a first-ever **production** deploy = success). **Recovery is PRODUCTION-only.** -- **`compute-app-cli-identity`** — inputs: `app-repository`/`app-repo-id`, `source-revision`, - `workspace-root`, `app-cli-package`/`app-cli-bin`; reads `platform-id`/`container-ref` from - same-SHA `image.json`; outputs the full `ExpectedIdentity`. -- **Disclosure (enforceable):** because the action cannot compare reader sets, require - **`disclosure-acknowledged: true` for every cross-repository build** (`app-repo-id` ≠ the deployer - repo id), **exempting only equal repository ids**. The sccache cache holds **compiled objects** - (not dependency source), so the exposure it acknowledges is compiled artifacts; `deploy-fastly.cache` - carries the same acknowledgement. - -### 3.8 Reusable-workflow contract - -Inputs: `app-repository`, `app-ref`, **`app-repo-id`** (string, always required), `working-directory` -(`.`), `workspace-root` (required), `app-cli-package` (required), `app-cli-bin`, `app-cli-artifact` -(**unique per matrix leg**), `cache` (default `false`), `cache-key-suffix`, `disclosure-acknowledged` -(required-true for cross-repo), `timeout-minutes` (30). **No `rust-toolchain`/feature inputs.** Secret -`app-checkout-token`. Job `permissions: { contents: read }` (caller grants ≥ that); -`persist-credentials: false`. **Runner floor 2.336.0** (self-repo `$/`). - -**Matrix:** v1's shared workflow outputs are **single-build** (GitHub returns only the last matrix -leg's outputs). A **matrix caller uses unique per-leg `app-cli-artifact` names and computes each -leg's `ExpectedIdentity` via `compute-app-cli-identity`** — it does not consume the shared outputs. - -## 4. Testing - -sccache (fresh `CARGO_TARGET_DIR` each run; `RUSTC_WRAPPER=sccache` action-owned; cold-to-warm shows -a sccache hit-rate rise and reduced compile with **network disabled** on the warm run; a corrupt -restored `SCCACHE_DIR` triggers one cold rebuild; the audited cache path is exactly `SCCACHE_DIR`; -**a git dependency (the EdgeZero repo) builds and caches**). Container/runner/launcher (self-hosted -fails closed; read-only rootfs; manifest command creating `dist/` succeeds in the writable copy while -the original stays clean; enumerated mounts only; host-side `mutation-attempted` before mutation; -cancellation `docker stop -t`+reconcile). Env/config (constructed minimal env — a caller -`RUSTC_WRAPPER` is absent, not merely rejected; non-allowlisted config anywhere fails). Identity -(`app-repo-id` API-verified against `app-repository`; `platform-id` from `image.json`, not caller; -consumer re-verifies checkout id/HEAD/workspace before+after). Provenance (canonical-JSON + dup-key; -archive exactly-two-members/format/size; **ABI loadability** — resolve `DT_NEEDED` in the image + a -network-disabled `--help`; a real wrong-runtime rejected; provenance documented consistency-only). -Disclosure required for every cross-repo build (equal-id exempt). Recovery production-only. - -## 5. Rollout, docs, migration - -**Atomic same-SHA rollout** (container image w/ sccache, reusable workflow, all three actions, -consumers, recovery); direct-composite producer retired → adopters migrate to the **two-job** -topology; runner floor **2.336.0**. Scope the parent's exact-key/target-only caching language to -`deploy-fastly.cache`; document that `build-app-cli.cache` is an **sccache disk cache** (compiled -objects, no source); apply the cross-repo disclosure rule to both caches; add the container-runner, -sccache, provenance, single-producer, and 2.336.0 updates; correct the "consumers own -checkout/runner/timeout; actions never call `checkout`" claims. Pin gate/`zizmor`/actionlint: -container digest pin, `$/` carve-outs. Public-surface golden: the `ExpectedIdentity` table, the -committed JSON Schema + **golden meta/archive vectors**, all three actions. - -## 6. Default and effect - -**Off by default** (caching). Container execution + provenance unconditional. With `cache: true` on -an authorized deployer build, sccache reuses compiled dependency objects across runs (the bulk of -the ~10 min); changed local crates recompile. - -## 7. Out of scope / future - -Caching checksum-verified `.crate` archives (download savings); workflow-bound artifact -**attestation**; native-tool (`cc`) sccache wrapping; trusted **self-hosted** runner mode; -cross-image/directional ABI; alternate toolchains (a second container); non-default features; -`cli-profile`; non-Fastly adapters. - -## 8. History - -… v6.11 (container-only) → v6.12 (own restore+save, full-runtime container) → v6.13 (crates.io-only, -hosted-only, four-root prune) → **v6.14 (sccache pivot)**: replace the unbuildable `target/` unit-graph -pruner and the unusable crates.io-only rule with a **fresh `CARGO_TARGET_DIR` + an action-owned pinned -`sccache` disk cache** (content-addressed, no pruning, any source incl. git deps, no source cached); -coarse rolling `run_id` generation key; **constructed minimal build env**; **writable working copy** -for manifest commands (read-only original for the freeze checks); `app-repo-id` **API-verified**, -`platform-id` **from `image.json` not the caller**, consumer **re-verifies checkout before+after**; -**disclosure required for every cross-repo build** (equal-id exempt); **ABI loadability** via resolved -`DT_NEEDED` + a network-disabled `--help`; normative **canonical-JSON + tar** contracts with golden -vectors; **matrix caller computes per-leg identity**; container plan gains a **verify-by-digest-then-PR** -publish (§ container sub-plan). - -## 9. Deferred to the implementation plan (mechanics only) - -Exact `prepare`/`compile`/launcher/helper signatures; the Dockerfile (checksum-verified Fastly CLI + -pinned sccache) + digest-pin + **verify-by-digest-then-PR** GHCR publish; the committed JSON Schema + -golden vectors; and the writer-fidelity / API-repo-id-binding / canonical-JSON predicate expressions. diff --git a/docs/superpowers/plans/2026-08-20-build-cache-actions.md b/docs/superpowers/plans/2026-08-20-build-cache-actions.md new file mode 100644 index 00000000..e9410183 --- /dev/null +++ b/docs/superpowers/plans/2026-08-20-build-cache-actions.md @@ -0,0 +1,176 @@ +# Build Cache Actions Implementation Plan (plan 2 of 5) + +> **Execution:** Start only after `2026-08-20-build-cache-container.md` records passing +> `{G,S,D,B}`. Use test-driven development and the gate-rotation procedure for every gate-owned test +> or helper change. + +**Goal:** Implement the shared app-source/identity/environment preflight and the optional sccache +restore/compile/save primitive that later plans consume, without making cache availability part of +build correctness or exposing credentials to compilation. + +**Spec:** `docs/superpowers/specs/2026-08-20-edgezero-deploy-build-caching-design.md` v6.26 Sections +2 through 5 and 9. + +## 1. Fixed decisions + +- Pin `actions/cache/restore@v6.1.0` and `actions/cache/save@v6.1.0` exactly. +- Pin host Git LFS to exact `3.7.1` and verify + `git-lfs-linux-amd64-v3.7.1.tar.gz` against SHA-256 + `1c0b6ee5200ca708c5cebebb18fdeb0e1c98f1af5c1a9cba205a4c0ab5a5ec08` and exact size + 5,524,590 bytes before installation. +- Cache only `${RUNNER_TEMP}/edgezero-sccache-v1`, mounted as `/work/sccache`; never cache target, + Cargo home, sources, registry data, credentials, or artifacts. +- Cache family is exactly `edgezero-sccache-v1--`, generation is exact + `job.check_run_id`, and the only restore prefix is `-`. +- `cache: false` selects `uncached-compile` and performs no restore, cache audit, sccache mount/start, + stats, stop, or save. `cache: true` accepts the documented stale-object and disclosure risks but + does not authorize save by itself. +- After the required metadata preflight, Cargo has exactly one compile/build invocation. The action + has no compile retry. Pinned sccache's documented post-`CompileStarted` local fallback is accepted + internal client behavior. + +## 2. Gate update before implementation + +- [ ] Add gate-owned failing fixtures and structural tests for the exact cache action versions, + authority/export boundaries, shared identity/environment/toolchain policy, stable host/path + literals, key grammar, separate lookup/save predicates, no credential mount/environment, exact + ordering, warning-only restore/save behavior, and one Cargo compile/build invocation after the + metadata preflight. +- [ ] Add shell fixtures for sccache 0.10.0 layout/stats, including corrupt records, unexpected paths, + write errors, stop failure, sparse files, hardlinks, special files, owner mismatch, path limits, + entry-count limit, and checked-byte overflow. +- [ ] Add closed canonical `.github/actions/deploy-core/host-tools.json`, the trusted Git LFS + installer, object-first authority materializer, exporter implementation, and focused tests to + the gate-owned path manifest. These exact runtime helper bytes are part of the gate update; no + public action metadata or workflow calls them yet. The manifest has exactly the design's literal + JCS bytes; trusted code constructs the fixed official release URL. + Permit at most three HTTPS redirects from exact `github.com` to a final exact + `release-assets.githubusercontent.com` host without credentials. Require final HTTP 200, + identity encoding, one exact `Content-Length: 5524590`, a streaming cap of 5,524,590 bytes, and + exact received size. Reject unknown/missing fields, unsupported platforms, a different asset/ + version/size, downgrade/host drift, absent/duplicate/malformed/mismatched length, chunked or + oversized/partial/trailing transfer, checksum or archive-layout mismatch, and replacement or + wrong-version execution of the installed binary. +- [ ] Land those tests as a gate-update PR under old `G`, activate the resulting gate revision, and + complete the full rotation/recovery checklist. Record it as this plan's active gate. Do not mix + public action wiring/metadata into the gate PR; the reviewed materializer/exporter helper itself + is gate code and lands here. Do not publish a new image because image-context bytes are unchanged. + +## 3. Shared build preflight, identity, and keys + +- [ ] Write failing tests for the length-framed `workspace-id` and suffix hash. Commit vectors for root + `.`, nested workspace, repository IDs of different decimal lengths, empty/255-byte suffixes, + overlong/control suffixes, malformed UTF-8 paths, and byte-distinct NFC/NFD names. +- [ ] Implement identity calculation in one shared host helper, which remains the sole owner in later + plans. Validate the authority checkout, canonical `git-root`, workspace and working-directory + containment, tracked regular `Cargo.lock`, and credential-free `cargo metadata --locked` + agreement before constructing either hash. +- [ ] Consume, without modifying, the active gate's shared authority materializer and trusted + exporter. Wire validated repository/ref/token inputs to those immutable helper bytes. They + install the exact verified Git LFS binary; fetch exact commits with system/global config, + includes, templates, hooks, credential helpers, worktree creation, filter execution, and + submodule initialization disabled; recursively validate committed attributes/config/gitlinks; + then create worktrees with filters disabled and materialize through the absolute LFS binary. + Require their existing tests for no forbidden command/origin contact, pointer residue, + configuration races, credential cleanup, and `.git`-free non-hardlinked Copy A/B to pass. Any + required helper change stops action work and returns to a separate gate rotation. +- [ ] Move the duplicate-rejecting bounded `app-env` decoder, empty Cargo-config/credentials policy, + path-dependency confinement, and exact `rust-toolchain` comparison into shared helpers here. + Deny exact `RUSTC`/`RUSTDOC`, every `RUSTC_`/`RUSTDOC_` name, and native-tool/flag prefix and + suffix forms including `CC_`, `_CC`, `HOST_CC`, `TARGET_CC`, and corresponding + `ARFLAGS`/`CFLAGS`/`CXXFLAGS`/`CPPFLAGS`/`LDFLAGS` variants. Also deny `CXXSTDLIB`, + `CXXSTDLIB_STATIC`, `CRATE_CC_NO_DEFAULTS`, all `CRATE_CC_*`, and every other exact design + control. Commit the full valid/invalid name/value/filter/config/toolchain fixtures and prove both + cached and uncached profiles reject compiler/wrapper/native-build replacement. Plans 3 and 4 + call these helpers and must not reimplement them. +- [ ] Emit shell-safe typed outputs and reject duplicate, missing, multiline, or malformed output. + Tests independently recompute SHA-256 bytes; they do not call the implementation as oracle. +- [ ] Require hosted `linux/amd64`, exact workflow repository/path/ref/SHA identity, full lowercase + app ref, and canonical positive `job.check_run_id` before any cache action runs. + +## 4. Restore and pre-use audit + +- [ ] Compute lookup eligibility before creating the host root or invoking `actions/cache/restore`. + First authenticate `app-repository`, verify its actual repository id and authority checkout, + and reject a mismatched caller `app-repo-id`. Same-repository builds are eligible only when that + verified id equals the event repository id; cross-repository `cache:true` requires boolean + `disclosure-acknowledged:true`. Missing, false, stringified, or malformed acknowledgement fails + the action before restore rather than degrading to restore-only. Commit a complete lookup truth + table, including a forged same-repository id, independent of save authorization. +- [ ] Create the stable host directory fresh and prove its canonical path equals the fixed runner-temp + child. Require it absent before create, mode 0700, uid/gid 1001, non-mount status, and stable + device/inode. Reject symlinked runner temp, any preexisting path, root replacement, nested mount, + wrong owner, or cleanup failure. +- [ ] Restore with exact primary key and sole family prefix. Restore failure, absence, download error, + or audit failure emits a warning, removes the complete host directory without following links, + recreates it empty, and continues cold. +- [ ] Implement the audit over descriptor-relative traversal. Accept only expected sccache 0.10.0 + regular files/directories beneath root with container uid/gid and `nlink==1` for files. Reject + links, sockets, FIFOs, devices, nested mounts, path escape, unknown layout, and arithmetic error. +- [ ] Enforce at most 2,147,483,648 summed regular-file `st_size` bytes, no sparse file + (`st_blocks*512 < st_size`), at most 100,000 descendants, path length at most 4,096 bytes, and + component length at most 255 bytes. Do not inspect or predict the cache action's tar/zstd wire + representation. + +## 5. Compile lifecycle + +- [ ] Implement a closed `uncached-compile` branch with Copy A, fresh target/Cargo home, and the shared + validated environment, but no sccache mount, process, socket, wrapper, or `SCCACHE_*` variable. + Invoke Cargo compile/build exactly once after metadata and test the complete mount/environment/ + process snapshot. +- [ ] Launch the pinned image by digest with read-only root, uid/gid 1001, dropped capabilities, + `no-new-privileges`, 6 GiB memory/no extra swap, 512 pids, bridge network, numeric integer + `timeout-minutes` in 1..120, and only the cached-compile mounts from the design. + Checkout tokens, GitHub file-command paths, provider inputs, and provider tokens must be absent. +- [ ] Construct the exact closed environment, including absolute `RUSTC_WRAPPER`, `SCCACHE_DIR`, 2G + managed size, `SCCACHE_IGNORE_SERVER_IO_ERROR=1`, zero incremental mode, empty encoded rustflags, + and validated `app-env`. Enforce the design's empty Cargo-config policy before launch. +- [ ] Start sccache, zero stats, invoke Cargo compile/build exactly once after metadata with locked inputs, capture exact + `sccache --show-stats --stats-format=json`, and stop the server. Validate the complete closed + v0.10.0 `ServerInfo`/`ServerStats` schema before reading `stats.cache_write_errors`: the six + top-level fields and all 22 exact `stats` fields listed by the design, exact language/count-map + and duration shapes, `cache_location` equal to `Local disk: "/work/sccache"`, bounded nonnull + `cache_size`, `max_cache_size:2147483648`, false preprocessor mode, and version `0.10.0`. Reject + duplicate/unknown/missing fields, wrong types, invalid duration nanoseconds, negative/overflow + counters, wrong version/cache path, and trailing data. Surface Cargo failure once. A wrapper startup/connection or + nonaccepted sccache failure is a build failure, not an action retry. +- [ ] On successful compile, require stop success, parse exact 0.10.0 stats, and rerun the complete + stopped-directory audit. Stop failure, malformed stats, nonzero `cache_write_errors`, or final + audit failure skips save with a warning but preserves successful build output. + +## 6. Save authorization + +- [ ] Commit a complete save truth table distinct from lookup eligibility. Save is true only for exact `push` or `workflow_dispatch`, boolean + `github.ref_protected==true`, boolean `github.event.repository.fork==false`, equal event/current + repository IDs, passed workflow identity, successful compile/stop/audit, zero write errors, and + satisfied cross-repository disclosure. Missing, stringified, malformed, or caller-supplied + substitutes are false. +- [ ] Explicitly cover `pull_request`, `pull_request_target`, `merge_group`, fork events, deleted or + unprotected refs, repository mismatch, same-repository disclosure exemption, and cross-repository + acknowledgement. A save-denied row remains restore-only only when lookup eligibility passed. +- [ ] Save with the immutable generation key. Save failure is warning-only. Verify no cache deletion, + reservation protocol, fallback key, mutable exact-key overwrite, or post-token save exists. +- [ ] After the sole save attempt or every earlier terminal path, remove only the recorded root by + descriptor-relative no-follow traversal and verify it is absent. Cleanup failure is fatal even + when restore/save failure was warning-only; test consecutive invocations in one job cannot + inherit a dirty fixed directory. + +## 7. Integration and completion + +- [ ] Integrate the helper only into a non-public test harness under the protected contract suite. The + build-only reusable workflow does not exist before plan 3, and the current direct-composite + producer must not expose an intermediate cache/provenance contract. Do not change provider or + public producer behavior in this plan. +- [ ] Run cold, warm, corrupt-restore, concurrent-generation, stop-failure, write-error, audit-failure, + save-denied, save-warning, and sccache response-loss fixtures. Assert the design's exact cold + Rust miss/write counters, a new-job warm restore with at least one post-zero Rust cache hit and + equal binary digest, and complete default-off absence; elapsed time is never evidence. +- [ ] Run shellcheck, the protected contract/container harness, `scripts/run-actionlint.sh`, zizmor, + and all Rust checks. Defer real reusable-workflow cold/warm evidence to plan 3 after the workflow + exists. Confirm all non-local actions remain exact-version pinned. +- [ ] Merge through the one-entry queue. Record the resulting commit and active gate revision for plan + 3; do not designate final `P` yet. + +**Gate:** the isolated primitive compiles correctly with an empty or unavailable cache; no restored +byte or application process can observe a credential; and only an action-derived authorized event can +attempt a save. It is not a supported producer until plan 3 integrates it. diff --git a/docs/superpowers/plans/2026-08-20-build-cache-consumer-adoption.md b/docs/superpowers/plans/2026-08-20-build-cache-consumer-adoption.md new file mode 100644 index 00000000..b7560e2d --- /dev/null +++ b/docs/superpowers/plans/2026-08-20-build-cache-consumer-adoption.md @@ -0,0 +1,268 @@ +# Build Cache Consumer and Adoption Implementation Plan (plan 5 of 5) + +> **Execution:** Start after plan 4 is merged and its gate revision is active. Select candidate exact +> version `C` before opening the final executable candidate. Select and publish stable `V` only after +> that merged commit passes detached local and immutable-candidate hosted verification; release the +> runnable documentation afterward as protected revision `R`. + +**Goal:** Ship the two-job producer/consumer workflow, prove real app repositories can adopt it with +full-SHA app identity and explicit inputs, publish exact stable action version `V`, then activate +synchronized runnable documentation at revision `R` without ever merging an unpublished version ref. + +**Spec:** `docs/superpowers/specs/2026-08-20-edgezero-deploy-build-caching-design.md` v6.26 Sections +3.3, 5.2, 7, 9, and 10, plus the parent deploy lifecycle contract. + +## 1. Release structure + +- `C` is an unused canonical exact patch version with no prerelease suffix. After the final candidate + reaches protected main as commit `H`, `C` is published as an immutable GitHub Release at `H` with + `prerelease:true` so hosted cross-repository workflows can test the exact merged bytes while every + `uses:` ref remains a full version. +- `H` becomes final action revision `P` only after the complete clean-detached local suite at `H` and + complete hosted suite through literal `C` pass. Only then is distinct unused canonical stable + version `V` selected and published as an immutable release at exact `P`. +- `R` is a later documentation-only protected-main commit. After the literal-`V` hosted smoke passes, + it adds canonical `docs/.edgezero-action-release.json` bound to `{V,P}` and replaces the bootstrap + placeholder in tracked Markdown with literal `V`. It changes no action/workflow implementation or + gate-owned path, and it is not a new action revision. +- Repository immutable releases must report enabled, and exact no-bypass `refs/tags/v*` rules prevent + action-version tag update/deletion. Failed candidate versions remain immutable; a correction uses a + new commit and newly selected unused `C`. A failed published stable version is never + retargeted; it is superseded by a new patch. Deletion of a GitHub Release object or mutation of its + title, notes, prerelease, or latest metadata by a sufficiently privileged actor remains an accepted + availability/discovery risk, so final evidence preserves release attestations and audits release + existence/state; tag deletion and retargeting remain blocked by the no-bypass ruleset. +- The gate built in plan 1 owns the entire bootstrap-to-released documentation transition. Before + `R`, only the four named prepublication documents may use the exact placeholder and are not claimed + runnable. At and after `R`, all tracked Markdown is placeholder-free and every EdgeZero `uses:` ref + names literal stable `V`. + +## 2. Gate update and contract fixtures + +- [ ] Add parsed-workflow tests for exact two-job separation, canonical exact patch-version + reusable-workflow/action refs, unique artifact names, caller and action identity forwarding, + public identity-action use, action-local source materialization with no authority-path handoff, + no aggregate matrix identity, minimal permissions, secret confinement, and absence of + direct-composite producer use. +- [ ] Add public/private app repository fixtures, root/nested workspace fixtures, public Git and + sibling path dependencies, submodules, no-filter and pinned-LFS cases, app-env migration, + generated outputs, production/staging provider paths, and cache enabled/disabled cases. +- [ ] Add negative fixtures for major/minor/prerelease/SHA/branch refs in published workflows, mixed + EdgeZero versions, producer provider inputs, missing authority/Copy B identity check, authority + path/descriptor/handle passed between actions, checkout token passed to a source-free action, + artifact-name reuse, caller-supplied platform/expected identity, ambient env reliance, custom + Git filters, and legacy `--stage`. +- [ ] Land and activate this gate update before changing reusable workflow/action interfaces. The gate + validates version grammar and equality, not a not-yet-created value for `V`. + +## 3. Final reusable producer workflow + +- [ ] Freeze `.github/workflows/build-app-cli.yml` as a build-only `workflow_call` interface with the + exact design inputs, required `rust-toolchain`, `app-checkout-token` secret, hosted-only workflow + version/resolved-SHA identity checks, bounded timeout, cache default `false`, and no provider + credential or mutation surface. +- [ ] Materialize EdgeZero source separately at exact `job.workflow_sha`. Checkout the app into a + recursive non-sparse authority root at full lowercase `app-ref`, verify authenticated repository + id and CallerExpectedIdentity, remove credentials, enforce the no-filter-or-pinned-LFS contract, + and invoke the shared exporter to create non-hardlinked `.git`-free Copy A. Prove no local action + or helper resolves from app data. +- [ ] Compile/package through the digest-pinned image and upload one deterministic named artifact. + Reverify the authority after use and remove Copy A plus every operation root on all exits. +- [ ] Emit only `artifact-name`, trusted `action-version`, resolved `action-revision`, and the five + CallerExpectedIdentity fields. Do not expose host paths, image ref/digest, protocol, cache path, + token, provider state, or an aggregate matrix value. +- [ ] Preserve separate cache lookup and save decisions: cross-repository cache use without disclosure + acknowledgement fails before restore; save additionally requires the protected-event predicate. + `cache:false` uses the no-sccache `uncached-compile` profile and Cargo has exactly one compile/ + build invocation after metadata preflight. + +## 4. Consumer job topology + +- [ ] Provide tested workflows in which job 1 calls the reusable producer and job 2 invokes public + `compute-app-cli-identity` at the same action version with the exact + repository/ref/id/workspace/cwd/package/bin/toolchain inputs and `app-checkout-token`. Compare + all five typed outputs with the producer and only then invoke provider actions with the named + artifact. The identity action destroys its action-private authority before returning and exposes + no host path or handle. +- [ ] Pin every EdgeZero reusable workflow and action within a published consumer workflow to one + identical literal stable `V`. Generated candidate-release tests substitute one identical literal + `C`. Reject mixed versions, major/minor tags, prereleases in published examples, SHAs, and + branches even when each ref resolves. +- [ ] Pin every third-party action to a separately reviewed canonical stable patch version. Record its + release URL and resolved commit in release evidence and prove no same-named branch exists at + review time. Later version-tag movement/deletion or same-name branch ambiguity introduced by a + trusted third-party publisher is accepted risk; branches, major/minor tags, prereleases, commit + SHAs, and mutable Docker tags remain prohibited in repository text. +- [ ] Pass producer `action-version` and `action-revision` to the comparison step. Every EdgeZero + composite requires its runner-provided action repository/ref to equal + `stackpop/edgezero@` before downloading anything. EdgeZero immutable release and + tag protections bind `V` or `C` to the producer revision. +- [ ] Give each matrix leg a deterministic unique artifact name and keep its identity comparison in + that leg. Reject aggregation, `merge-multiple`, wildcard downloads, and outputs inferred from a + matrix-wide reusable-workflow call. +- [ ] Keep checkout tokens host-side and provider tokens only in consumer steps that require them. + Pass `app-checkout-token` only to the identity action and the exact source-bearing actions + `deploy-fastly` and `config-push-fastly`; each independently materializes its authority and + removes its checkout credential channel before app code or provider-token creation/injection. + Source-free actions receive neither source-materialization inputs nor the token. Define explicit + job permissions and prove artifacts, caches, summaries, logs, and outputs contain neither token. +- [ ] Exercise `validate-app-cli-provenance`, `active-version-fastly`, `deploy-fastly`, + `healthcheck-fastly`, `rollback-fastly`, and `config-push-fastly` as independent consumers. Each + action downloads its own artifact, derives PlatformIdentity locally, writes a fresh expected file + from verified caller plus local platform fields, validates/smokes, and cleans its private state. + `deploy-fastly` and `config-push-fastly` additionally rematerialize and verify independent + action-local authorities; they never reuse the identity action's destroyed authority. + +## 5. App-repository migration behavior + +- [ ] Replace ambient workflow `env` examples with the explicit duplicate-safe `app-env` JSON object. + Document the exact deny rules and state that otherwise allowed values are caller-classified as + non-secret and may affect cross-repository compilation cache contents. +- [ ] Add `generated-output-paths` only for absent repository-relative roots genuinely written by the + credential-free app build. Explain that selected Fastly project `bin` and `pkg` roots are + implicit, callers must not list them, and preexisting/overlapping/tracked-containing roots fail. +- [ ] Require full app commit SHA, canonical repository id, explicit workspace root/package/bin and + `rust-toolchain`, and a tracked lockfile. Cover nested workspaces and private authority checkouts + without suggesting app branch or tag refs. +- [ ] Document the exact consumer authority interface: `compute-app-cli-identity` returns only five + typed identity fields; source-bearing actions receive the same explicit source inputs plus the + checkout token and rematerialize independently; source-free actions receive neither. Do not + document or expose an authority path, checkout step output, or reusable handle. +- [ ] State that the caller repository's effective Actions policy must permit version-tag action and + reusable-workflow refs; an organization/enterprise full-SHA mandate is incompatible with this + release policy and must fail adoption preflight rather than trigger an undocumented SHA fallback. +- [ ] Explain that protocol 1 rejects custom Git filters, supports only no filter or the action's + pinned Git LFS materialization path, rejects repository/enclosing Cargo config and credentials, + requires the image toolchain, permits only public dependency fetching, and preserves the + accepted undeclared proc-macro/build-script cache risk. +- [ ] Keep caching opt-in and distinguish `build-app-cli.cache` from `deploy-fastly.cache`. Both require + disclosure acknowledgement before cross-repository restore and use the same protected-event save + predicate. The latter applies only to credential-free app-build, saves before token introduction, + and does not prevent Fastly's token-bearing deploy compile. +- [ ] Preserve production/staging, first-deploy, healthcheck, rollback, cancellation, config-push, and + mutation-attempt semantics from the parent guide. Every staged command uses `--staging`; no + compatibility alias or legacy `--stage` instruction remains. + +## 6. Candidate-independent integration preparation + +- [ ] Create disposable public and private app repositories or equivalent GitHub-owned fixtures with + immutable source SHAs. Validate their repository ids, authority/export state, required LFS cases, + explicit app inputs, and provider test credentials before selecting `V`; do not replace hosted + evidence with local `act` or Docker-only tests. +- [ ] Build a release harness that writes one exact EdgeZero version into every producer/provider ref, + verifies all refs match, triggers hosted linux/amd64 runs, polls exact run/job attempts, and + records artifact/image/action/app identities without logging credentials or app-env values. +- [ ] Locally prove cold/warm/default-off cache behavior, source relocation, nested workspace identity, + artifact transfer, identity-action/source-bearing authority separation, Copy A/Copy B + independence, expected-file freshness, and exact caller/platform/action validation using + generated fixtures. +- [ ] Prepare production/staging provider fixtures for successful deploy, unhealthy rollback, first + deploy, active-version, healthcheck, config push, and cancellation reconciliation. Assert no + mutation on identity, source-freeze, output-root, loader, or token-order failure. + +## 7. Build final candidate `H` + +- [ ] Query current releases and remote refs, select unused canonical patch version `C`, and validate + its grammar. Require no current Git ref or release with that `tag_name`; release display `name` + is not identity and supplies no historical proof. Record the queries and results, and require + later creation to succeed without tag reuse. Do not create the tag yet, and do not select `V`. +- [ ] Reconcile the parent spec, original implementation plan, adoption guide, and public guide + against shipped metadata: two-job topology, authority materialization, action/caller identity, + exact app inputs, `app-env`, generated outputs, cache defaults, and provider lifecycle. Keep the + exact `` bootstrap placeholder in these four prepublication documents; + do not introduce a guessed or unpublished stable version. +- [ ] Run plan 1's permanent documentation scanner in bootstrap mode over every tracked Markdown + file. Prove `docs/.edgezero-action-release.json` is absent, the placeholder appears only in the + four named surfaces, no other action-ref placeholder exists, and every third-party ref is an + exact stable patch version. Do not modify the gate-owned scanner in this plan. +- [ ] Run every protocol, cache, image, launcher, source-freeze, provider, workflow, fixture, docs/pin, + actionlint, zizmor, shellcheck, Rust, and local integration suite at one clean candidate descended + from `B`. Confirm `image.json` remains reviewed `{D,S,protocol}`. +- [ ] Run independent contract and release-adversary reviews against design v6.26, including exact-tag + policy, third-party tag movement risk, EdgeZero immutable releases, action-version mixing, + substitution, identity replay, malformed artifacts, host/container races, source mutation, + generated-output escape, credential flow, cache disclosure, rollback, and cancellation. +- [ ] Merge through the one-entry queue and require exact protected-main push checks. Record the + resulting lowercase full commit SHA as `H`; do not designate it `P` or publish stable `V` yet. + +## 8. Qualify `H` and publish `V` + +- [ ] From a clean detached checkout at exact `H`, rerun the complete local suite from Section 7, + including the bootstrap-state docs/pin scanner, exact staged container build, Docker-backed + provider tests, and repository CI commands. Preserve logs and digests bound to `H`. +- [ ] Select the release operator before `C` exists. Use a short-lived fine-grained PAT selected only + for `stackpop/edgezero`, expiring within 24 hours, with exactly repository `Contents:write` and + `Workflows:write`, implicit metadata read, organization `Members:read`, and no other grant. Keep + it outside Actions/argv/environment, reject classic or installation tokens, and preserve its + settings as review evidence. The local helper permits only no-redirect versioned `GET /user`, + exact organization-id/team-id membership GET, release POST, release-id PATCH, and release-id GET + routes from the design. Require active team membership and no unallowlisted method/path/query/ + body field; destroy the token after release work. +- [ ] For each action release, create a draft with exact `tag_name`, full target commit, no assets, + and required `prerelease` boolean; publish only by a PATCH that changes `draft` to false. Record + requests/responses, authenticated login, release `author.login`, team membership, release id/ + URL, remote peeled ref, exact state, and generated attestation. A different actor, broader token, + or direct unrecorded tag creation fails release. +- [ ] Reconfirm the immutable-releases endpoint returns HTTP 200 with `enabled:true` and boolean + `enforced_by_owner`, and both action-version tag rulesets are exact. Publish candidate `C` as an + immutable release targeting exact `H` with API field `prerelease:true`; require its exact + patch-version `tag_name`, `draft:false`, `immutable:true`, and the remote peeled tag to resolve + to `H`, and record the generated release attestation. Never move or delete a failed candidate + tag. +- [ ] Run the complete hosted cross-repository suite with every EdgeZero workflow/action ref equal to + literal `C`: cold/warm/default-off caches; public/private and LFS source; two matrix identities; + artifact/identity/expected handoff; all provider lifecycles; all negative pre-mutation cases; and + cancellation reconciliation. Verify exact run attempts and `action-revision==H`. +- [ ] If either exact-`H` suite fails, fix on a new commit, choose a new unused `C`, and repeat Sections + 7-8; no stable `V` has been selected. After both pass, designate `H` as final action revision `P`. +- [ ] Query current releases and remote refs, then select a distinct unused canonical stable patch + version `V`. Require no current ref or release with that `tag_name`. Publish `V` through the same + exact fine-grained-PAT actor/draft/publish procedure targeting `P`; require API and anonymous + peeled-ref resolution to equal `P`, `draft:false`, `prerelease:false`, and `immutable:true`. + Preserve the attestation/operator evidence and verify no major/minor alias was created or moved. +- [ ] Run a final hosted producer/consumer identity smoke with every EdgeZero ref literal `V` and + require `action-revision==P`. A post-publication failure does not permit changing `V`; publish a + corrected new patch through the normal protected process. Do not create documentation revision + `R` until this smoke passes. + +## 9. Publish documentation revision `R` + +- [ ] Create canonical `docs/.edgezero-action-release.json` with exact JCS bytes + `{"action-revision":"

","action-version":"","schema-version":1}` and no trailing newline. + Replace every `` ref in tracked Markdown with literal `V`. Change only + tracked Markdown plus that record; do not touch action/workflow code, metadata, gate-owned paths, + or implementation fixtures. +- [ ] Run the plan-1 dual-state scanner on the documentation PR and its final merge-group candidate + using the closed event-to-range table. The selected base is the then-current protected-main + commit, which may be newer than `P`; the synthetic candidate is not named `R`. Require the + one-way bootstrap-to-released transition, exact record schema/JCS, no placeholder in any fenced + YAML, one identical EdgeZero `V` per workflow, and exact third-party patch versions. The hosted + transition verifier must prove public release `V` is `draft:false`, `prerelease:false`, + `immutable:true` and its API target and anonymously peeled ref both equal record `P`. +- [ ] Parse every fenced YAML example, validate it against action/workflow metadata, and prove examples + are runnable after only repository/application-value substitution. Run the complete docs build, + pin scanner, actionlint, and required repository checks; merge through the one-entry queue and + record the resulting protected-main commit as `R`. Require the protected-main push scanner to + compare `event.before` with `event.after==R`, then rerun released-state checks. Do not move or + recreate `V`. + +## 10. Final review + +- [ ] Compare every documented input, output, default, secret, permission, mount, environment, + artifact, cache, source-freeze, provider, and failure behavior at documentation revision `R` to + actual action metadata/tests at `P`. +- [ ] Search for forbidden external major/minor/prerelease/branch/SHA `uses:` refs, EdgeZero version + mismatches, direct `build-app-cli` composite producer guidance, caller-provided platform or + expected fields, ambient app env, writable source mounts outside declared roots, and legacy + `--stage`. Every hit must be a clearly marked rejected example or fail documentation release. +- [ ] Verify public anonymous image pull by digest and an end-to-end fresh app adoption from the + published guide using literal `V`. Record image-release gate `G`, the ordered gate-rotation + lineage and final active gate, `{S,D,B,P,C,V,R,protocol}`, action-version resolved commits, + release attestations, hosted run ids/attempts, and documentation-check results. Do not collapse + the post-`B` gate revisions into the image-release `G` label. + +**Gate:** exact merged revision `P` passed the complete local and candidate-version hosted suites; +immutable stable release `V` resolves to `P`; protected documentation revision `R` activated the +one-way released scanner state and all concrete consumer examples use `V`; no consumer relies on a +floating ref, same-job producer shortcut, ambient application environment, or provider mutation +before independent validation. diff --git a/docs/superpowers/plans/2026-08-20-build-cache-container.md b/docs/superpowers/plans/2026-08-20-build-cache-container.md index eb9a0df6..5a668991 100644 --- a/docs/superpowers/plans/2026-08-20-build-cache-container.md +++ b/docs/superpowers/plans/2026-08-20-build-cache-container.md @@ -1,393 +1,1058 @@ -# Build-Cache Container Implementation Plan (sub-plan 1 of 4) - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Publish a pinned, single-manifest `linux/amd64` build container that bakes the exact Rust toolchain + build tools, so `platform-id` for the cached-build feature is an immutable digest. - -**Architecture:** A versioned in-repo Dockerfile builds an image FROM a digest-pinned base with the workspace's pinned Rust toolchain and the tools `build-app-cli` needs (`git`, `jq`, `tar`, `curl`, `ca-certificates`, a C toolchain for `build.rs`). A publish workflow builds it single-arch, pushes it to GHCR, and records its **manifest digest** in a committed `image.json`. A fail-closed `check-image-pin.sh` (wired into the existing pin gate's test harness) proves the recorded reference is pinned by a 64-hex `sha256` digest, never a mutable tag. - -**Tech Stack:** Docker (BuildKit), GitHub Actions (`docker/build-push-action`), GHCR, Bash, `jq`. - -**Spec:** `docs/specs/edgezero-deploy-build-caching.md` (v6.14, sccache pivot) — §2 (single-producer, hosted-only v1), §3.1 (sccache cache mechanism), §3.6 (image contract: baked Rust + `wasm32-wasip1` + **sccache** + Fastly CLI, read-only/non-root), §5 (digest pin, atomic same-SHA rollout). - -## Global Constraints - -- **Rust toolchain baked = `1.95.0`** (verbatim from `.tool-versions`); a build that resolves a different toolchain must fail closed downstream, so this image is the single source of truth. -- **Full build+deploy runtime baked** (spec §3.6): `1.95.0` + `wasm32-wasip1` + a pinned **`sccache`** (the cache mechanism, spec §3.1) + the pinned **Fastly CLI `15.1.0`** (`.tool-versions`) + `git jq tar curl cc` — the container is the deploy runtime, not only the CLI-compile runtime. -- **Runtime posture:** consumed **read-only root filesystem, non-root user**, explicit writable mounts only (spec §3.7). -- **Single-manifest `linux/amd64` only** — no multi-arch index (an index digest can select another architecture). -- **No Python in CI tooling** — Bash + `jq` only. -- **Pin policy:** every referenced image/action is pinned; the base image is pinned by `sha256` digest, and the published image is recorded by `sha256` digest. -- **No AI bylines** in commits or PR bodies. -- **Bash 3.2-compatible** scripts (macOS dev parity); scripts are `shellcheck -S warning` clean. - -## File Structure - -- `.github/docker/build-app-cli/Dockerfile` — the image definition (one responsibility: the build environment). -- `.github/docker/build-app-cli/image.json` — the published image's canonical reference + digest (the pin record). -- `.github/docker/build-app-cli/check-image-pin.sh` — fail-closed validator of `image.json`. -- `.github/actions/deploy-core/tests/check-image-pin.test.sh` — unit tests for the validator (colocated with the existing action test harness). -- `.github/workflows/publish-build-container.yml` — build + push + digest capture (runs on a `build-container-v*` tag). -- `.github/actions/deploy-core/tests/run.sh` — modified to invoke the new validator suite. - ---- - -### Task 1: Fail-closed `image.json` validator (pure TDD) +# Build-Cache Container Implementation Plan (plan 1 of 5) + +> **Execution:** Use `superpowers:subagent-driven-development` or +> `superpowers:executing-plans`. Follow the tasks in order and stop at every release checkpoint. + +**Goal:** Publish and pin a public, leaf `linux/amd64` runtime image containing the exact EdgeZero +build/deploy toolchain and the trusted provenance validator required by build caching. + +**Architecture:** A separately landed, immutable gate baseline `G` owns the validator, fixtures, +classifier, image verifier, publisher checker, exact Dockerfile, complete image-context manifest, and +organization-required workflow. Source revision `S` is an isolated canonical release request whose +repository image-context bytes remain identical to `G`. The publisher stages a fresh context solely from the +verified `G` checkout, captures and verifies immutable digest `D`, proves anonymous access, and opens +an idempotent, forward-only App-authored PR adding the image and release-evidence records. That pin +forms baseline `B`. Four remaining feature +plans land on top. Their final passing action revision `P` contains the unchanged `{D, S, protocol}` +record and the adoption documents for exact stable version `V`; consumers pin immutable release +version `V`, which resolves to `P`. + +**Spec:** `docs/superpowers/specs/2026-08-20-edgezero-deploy-build-caching-design.md` v6.26. + +**Tooling:** Rust, Docker BuildKit/buildx, GHCR, GitHub Actions, Bash 3.2, `jq`, `gh`, `actionlint`, +`shellcheck`, and `zizmor`. + +## 1. Non-negotiable contracts + +- Rust is the exact version in `.tool-versions` (`1.95.0` at plan time). +- Fastly CLI is the exact version/checksum in `.github/actions/deploy-fastly/versions.json` + (`15.1.0` at plan time). +- sccache is exactly `0.10.0`, fetched as the upstream + `sccache-v0.10.0-x86_64-unknown-linux-musl.tar.gz` client artifact and verified against upstream + checksum `1fbb35e135660d04a2d5e42b59c7874d39b3deb17de56330b25b713ec59f849b`. +- The base is the official `rust:1.95.0-slim-bookworm` `linux/amd64` leaf manifest, resolved on + 2026-08-31 as `sha256:6f9e63259f12e1e599296f5ecfed2bae46de4af0ee0525dd8b89c046e236d5c5` + and re-resolved immediately before the Dockerfile commit. No placeholder digest or checksum is + committed. +- The final image is a leaf `linux/amd64` image manifest, not an OCI index. +- The final image contains an installed `wasm32-wasip1` target, not merely a rustc target-list entry. +- The project-owned validator, schema, and capability fixtures are baked and tested before push. +- Runtime is non-root uid/gid 1001 and works with a read-only root filesystem plus explicit tmpfs. +- Every committed non-local external action and reusable workflow ref is a canonical exact stable + `v..` release tag. Major/minor tags, prereleases, branches, commit SHAs, and + floating refs fail. Docker image refs use immutable `sha256` digests. Local `./...` actions remain + local refs. Third-party tag movement/deletion and future same-name branch ambiguity are explicitly + accepted risks; EdgeZero `V` is an immutable release protected by no-bypass tag rules. +- In this plan, an action/workflow "pin" means that exact stable version tag. Full Git commit SHAs + identify app source, resolved workflow execution, protected gate/release commits, and the + organization required-workflow descriptor only; they are never written as non-local `uses:` refs. +- Bash scripts are Bash 3.2-compatible and `shellcheck -S warning` clean. CI helper scripts do not use + Python. No AI bylines appear in commits or PRs. +- Publication never records a digest before the image passes authenticated verification and a clean, + anonymous pull by digest. +- Candidate source never supplies the workflow, classifier, verifier, completion marker, or publisher + policy used to approve itself. Gate code runs from full SHA `G` with no secret or mutation token. +- The exact actionlint version is `1.7.12`; the installer carries reviewed SHA-256 values for its + linux/darwin amd64/arm64 archives. Earlier actionlint releases are not release evidence because they + reject the required `environment.deployment: false` syntax. Because 1.7.12 predates GitHub's + `concurrency.queue`, the gate uses the design's exact yq-backed compatibility wrapper; no other + actionlint diagnostic is ignored. + +## 2. Dependency order + +Although this is plan 1 of five, image publication cannot run first. Execute these gates: + +1. Complete the repository-wide exact-version pin-gate migration and actionlint upgrade (Task 0). +2. Complete the protocol-owner validator, schema, fixtures, pin validator, image verifier, exact image + source/context, fail-closed classifier, publisher contract checker, and protected workflow (Tasks + 1-2). Merge these as the separately reviewed protected gate baseline `G`. +3. Configure the organization required-workflow descriptor at exact SHA `G`, mandatory merge queue, + protected environment, split tag rulesets, audit credentials, and dedicated GitHub App. Prove the rules + and credential smoke before any candidate becomes `S` (Task 2). +4. Add only the canonical `release-request.json` for `G`. Run this isolated candidate through `G`, + merge only through the queue, and require the API-visible exact post-merge push assertion. Record + that default-branch commit as `S` (Task 3). +5. Run the already-landed publisher at `S`, verify digest `D`, and merge its ancestry-checked pin PR to + create baseline `B` (Tasks 3-4). +6. Execute the four remaining cached-build, provenance-integration, launcher, and consumer plans on + `B`; their final passing commit becomes action revision `P`. + +Do not publish a provisional image without the validator. Do not use a placeholder `image.json` to +break the dependency cycle. + +## 3. Planned file surface + +Create: + +- `.github/tools/edgezero-provenance-validator/{Cargo.toml,Cargo.lock}` +- `.github/tools/edgezero-provenance-validator/src/{lib,main,json_contract,archive,elf,extract}.rs` +- `.github/tools/edgezero-provenance-validator/tests/cli.rs` +- `.github/docker/build-app-cli/provenance.schema.json` +- `.github/docker/build-app-cli/fixtures/provenance/**` +- `.github/docker/build-app-cli/fixtures/wasm-smoke.rs` +- `.github/docker/build-app-cli/Dockerfile` +- `.dockerignore` +- `.github/docker/build-app-cli/gate-paths.txt` +- `.github/docker/build-app-cli/image-context-paths.txt` +- `.github/docker/build-app-cli/verify-toolchain.sh` +- `.github/docker/build-app-cli/verify-published-image.sh` +- `.github/docker/build-app-cli/stage-build-context.sh` +- `.github/docker/build-app-cli/assert-build-container-context.sh` +- `.github/docker/build-app-cli/verify-release-prerequisites.sh` +- `.github/docker/build-app-cli/release-approval-gate.sh` +- `.github/docker/build-app-cli/write-image-release-record.sh` +- `.github/docker/build-app-cli/update-image-pin-pr.sh` +- `.github/docker/build-app-cli/classify-build-container-change.sh` +- `.github/docker/build-app-cli/run-build-container-gate.sh` +- `.github/docker/build-app-cli/check-build-container-publisher.sh` +- `.github/docker/build-app-cli/verify-gate-rotation-lock.sh` +- `.github/actions/deploy-core/tests/verify-toolchain.test.sh` +- `.github/actions/deploy-core/tests/verify-published-image.test.sh` +- `.github/actions/deploy-core/tests/stage-build-context.test.sh` +- `.github/actions/deploy-core/tests/assert-build-container-context.test.sh` +- `.github/actions/deploy-core/tests/verify-release-prerequisites.test.sh` +- `.github/actions/deploy-core/tests/release-approval-gate.test.sh` +- `.github/actions/deploy-core/tests/write-image-release-record.test.sh` +- `.github/actions/deploy-core/tests/update-image-pin-pr.test.sh` +- `.github/actions/deploy-core/tests/classify-build-container-change.test.sh` +- `.github/actions/deploy-core/tests/run-build-container-gate.test.sh` +- `.github/actions/deploy-core/tests/check-build-container-publisher.test.sh` +- `.github/actions/deploy-core/tests/verify-gate-rotation-lock.test.sh` +- `.github/actions/deploy-core/tests/build-container-workflows.test.sh` +- `.github/actions/deploy-core/tests/check-doc-action-pins.sh` +- `.github/actions/deploy-core/tests/run-actionlint.test.sh` +- `.github/workflows/build-container-ci.yml` +- `.github/workflows/publish-build-container.yml` +- `.github/workflows/rotate-build-container-gate.yml` +- `.github/CODEOWNERS` +- `scripts/run-actionlint.sh` + +Created by the isolated source/release PR, not gate `G`: + +- `.github/docker/build-app-cli/release-request.json` + +Created by the pin PR, not source revision `S`: + +- `.github/docker/build-app-cli/image.json` +- `.github/docker/build-app-cli/image-release-evidence.json` + +Modify: + +- `.github/docker/build-app-cli/check-image-pin.sh` +- `.github/actions/deploy-core/tests/check-image-pin.test.sh` +- `.github/actions/deploy-core/tests/check-action-pins.sh` +- `.github/actions/deploy-core/tests/run.sh` +- `.github/zizmor.yml` +- `.github/workflows/deploy-action.yml` +- `scripts/install-actionlint.sh` +- every existing `.github` workflow/composite containing a non-local external `uses:` ref +- the four deploy/adoption documents containing consumer `uses:` examples + +## 4. Task 0: Enforce exact-version external references repository-wide + +The current pin gate permits major/minor tags, prereleases, and commit SHAs. That is broader than +v6.26 and must be narrowed before adding the write-privileged publisher. **Files:** -- Create: `.github/docker/build-app-cli/check-image-pin.sh` -- Test: `.github/actions/deploy-core/tests/check-image-pin.test.sh` -**Interfaces:** -- Consumes: nothing (leaf). -- Produces: `check-image-pin.sh ` — exit `0` iff the JSON has string `repository`, string `tag`, and a `digest` matching `^sha256:[0-9a-f]{64}$`; prints `::error::` and exits `1` otherwise. Reused by the pin gate and the publish workflow. - -- [ ] **Step 1: Write the failing test** +- Modify `.github/actions/deploy-core/tests/check-action-pins.sh` and its tests in `run.sh`. +- Create `.github/actions/deploy-core/tests/check-doc-action-pins.sh`. +- Modify `.github/zizmor.yml`. +- Modify `scripts/install-actionlint.sh` and the workflow environment that selects its version. +- Create `scripts/run-actionlint.sh` and its focused compatibility test. +- Modify external refs in `.github/workflows/{codeql,deploy-action,deploy-docs,fastly-installer-check,format,test}.yml`. +- Modify external refs in `.github/actions/{build-app-cli,config-push-fastly,deploy-fastly,healthcheck-fastly,rollback-fastly}/action.yml`. +- Modify examples in `docs/specs/edgezero-deploy-github-action.md`, + `docs/specs/edgezero-deploy-action-implementation-plan.md`, + `docs/specs/edgezero-deploy-adoption-guide.md`, and `docs/guide/deploy-github-actions.md`. + +- [ ] Write failing pin-gate tests proving `@v1`, `@v1.2`, branches, prereleases, build metadata, + full/abbreviated SHAs, malformed/leading-zero versions, and empty refs fail; canonical stable + `@v1.2.3` passes; local actions and digest-pinned Docker actions remain valid. Generate invalid + YAML fixtures under the test's temporary directory; do not commit them into a surface scanned + by the production gate. +- [ ] Resolve each existing external ref to a reviewed upstream exact stable patch release. Record + the release URL and resolved commit in review evidence, prove the release tag exists and no + same-named branch exists at review time, but write the version tag in YAML. +- [ ] Change the structural YAML scanner to require canonical exact stable patch versions for every non-local external + action and reusable workflow. Its default scan is exactly workflow `*.yml`/`*.yaml` files directly + under `.github/workflows`, plus every repository-wide `action.yml`/`action.yaml`, pruning `.git`, + `target`, and `node_modules`. Shell source and arbitrary YAML test data are not inputs. Do not add a + low-privilege exception. +- [ ] Reject empty and null `uses` scalars and count only parsed non-local external refs for the + non-vacuity assertion. Encode each structurally extracted scalar so a multiline value cannot split + into multiple shell records. +- [ ] Require Docker action refs to match an immutable lowercase + `docker://@sha256:<64-lowercase-hex>` form; tags, uppercase hex, short digests, and other + algorithms fail unless a separately reviewed digest algorithm is added to the policy. +- [ ] Update the four prepublication adoption documents to use literal + `` where the future consumer will substitute stable release `V`; + examples for third-party actions use real reviewed exact patch versions. +- [ ] Add `check-doc-action-pins.sh` to parse fenced YAML in every tracked Markdown file and implement + both documentation states from the design. In bootstrap state, absent + `docs/.edgezero-action-release.json` permits the exact EdgeZero placeholder only in the four + named prepublication documents. In transition state, a candidate adds the exact JCS `{V,P}` + record, changes only tracked Markdown plus that record, removes every placeholder, and uses one + literal `V` in all EdgeZero refs. In released state, the base record cannot disappear; it is + either byte-identical or replaced atomically with all documentation refs by a strictly greater + stable version under the same hosted release/ref proof. Concrete third-party refs always use + exact stable patch versions; major/minor/prerelease/branch/SHA refs fail. Add positive/negative + state-transition, base/candidate, partial-update, downgrade/deletion, hidden-placeholder, and + mixed-version cases to `run.sh`. +- [ ] Add the hosted transition verifier to gate `G`. With only read permissions and fixed + no-redirect versioned requests, it proves record `V` is a published `draft:false`, + `prerelease:false`, `immutable:true` release whose API target and anonymous peeled remote ref + both equal record `P`. Bootstrap and unchanged released-state checks remain offline. Candidate + code cannot replace the verifier or release record parser. +- [ ] Retain global zizmor `ref-pin` as defense in depth and document that the structural scanner is + stricter. Rewrite `.github/zizmor.yml`'s existing comment so it no longer claims full commit + SHAs pass repository policy: `ref-pin` accepts symbolic refs, while the structural gate permits + only exact stable patch tags. Update contradictory prose in all four named documents, not only + their fenced YAML examples. +- [ ] Upgrade actionlint to exactly `1.7.12`. Pin these reviewed release archives in + `scripts/install-actionlint.sh`: linux/amd64 + `8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8`, linux/arm64 + `325e971b6ba9bfa504672e29be93c24981eeb1c07576d730e9f7c8805afff0c6`, darwin/amd64 + `5b44c3bc2255115c9b69e30efc0fecdf498fdb63c5d58e17084fd5f16324c644`, and darwin/arm64 + `aba9ced2dee8d27fecca3dc7feb1a7f9a52caefa1eb46f3271ea66b6e0e6953f`. Test exact version, + supported tuples, unknown tuple rejection, and checksum mismatch. Add an actionlint regression + fixture containing `environment: {name: build-container-release, deployment: false}`. +- [ ] Before the publisher exists, add failing tests for pinned actionlint's two known syntax gaps. + `run-actionlint.sh` requires mikefarah yq 4.53.3 and structurally permits workflow-level + `concurrency.queue: max` only in the publisher and gate-rotation workflows, each with exact group + `edgezero-build-container-publication` and literal `cancel-in-progress:false`. It permits exactly + the four `job.workflow_*` properties only in approved expressions/checkout ref locations of + `.github/workflows/build-app-cli.yml`. Reject duplicate/aliased/misplaced/wrong values, + misspellings, extra job properties, other workflows, and dynamic expressions. +- [ ] After structural validation, make line-count-preserving temporary copies that blank only the two + approved queue lines and substitute same-type constants for only the approved job-context + expressions. Run unfiltered actionlint 1.7.12 on those files and remap paths/lines; do not use + `-ignore` or filter diagnostics. Raw canonical fixtures must emit exactly the reviewed unsupported + queue/job-context diagnostic set, sanitized fixtures must pass, and every unrelated actionlint + error must remain fatal. +- [ ] Scan that exact default surface, including reusable-workflow job-level `uses`, and require at + least one parsed external ref so a broken parser cannot pass vacuously. +- [ ] Run the pin suite, actionlint, and zizmor. ```bash -#!/usr/bin/env bash -# .github/actions/deploy-core/tests/check-image-pin.test.sh -set -euo pipefail -DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) -CHECK="$DIR/../../../docker/build-app-cli/check-image-pin.sh" -WORK=$(mktemp -d) -trap 'rm -rf "$WORK"' EXIT -pass=0 fail=0 -ok(){ printf ' ok %s\n' "$1"; pass=$((pass+1)); } -no(){ printf ' FAIL %s\n' "$1"; fail=$((fail+1)); } -run(){ bash "$CHECK" "$1" >/dev/null 2>&1; } - -printf '{"repository":"ghcr.io/stackpop/edgezero-build-app-cli","tag":"v1","digest":"sha256:%064d"}\n' 0 >"$WORK/ok.json" -run "$WORK/ok.json" && ok "a digest-pinned reference passes" || no "a digest-pinned reference passes" - -printf '{"repository":"ghcr.io/x","tag":"v1","digest":"v1"}\n' >"$WORK/tag.json" -run "$WORK/tag.json" && no "a non-digest (tag) reference is rejected" || ok "a non-digest (tag) reference is rejected" - -printf '{"repository":"ghcr.io/x","tag":"v1"}\n' >"$WORK/nodigest.json" -run "$WORK/nodigest.json" && no "a missing digest is rejected" || ok "a missing digest is rejected" - -printf 'not json\n' >"$WORK/bad.json" -run "$WORK/bad.json" && no "malformed JSON fails closed" || ok "malformed JSON fails closed" - -printf 'Passed: %d Failed: %d\n' "$pass" "$fail" -[ "$fail" -eq 0 ] +bash .github/actions/deploy-core/tests/run.sh +.github/actions/deploy-core/tests/check-action-pins.sh +.github/actions/deploy-core/tests/check-doc-action-pins.sh +scripts/run-actionlint.sh +zizmor --offline .github/workflows .github/actions ``` -- [ ] **Step 2: Run it to verify it fails** - -Run: `bash .github/actions/deploy-core/tests/check-image-pin.test.sh` -Expected: FAIL (the `check-image-pin.sh` file does not exist yet). - -- [ ] **Step 3: Write the minimal implementation** - -```bash -#!/usr/bin/env bash -# .github/docker/build-app-cli/check-image-pin.sh -# Fail-closed: the build container reference must be pinned by a sha256 digest, -# never a mutable tag (spec §3.7/§5). Requires mikefarah yq/jq-free: uses jq. -set -euo pipefail - -file="${1:?usage: check-image-pin.sh }" -if ! command -v jq >/dev/null 2>&1; then - echo "::error::check-image-pin.sh requires jq" >&2 - exit 2 -fi -if ! json=$(jq -e . "$file" 2>/dev/null); then - echo "::error::$file is not valid JSON — refusing to pass an unreadable image pin" >&2 - exit 1 -fi -repo=$(jq -r '.repository // empty' <<<"$json") -tag=$(jq -r '.tag // empty' <<<"$json") -digest=$(jq -r '.digest // empty' <<<"$json") -if [[ -z "$repo" || -z "$tag" ]]; then - echo "::error::$file must set string 'repository' and 'tag'" >&2 - exit 1 -fi -if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then - echo "::error::$file 'digest' must be a sha256 manifest digest (sha256:<64-hex>), not a tag: '$digest'" >&2 - exit 1 -fi -echo "build container reference is pinned: $repo@$digest" -``` +**Gate:** both structural scanners pass their exact surfaces and report non-zero parsed-reference +counts; no broad `rg` gate scans intentional invalid test strings. -- [ ] **Step 4: Run the test to verify it passes** +## 5. Task 1: Implement the protocol-owner validator for gate baseline `G` -Run: `chmod +x .github/docker/build-app-cli/check-image-pin.sh && bash .github/actions/deploy-core/tests/check-image-pin.test.sh` -Expected: `Passed: 4 Failed: 0`. +This task owns protocol-1 encoding and validation. No shell, `jq`, system `tar`, or general-purpose +archive crate may become a second wire implementation. It is a hard dependency of Task 2 and lands in +protected gate baseline `G` before source revision `S` is proposed. -- [ ] **Step 5: Shellcheck** +**Files:** -Run: `shellcheck -S warning .github/docker/build-app-cli/check-image-pin.sh` -Expected: no output (clean). +- Create standalone workspace `.github/tools/edgezero-provenance-validator` with its own + `Cargo.toml`, `[workspace]`, `Cargo.lock`, and + `src/{lib,main,json_contract,archive,elf,extract}.rs`. +- Put module unit tests beside their implementation under `src/`; create only the true process-level + integration test `.github/tools/edgezero-provenance-validator/tests/cli.rs`. +- Create `.github/docker/build-app-cli/provenance.schema.json`. +- Create `.github/docker/build-app-cli/fixtures/provenance/{valid,invalid}/**`. +- Do not modify or include the root workspace manifests; the validator has no external local path + dependency. + +### 5.1 JSON/schema tranche + +- [ ] Add one Draft 2020-12 schema and exact valid/invalid fixtures for both `expected.json` and + `app-cli-meta.json` from design Section 6.2. Write colocated failing tests for RFC 8785 bytes, + recursive duplicate-key rejection before object construction, every exact field/type/bound, + unknown and missing fields, noncanonical decimal/hash/name values, schema/protocol mismatch, + `container-ref` derivation, `workspace-id` rendering, and complete caller/platform identity + mismatch. Workspace/suffix hash computation vectors belong to the cache-actions follow-on plan, + not this protocol crate. +- [ ] Test a closed typed canonical encoder. Protocol 1 contains only bounded strings, positive + integers, null, fixed objects, and the `needed` array; no generic floating-point value is accepted. +- [ ] Run `cargo test --manifest-path .github/tools/edgezero-provenance-validator/Cargo.toml json_contract::tests`; expected: non-zero for + unimplemented behavior. +- [ ] Implement only `json_contract.rs`; rerun the focused and full crate tests; expected: pass. + Commit the green JSON/schema tranche. + +### 5.2 Archive/extraction tranche + +- [ ] Add a byte-for-byte golden archive from design Section 6.3 plus malformed base-256/octal, + checksum, embedded-NUL, PAX/GNU, sparse, duplicate, extra, traversal, link, special-file, header, + order, size, padding, end-block, overflow, and trailing-data fixtures. +- [ ] Write failing encoder, parser, and extraction tests. Assert two repeated encodes are identical, + all payload padding is zero, exactly two end blocks precede EOF, and failure leaves the fresh output + parent empty. +- [ ] Run `cargo test --manifest-path .github/tools/edgezero-provenance-validator/Cargo.toml archive::tests`; expected: non-zero for + unimplemented protocol behavior. +- [ ] Implement `archive.rs` and `extract.rs` directly over bounded `Read + Seek`/`Write`; do not + invoke system `tar`, add a tar crate, or load the allowed 512 MiB binary wholesale. Create outputs + atomically and require the final regular file to have mode 0755 and link count one. Rerun focused + and full crate tests; expected: pass. Commit the green archive/extraction tranche. + +### 5.3 ELF/loadability tranche + +- [ ] Add controlled static/dynamic valid, wrong class/endian/type/architecture/interpreter, + wrong `EI_VERSION`/`e_version`/`EI_OSABI`/`EI_ABIVERSION`/`EI_PAD`/`e_flags`/`e_ehsize`/ + `e_phentsize`, zero `e_phnum`, `PN_XNUM`, + malformed/duplicate `PT_DYNAMIC`, missing/nonzero-after `DT_NULL`, conflicting string-table tags, + unmapped/overlapping string ranges, malformed string/interpreter termination, RPATH/RUNPATH, + AUDIT/DEPAUDIT/CONFIG/AUXILIARY/FILTER/POSFLAG rejection, valid bounded SONAME, empty/oversized/ + slash-containing/duplicate SONAME rejection, NODEFLIB/LOADFLTR and unknown-flag rejection, every + in-range and just-outside case for the closed numeric tag allowlist, exact + `DT_FLAGS=0x0000001e` and `DT_FLAGS_1=0x5eff976f` mask boundaries, unknown standard/GNU/OS/processor + tag rejection, duplicate rejection for every singleton tag, slash/backslash/dollar-containing + dependency including every `$ORIGIN`, `$LIB`, and `$PLATFORM` spelling, missing + direct/transitive flat-closure library, duplicate basename, dangling or escaping candidate, + mixed architecture, duplicate-needed, interpreter dependency, and cycle fixtures for the + controlled loader profile in design Section 6.4. +- [ ] Write failing tests for machine, interpreter/null, byte-sorted duplicate-preserving direct + `DT_NEEDED`, digest, size, exact `/opt/edgezero/runtime-lib` lookup, symlink/hardlink/subdirectory + rejection, duplicate basename, interpreter parsing, and recursive dependency resolution against + a synthetic image root. Add preload presence, cache-only/default-directory/hardware-capability + substitution, direct-loader argv, and explicit `dlopen` non-claim fixtures. +- [ ] Run `cargo test --manifest-path .github/tools/edgezero-provenance-validator/Cargo.toml elf::tests`; expected: non-zero for + unimplemented inspection/loadability behavior. +- [ ] Implement `elf.rs` with bounded ranged reads and checked offsets. Do not invoke `ldd`, the + loader, or the artifact. Rerun focused and full crate tests; expected: pass. Commit the green ELF + tranche. + +### 5.4 CLI/capability tranche + +- [ ] Write failing library integration tests using a private synthetic-root harness for deterministic + expected-write/package/validate round trips, identity mismatch, atomic cleanup, no-replace + collision, and host-deletion recovery. This + harness calls library entry points and is not a CLI option or production bypass. Write host process + tests proving all output-producing commands reject every `--work-root` that does not canonicalize + to literal `/work`, plus process tests for self-test fixture integrity. Run + `cargo test --manifest-path .github/tools/edgezero-provenance-validator/Cargo.toml --test cli`; + expected: non-zero until wired. Implement: + +```text +edgezero-provenance-validator write-expected \ + --work-root /work \ + --app-repo-id \ + --source-revision <40-lowercase-hex> \ + --app-cli-package \ + --app-cli-bin \ + --workspace-id sha256:<64-lowercase-hex> \ + --platform-id sha256:<64-lowercase-hex> \ + --provenance-protocol 1 \ + --output /work/expected/expected.json + +edgezero-provenance-validator write-release-request \ + --work-root /work \ + --gate-sha <40-lowercase-hex> \ + --provenance-protocol 1 \ + --release-tag build-container-v \ + --output /work/release/release-request.json + +edgezero-provenance-validator package \ + --work-root /work \ + --binary /work/input/app-cli \ + --schema /usr/local/share/edgezero/provenance.schema.json \ + --expected /work/input/expected.json \ + --app-cli-version \ + --archive /work/packaged/artifact.tar + +edgezero-provenance-validator validate \ + --work-root /work \ + --archive /work/input/artifact.tar \ + --schema /usr/local/share/edgezero/provenance.schema.json \ + --expected /work/input/expected.json \ + --output /work/validated/app-cli + +edgezero-provenance-validator self-test \ + --fixtures /usr/local/share/edgezero/provenance-fixtures +``` -- [ ] **Step 6: Commit** +- [ ] Make production `write-expected`, `write-release-request`, `package`, and `validate` require + canonical `--work-root /work`. + `write-expected` accepts only the typed bounded scalars above, fixes schema version `1`, derives + `container-ref`, and is the sole expected-identity encoder. Every command creates exactly one + output through a create-new temporary sibling plus Linux no-replace rename and fails if the parent + is not fresh, empty, canonical, writable, and confined. Test lexical and canonical confinement for + every binary, expected, archive, schema, fixture, and output path; require schema and fixture paths + to equal their baked image-owned literals. Handled failures remove the + sibling; synthetic-root library tests model host deletion of the whole parent after + SIGKILL/timeout. The validator never executes the app binary. Positive CLI round trips run only in + Task 3's container, where literal `/work` exists. +- [ ] Make `write-release-request` the sole release-request producer. Accept only typed gate SHA, + protocol `1`, canonical release tag, and the literal fresh output path; test exact three-key JCS + bytes, duplicate/missing/unknown flags, no-replace publication, and output cleanup. +- [ ] Implement `self-test` as a compiled manifest of exact relative paths, fixture SHA-256 values, + and valid/invalid outcomes. A missing, extra, or changed fixture fails. +- [ ] Use synchronous Rust; do not add Tokio or change dependencies of core/adapter crates. +- [ ] Run process, focused, and full crate tests; expected: pass. Commit the green CLI/capability + tranche. +- [ ] Run the focused crate tests, then the repository-required Rust and documentation checks. ```bash -git add .github/docker/build-app-cli/check-image-pin.sh .github/actions/deploy-core/tests/check-image-pin.test.sh -git commit -m "build-cache container: fail-closed image.json digest-pin validator" +cargo test --manifest-path .github/tools/edgezero-provenance-validator/Cargo.toml +cargo fmt --manifest-path .github/tools/edgezero-provenance-validator/Cargo.toml --all -- --check +cargo clippy --manifest-path .github/tools/edgezero-provenance-validator/Cargo.toml \ + --workspace --all-targets --all-features -- -D warnings +cargo fmt --all -- --check +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test --workspace --all-targets +cargo check --workspace --all-targets --features "fastly cloudflare spin" +cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin +npm --prefix docs ci +npm --prefix docs run format +npm --prefix docs run lint +npm --prefix docs run build +./scripts/check_no_placeholder_pins.sh +./scripts/check_no_legacy_typed_reads.sh +cargo run -q --bin check_no_nested_app_config --features nested-app-config-check -- \ + examples/app-demo crates/edgezero-cli/src/templates +cargo test -p edgezero-cli --features nested-app-config-check --bin check_no_nested_app_config +cargo test -p edgezero-adapter-fastly --all-targets --features cli +cargo test -p edgezero-cli --test generated_project_builds -- --ignored +cargo clippy -p edgezero-adapter-fastly --features cli --all-targets -- -D warnings +cargo clippy -p edgezero-adapter-fastly --no-default-features --lib -- -D warnings +cargo fmt --manifest-path examples/app-demo/Cargo.toml --all -- --check +cargo clippy --manifest-path examples/app-demo/Cargo.toml \ + --workspace --all-targets --all-features -- -D warnings +cargo test --manifest-path examples/app-demo/Cargo.toml --locked --workspace --all-targets ``` ---- +**Gate:** deterministic expected-write/package/validate golden tests and every capability fixture hash pass from a +clean checkout. The candidate PR must also pass every current format/test matrix job, including the +four wasm clippy legs and three wasm test runners; the local command list does not replace those +runner-backed gates. Task 2 copies the exact reviewed validator source, schema, and fixtures into the +gate-owned image source; the Dockerfile rebuilds the binary from that closed source rather than +copying this host build. -### Task 2: The pinned Dockerfile +## 6. Task 2: Establish protected gate baseline `G` -**Files:** -- Create: `.github/docker/build-app-cli/Dockerfile` -- Create: `.github/docker/build-app-cli/image.json` (placeholder digest until Task 3 publishes) - -**Interfaces:** -- Consumes: the Global Constraints (Rust `1.95.0`, single-arch amd64). -- Produces: an image whose `rustc --version` is `1.95.0` and which has `git jq tar curl cc` on `PATH`; consumed by Task 3's publish and by sub-plans 2–4 as `platform-id`. - -- [ ] **Step 1: Write the Dockerfile** - -```dockerfile -# .github/docker/build-app-cli/Dockerfile -# Single-manifest linux/amd64 FULL build+deploy runtime (spec §3.7): the pinned -# Rust toolchain, wasm32-wasip1, the pinned Fastly CLI, and build tools. This -# image IS the toolchain/ABI identity; it runs read-only/non-root at runtime. -# Base pinned by digest; replace the digest below with a current -# rust:1.95.0-bookworm linux/amd64 manifest digest (see README in this dir). -FROM rust:1.95.0-bookworm@sha256:0000000000000000000000000000000000000000000000000000000000000000 - -# Pinned downloads (spec §3.6): fastly 15.1.0 (versions.json) and a pinned sccache. -# Each ARG carries the exact release URL + sha256 (fill the sccache values from the -# chosen sccache release; the fastly values are versions.json's). -ARG FASTLY_URL="https://github.com/fastly/cli/releases/download/v15.1.0/fastly_v15.1.0_linux-amd64.tar.gz" -ARG FASTLY_SHA256="3ba3d8a739b7a88d0a612825a9755d735efb87a9b02ea67e53a11b96d178d500" -ARG SCCACHE_VERSION="0.10.0" -ARG SCCACHE_URL="https://github.com/mozilla/sccache/releases/download/v0.10.0/sccache-v0.10.0-x86_64-unknown-linux-musl.tar.gz" -ARG SCCACHE_SHA256="REPLACE_WITH_RELEASE_SHA256" - -RUN set -eux; \ - apt-get update; \ - apt-get install -y --no-install-recommends \ - git jq tar curl ca-certificates build-essential; \ - rm -rf /var/lib/apt/lists/*; \ - rustup target add wasm32-wasip1; \ - curl -fsSL -o /tmp/fastly.tar.gz "$FASTLY_URL"; \ - echo "${FASTLY_SHA256} /tmp/fastly.tar.gz" | sha256sum -c -; \ - tar -xzf /tmp/fastly.tar.gz -C /usr/local/bin fastly; \ - curl -fsSL -o /tmp/sccache.tar.gz "$SCCACHE_URL"; \ - echo "${SCCACHE_SHA256} /tmp/sccache.tar.gz" | sha256sum -c -; \ - tar -xzf /tmp/sccache.tar.gz --strip-components=1 -C /usr/local/bin "sccache-v${SCCACHE_VERSION}-x86_64-unknown-linux-musl/sccache"; \ - chmod +x /usr/local/bin/sccache; \ - rm /tmp/fastly.tar.gz /tmp/sccache.tar.gz; \ - fastly version; sccache --version - -# No ambient rustflags/wrapper env (spec §3.8 also scrubs at runtime); non-root. -ENV CARGO_TERM_COLOR=never RUSTFLAGS="" CARGO_ENCODED_RUSTFLAGS="" -RUN useradd -m -u 1001 build -USER build -WORKDIR /home/build -``` +This task creates the trust root that evaluates the later release request, owns every repository +image-context input and fixed external-source verification rule, and owns every script that can see a +release credential. Candidate code is always subject data. No image is published in this task. -> The Fastly CLI download is checksum-verified against `versions.json`'s pinned -> `sha256` (above). The publish workflow (Task 3) builds on a hosted runner and -> **makes the GHCR package public** (GHCR packages are private on first publish); the -> image is consumed **read-only/non-root** with explicit writable mounts (spec §3.7). +**Files:** -- [ ] **Step 2: Write the placeholder pin record** +- Create `.github/docker/build-app-cli/{gate-paths,image-context-paths}.txt`, the Dockerfile, and root + `.dockerignore`. +- Create `.github/CODEOWNERS`. +- Modify `.github/docker/build-app-cli/check-image-pin.sh`. +- Create `.github/docker/build-app-cli/{verify-toolchain,verify-published-image}.sh`. +- Create `.github/docker/build-app-cli/{stage-build-context,assert-build-container-context}.sh`. +- Create `.github/docker/build-app-cli/{classify-build-container-change,run-build-container-gate}.sh`. +- Create `.github/docker/build-app-cli/{verify-release-prerequisites,release-approval-gate}.sh`. +- Create `.github/docker/build-app-cli/{update-image-pin-pr,check-build-container-publisher}.sh`. +- Create the matching focused tests under `.github/actions/deploy-core/tests/`. +- Create `.github/workflows/{build-container-ci,publish-build-container}.yml`. +- Modify `.github/actions/deploy-core/tests/run.sh` and + `.github/workflows/deploy-action.yml`. + +### 6.1 Pin-record validator + +- [ ] Extend `check-image-pin.test.sh` first. Cover the valid five-field record; malformed, + duplicate, extra, and missing fields; wrong JSON types; foreign or empty repository; mutable, + zero, uppercase, or malformed digest/source; non-integer or non-`1` protocol; malformed tag; + and any use of the tag as a runtime pull reference. +- [ ] Add independent canonical bytes and malformed cases for the exact ten-field + `image-release-evidence.json`: duplicate/extra/missing/reordered keys, non-JCS bytes, wrong + strings/integer, run id/attempt precision, stale/invalid UTC time, login/challenge/digest/tag/S + grammar, and every cross-file mismatch. Pair add/change/delete must be atomic. +- [ ] Implement `check-image-pin.sh ` with Bash and `jq`. Detect duplicate top-level + keys from streaming parse events before ordinary object construction. Accept exactly: ```json { "repository": "ghcr.io/stackpop/edgezero-build-app-cli", "tag": "build-container-v1", - "digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000" + "digest": "sha256:<64-lowercase-hex>", + "image-source-revision": "<40-lowercase-hex>", + "provenance-protocol": 1 } ``` -(The placeholder digest is intentional; Task 3's publish workflow overwrites it with the real one, and `check-image-pin.sh` still passes on shape. The pin-gate wiring in Task 4 additionally forbids the all-zero placeholder in a release.) - -- [ ] **Step 3: Verify the image builds and bakes the toolchain (local integration check)** - -Run (requires Docker + a real base digest substituted into the `FROM`): -```bash -docker build --platform linux/amd64 -t edgezero-build-app-cli:local .github/docker/build-app-cli -docker run --rm --platform linux/amd64 edgezero-build-app-cli:local rustc --version -docker run --rm --platform linux/amd64 edgezero-build-app-cli:local rustc --print target-list | grep -x wasm32-wasip1 -docker run --rm --platform linux/amd64 edgezero-build-app-cli:local fastly version -docker run --rm --platform linux/amd64 edgezero-build-app-cli:local sccache --version -docker run --rm --platform linux/amd64 edgezero-build-app-cli:local sh -c 'command -v git jq tar curl cc' -# read-only/non-root smoke (spec §3.7): a read-only rootfs run still works with a tmpfs. -docker run --rm --read-only --tmpfs /tmp --user 1001 --platform linux/amd64 edgezero-build-app-cli:local rustc --version + The tag matches `^build-container-v[1-9][0-9]*$` and is informational. Expose only the + digest-qualified runtime ref, source SHA, and protocol through typed subcommands or shell-safe + output fields. + +- [ ] Run the focused test and `shellcheck -S warning`. Do not create placeholder + `image.json` or evidence record. +- [ ] Implement the gate-owned typed release-record writer and paired validator. The writer consumes + only trusted current publisher/approval scalars, emits exact JCS create-new bytes, and never + accepts raw JSON. Runtime actions continue to parse only `image.json`. + +### 6.2 Image and runtime verification + +- [ ] Immediately before the gate-image commit, re-resolve the official + `rust:1.95.0-slim-bookworm` linux/amd64 leaf and require the reviewed digest in Section 1. + Independently download the exact sccache asset and checksum companion and require the reviewed + checksum. Stop for review on movement or disagreement; never commit a placeholder. +- [ ] Commit the exact multi-stage Dockerfile, root `.dockerignore`, and sorted + `image-context-paths.txt` as gate-owned inputs. The context manifest excludes the root workspace + manifests and contains only the complete standalone validator directory, schema/fixtures, + `.tool-versions`, Fastly versions, Dockerfile, and `.dockerignore`; every entry is also in + `gate-paths.txt`. Run `cargo metadata --locked --manifest-path +.github/tools/edgezero-provenance-validator/Cargo.toml` inside the staged context and reject any + workspace member or path dependency outside that validator directory. Any new effective input + requires gate rotation. +- [ ] Write `stage-build-context.test.sh` before its helper. Cover missing/extra/duplicate/unsorted + manifest entries, symlink/hardlink/FIFO/device inputs, path escape, dirty gate checkout, + candidate Dockerfile substitution, changed `S` copy of a manifested byte, unmanifested source, + remote `ADD`, bind-mounted build context, broad `COPY`, and post-install replacement. The helper + creates a fresh directory outside both checkouts and copies only regular manifested files from + exact clean `G`, preserving paths and executable modes; the publisher passes that directory as + the sole Docker context. +- [ ] Build the validator in the Dockerfile with one + `cargo build --locked --release --manifest-path +.github/tools/edgezero-provenance-validator/Cargo.toml` invocation. Test both the exact staged- + context metadata command and exact Docker build. Use the reviewed Rust base digest and + checksum-verified Fastly/sccache assets; install `wasm32-wasip1`; copy only + the exact final binaries/assets and the complete startup-library closure. No later Dockerfile + instruction may replace an installed tool, validator, schema, fixture, interpreter, or closure + member. Static contract tests bind the instruction sequence and destinations. +- [ ] Populate flat `/opt/edgezero/runtime-lib` with the complete reviewed x86-64 startup closure, + require exact dynamic interpreter `/lib64/ld-linux-x86-64.so.2`, and remove + `/etc/ld.so.preload`. Dynamic app-binary launches use the container runtime's argv API with exact + interpreter options `--inhibit-cache --glibc-hwcaps-mask '' --library-path +/opt/edgezero/runtime-lib`; static binaries run directly. Test that no shell, cache, default + directory, hardware-capability directory, or preload can substitute a startup object. +- [ ] Write failing command-fixture tests for `verify-toolchain.sh`. Cover exact, prerelease, + extra-text, missing, and malformed Rust/Fastly/sccache version output; absent + `wasm32-wasip1`; failed minimal compile; invalid wasm magic; validator self-test failure; + wrong uid/gid; and read-only-root failure. +- [ ] Snapshot the `self-test` container's complete create/run contract: digest-pinned linux/amd64 + image, uid/gid 1001, read-only root, all capabilities dropped, no-new-privileges, no network, + 2 GiB memory/swap, 64 pids, 10-minute wall limit, no host bind mounts, only `/work/home` and + `/work/tmp` tmpfs, exact three-variable environment, and exact baked-fixture argv. Reject every + extra mount, environment value, flag, or path. +- [ ] Implement exact semantic-version parsing, installed-target inspection, and compilation of the + committed `wasm-smoke.rs` library into tmpfs. Substring version matching is forbidden. +- [ ] Write failing fixture tests for `verify-published-image.sh`. Cover accepted leaf Docker and + OCI manifests; rejected one-entry/multi-entry indexes; missing config/layers; malformed BuildKit + metadata; wrong OS/architecture; wrong source/revision/protocol labels; mutable-tag lookup; private + registry response; and every toolchain/validator failure. +- [ ] Implement the verifier over a supplied `repository@digest`, source SHA, and protocol. Use + `docker buildx imagetools inspect "$REF" --raw` for media type and + `--format '{{json .Image}}'` for image OS/architecture. Never inspect a tag to discover + identity. +- [ ] Exercise the baked validator's `write-expected` profile with only a fresh writable + `/work/expected`, tmpfs home/temp, no repository/binary/target/Cargo/cache/token mount, and + `--network=none`. Then run deterministic package twice and validate every golden/malformed + archive with each operation's exact mount profile. Assert no `/work/package` convention + exists. +- [ ] Run both focused tests and shellcheck before implementing, then rerun them green. + +### 6.3 Protected classifier and required workflow + +- [ ] Commit `gate-paths.txt` as the canonical, sorted list of every gate-owned path: + the standalone validator manifest/lockfile, validator crate, schema and fixtures, pin scanner, + all classifier/verifier/policy/approval/updater/publisher-checker helpers, + context manifest/Dockerfile/`.dockerignore`, their focused tests and `run.sh` wiring, + `scripts/{install-actionlint,run-actionlint}.sh`, `.github/CODEOWNERS`, `.github/zizmor.yml`, and + both container workflows. The manifest contains itself. `image-context-paths.txt` is a strict + subset, is also canonical/sorted, and closes over every Docker build input and local Cargo + dependency. +- [ ] Write failing classifier tests for the design's closed pull-request, merge-group, and protected- + push event-to-range table, including exact payload/context SHA and ref agreement; + add/change/rename/delete; all-zero first-push base; shallow/missing commits; malformed or duplicate + output; ordinary, isolated-release-request, and gate-update modes; mixed gate/non-gate changes; + old/candidate manifest union handling; exact failed-`G'` gate-rollback restoration; every + interrupted pointer/policy state; and every context-manifest entry. Pin classification is + exact paired add/change/delete detection for `image.json` and `image-release-evidence.json`; a release request is relevant only when it is + the sole changed path and has exact canonical shape and active `G`. +- [ ] Implement the classifier over explicit base/head SHAs from a full subject checkout. Its only + stdout is exactly two fixed-order lines: `mode=ordinary|gate-update|gate-rollback` and + `relevant=true|false`; both gate modes require true. Any invalid range, missing object, gitlink, + path ambiguity, mixed + gate/non-gate change, or unmanifested repository image-context input fails. Gate-update mode requires the + base tree to equal old `G`, validates candidate manifest and CODEOWNERS as inert data, and permits + changed paths only in the union of old and candidate manifests. Gate-rollback requires disabled + release, both pointers at old `G`, a base valid as failed `G'`, a head tree byte-equal to old + `G`, the two-manifest union, and no non-gate/release/pin change. +- [ ] Derive those classifier SHAs only through the trusted workflow's closed event selector. For + `pull_request`, require exact base repository/ref and synthetic merge-parent agreement. For + `merge_group:checks_requested`, require payload base/head SHAs, exact base/head refs, + `head_sha==github.sha`, the `gh-readonly-queue/main/` head prefix, and base ancestry. For + protected-main `push`, require nonzero `before`, `after==github.sha==github.workflow_sha`, exact + main ref, and base ancestry. Reject every other event or inconsistent/missing field before the + classifier runs. `workflow_dispatch` is a separate credential-smoke path and invokes neither + the classifier nor documentation scanner. +- [ ] Write `run-build-container-gate.test.sh` before its driver. Test separate gate/subject + roots, full-SHA checkout assertions, candidate helper substitution, symlink escape, dirty checkout, + missing/duplicate completion markers, ordinary relevant, gate-update, gate-rollback, and + explicit-not-applicable branches, pin deletion, mixed-change rejection, and propagation of every verifier + failure. +- [ ] Implement the driver so all authority comes from its own canonical gate root. The subject root is + read/build input only. It never sources, executes, or resolves a helper from the subject. +- [ ] Create workflow contract tests before YAML. They require: - organization-rule events `pull_request` and `merge_group` with + `merge_group` limited to `checks_requested`, local + protected-main `push`, and manual `workflow_dispatch`; - no workflow-level path filter and stable jobs `build-container-local` and + `build-container-pin` on every PR/merge-group candidate; - workflow permissions exactly `contents: read`, `actions: read`, and `pull-requests: read`, with + no secret/environment/mutation token in either required job; - repository variable `EDGEZERO_BUILD_CONTAINER_GATE_SHA` validated as a full SHA; - separate gate and subject checkouts with persisted credentials disabled; - all classifier, driver, completion, and verifier commands resolved under the gate checkout; - exact protected workflow repository/path parsing from `github.workflow_ref` and exact + `github.workflow_sha` assertions for required runs; - explicit not-applicable execution and an unconditional terminal completion assertion; and - fixed steps `assert-exact-g-dispatch-context` and `assert-exact-main-push-context`; generic + protected-main push assertions for event, ref, `event.after`, current head `Q`, workflow SHA, + active gate SHA, and both latest-attempt job conclusions, plus separate release-request + identity when `Q=S`. +- [ ] Implement `build-container-ci.yml`. For organization-required runs, + `github.workflow_ref` must identify `stackpop/edgezero` and the exact path, and + `github.workflow_sha` must equal repository variable `EDGEZERO_BUILD_CONTAINER_GATE_SHA`, whose + value is `G`. Push runs use local workflow SHA equal to current protected head `Q` while still + executing gate code from that variable. Each stable push job invokes the trusted context helper + in exactly one named `assert-exact-main-push-context` step. Keep the existing path-filtered + `deploy-action.yml` separate. +- [ ] Wire all focused suites into `run.sh`. The protected gate runs its own tests; it never + executes a candidate test script. + +### 6.4 Release-policy verifier and publisher + +- [ ] Write fake-API tests before `verify-release-prerequisites.sh`. Implement the exact + credential-specific method/path/query allowlists from design Section 8. Reject redirects, + unvalidated placeholders, unknown pagination, wrong credential use, any persistent mutation + route, hidden/missing `bypass_actors`, and partial/truncated pagination before network or state + interpretation. Every request must use exact `Accept: application/vnd.github+json`, + `X-GitHub-Api-Version: 2026-03-10`, and `User-Agent: edgezero-build-container-gate/1`; reject a + missing/different header. Require response + `X-GitHub-Api-Version-Selected: 2026-03-10`, parsed media type exactly `application/json` with + absent/UTF-8 charset for a body, exact GET/POST/DELETE statuses 200/201/204, and an empty 204 + revocation body. Require a clean detached checkout at exact `G` before reading any credential. +- [ ] Require local `EDGEZERO_RELEASE_POLICY_AUDIT_TOKEN` to be a short-lived fine-grained PAT + owned by the verified active `stackpop` organization-owner login and selected only for + `stackpop/edgezero` with repository Actions/read, Checks/read, + Environments/read, Pull requests/read, Variables/read, Metadata/read, Administration/write and organization + Members/read, Administration/write. A second operator records token id, resource owner, repository + selection, expiry, exact displayed grants, screenshot digest, and every absent write surface. + The helper does not claim GitHub can report the complete PAT grant set. +- [ ] Require separate `EDGEZERO_RELEASE_PACKAGE_AUDIT_TOKEN` to authenticate an active + `stackpop` owner and report exactly normalized scopes + `{read:org,read:packages}`. Reject byte-equal audit tokens before the first request. +- [ ] Verify the environment's nonempty reviewer rule, `prevent_self_review=true`, final sole + tag deployment policy `build-container-v*`, empty custom deployment-protection-rules endpoint, + and supplied manual administrator-bypass + evidence. Verify repository variable `EDGEZERO_BUILD_CONTAINER_GATE_SHA=G`; the exact + organization required-workflow descriptor bound directly to gate commit `G`, with no ref or + bypass actor, exact repository-id/main-ref conditions, and `do_not_enforce_on_create=false`; + and repository ruleset + `edgezero-build-container-main` with + target `branch`, active enforcement, no bypass, exact `main` include/no excludes, exact + pull-request review fields, and merge queue `{timeout:60, ALLGREEN, build:1, merge:1, SQUASH, +min:1, wait:0}` from the design. Every missing, extra, defaulted, or changed semantic field + fails. Require the immutable-releases endpoint to return HTTP 200, parse `enabled` as exact + boolean `true`, require `enforced_by_owner` to be present as a boolean, and record its value; + tolerate additional response fields. Require exact + image tag rulesets `edgezero-build-container-tag-creation` and + `edgezero-build-container-tag-immutability` for `refs/tags/build-container-v*`, plus action tag + rulesets `edgezero-action-version-tag-creation` and + `edgezero-action-version-tag-immutability` for `refs/tags/v*`; all have exact repository source, + tag target, active enforcement, include, and no excludes. Each creation ruleset has only the + creation rule and sole reviewed-team `always` bypass. Each immutability ruleset has no bypass + and only deletion plus update with `update_allows_fetch_and_merge:false`. + Require Repository ruleset `edgezero-build-container-pin-branches` with exact repository source, + branch target, active enforcement, `refs/heads/edgezero-build-container-pin/*` include/no + excludes, only the dedicated App Integration `always` bypass, and exact creation/update/deletion + rule array. +- [ ] Verify organization and repository Actions permissions both report + `sha_pinning_required:false`; add their exact GET routes to the policy-token allowlist. Run a + hosted exact-patch-version action fixture so any stricter enterprise override fails before + publication. +- [ ] Verify the candidate PR identity, final queue `merge_group` required-workflow run from + `G`, and exact post-merge run/job API records for `S`. The run must have event `push`, exact + workflow path, and `head_sha=S`; each stable job must have `head_sha=S`, succeed, and contain + exactly one successful `assert-exact-main-push-context` step. The trusted step checks ref, + `event.after`, current main head `Q`, `github.sha`, `github.workflow_sha`, and active gate internally because those + values are not exposed by the run REST response. Missing/duplicate/wrong-attempt assertion steps + fail. Matching check names from candidate workflow code are not evidence. +- [ ] Verify protected-environment App/installation/team variables and private-key secret metadata. + Use the local App key to authenticate the exact dedicated App and selected-repository installation + with only contents/write, pull-requests/write, and implicit metadata/read. The only non-GET calls + are creation of a repository-id-bounded test token and its guaranteed revocation. Reject extra + repository or permission scope. +- [ ] Resolve `EDGEZERO_BUILD_CONTAINER_PUBLISHER_BOT_LOGIN` through the exact public user endpoint + and require the response login, numeric id, and `type:"Bot"` to equal the independently reviewed + repository variables. Reject a user/login collision before trusting pin-PR authorship. +- [ ] Prove package absence only through the fully paginated verified-owner listing before first push. + After first push, require public visibility and linkage to `stackpop/edgezero`. A 404 or + authorization failure is never absence. +- [ ] Emit canonical evidence with every identity, rule id/URL, workflow/run/attempt/job URL, permission, + package state, manual-evidence digest, and timestamp, but no credential. A separately authenticated + operator posts it and the byte-identical PNG to the candidate PR. Test evidence-post failure. +- [ ] Add `build-container-release-preflight` to the gate-owned CI workflow only for + a `workflow_dispatch` body with `ref:"main"` while protected `main==G`, with a required + candidate PR-number, head-repository, and full head-SHA input. Its exact candidate-bound + `run-name` is API-visible. It uses + `environment: {name: build-container-release, deployment: false}`, checks out only exact `G`, + runs no candidate code, contains exactly one fixed `assert-exact-g-dispatch-context` step that + fetches the PR with the read-only token and compares all three inputs, and + uses the pinned App-token action only to prove the stored key can mint the exact repository- + scoped token. The run API must later show event `workflow_dispatch`, exact path, `head_sha=G`, + and that successful named assertion step. +- [ ] Fixture-test the bounded credential smoke: temporarily add literal deployment branch policy + `main`, dispatch the workflow with body `ref:"main"` while `main==G`, approve and complete the smoke, remove + only `main`, and restore the sole tag policy. Any wildcard, caller branch, candidate + commit, credential update, or workflow SHA other than `G` invalidates the smoke. Capture + the pre-`S` administrator-bypass PNG only after final tag-only policy is restored. +- [ ] Write `release-approval-gate.test.sh`. Cover canonical valid comment; missing, duplicate, + rejected, and bypassed reviews; wrong environment/reviewer/run id/run attempt/source/tag/PNG digest; + wrong/missing/duplicate challenge and image digest; attempted future-attempt predeclaration; + malformed/extra/reordered JSON; invalid calendar, fractional, offset, stale, or future time; + API/non-200/malformed response; reused earlier-attempt + evidence; and proof that token creation and every mutation have not run on failure. +- [ ] Implement the gate with only `actions:read` and `contents:read` available. It + performs only the exact two no-redirect GETs for the current run and its non-paginated approval + history, requires complete valid 200 responses, and requires exactly: + +```text +edgezero-release-evidence-v1 {"challenge":"<64-lowercase-hex>","image-digest":"","png-sha256":"sha256:<64-lowercase-hex>","release-tag":"","reviewed-at":"","run-attempt":"","run-id":"","source-revision":""} ``` -Expected: `rustc 1.95.0 (...)`, `wasm32-wasip1` present, `fastly` reports 15.1.0, all five tools resolve, and the read-only/non-root run succeeds. - -- [ ] **Step 4: Commit** -```bash -git add .github/docker/build-app-cli/Dockerfile .github/docker/build-app-cli/image.json -git commit -m "build-cache container: pinned single-arch Dockerfile + image pin record" -``` - ---- - -### Task 3: Publish workflow (build, push, record digest) - -**Files:** -- Create: `.github/workflows/publish-build-container.yml` - -**Interfaces:** -- Consumes: `.github/docker/build-app-cli/Dockerfile`, `check-image-pin.sh`. -- Produces: a GHCR image `ghcr.io/stackpop/edgezero-build-app-cli` whose **manifest digest** is written back to `image.json` on the release tag; consumed by sub-plans 2–4. - -- [ ] **Step 1: Write the workflow** - -```yaml -# .github/workflows/publish-build-container.yml -name: Publish build container -on: - push: - tags: ["build-container-v*"] -permissions: - contents: write # push the pin branch - packages: write # push the image to GHCR - pull-requests: write # open the image.json PR -jobs: - publish: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v7 - # Trusted publish job (no app code runs here); keep the token so the - # pin-record PR branch can be pushed. - with: - persist-credentials: true - - name: Log in to GHCR - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin - - name: Build and push (single-arch amd64) - id: push - run: | - set -euo pipefail - REPO="ghcr.io/stackpop/edgezero-build-app-cli" - TAG="${GITHUB_REF_NAME}" - docker buildx build --platform linux/amd64 \ - --provenance=false --sbom=false \ - --tag "$REPO:$TAG" --push .github/docker/build-app-cli - DIGEST=$(docker buildx imagetools inspect "$REPO:$TAG" --format '{{json .Manifest.Digest}}' | tr -d '"') - echo "digest=$DIGEST" >> "$GITHUB_OUTPUT" - - name: Verify the pushed image BY DIGEST before recording it - env: - REPO: ghcr.io/stackpop/edgezero-build-app-cli - DIGEST: ${{ steps.push.outputs.digest }} - run: | - set -euo pipefail - REF="$REPO@$DIGEST" - # Single-manifest linux/amd64 (reject a multi-arch index). - n=$(docker buildx imagetools inspect "$REF" --format '{{json .}}' \ - | jq '[.. | .manifests? // empty | .[] | select(.platform.os != "unknown")] | length') - [ "${n:-1}" -le 1 ] || { echo "::error::not single-manifest ($n)"; exit 1; } - # Anonymous pull (the package must be public) + the runtime smoke contract. - docker logout ghcr.io || true - docker run --rm --platform linux/amd64 "$REF" rustc --version | grep -F '1.95.0' - docker run --rm --platform linux/amd64 "$REF" sh -c 'rustc --print target-list | grep -qx wasm32-wasip1' - docker run --rm --platform linux/amd64 "$REF" fastly version - docker run --rm --platform linux/amd64 "$REF" sccache --version - docker run --rm --read-only --tmpfs /tmp --user 1001 --platform linux/amd64 "$REF" rustc --version - - name: Open a reviewable image.json PR (not an in-place commit) - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DIGEST: ${{ steps.push.outputs.digest }} - run: | - set -euo pipefail - f=.github/docker/build-app-cli/image.json - jq --arg t "${GITHUB_REF_NAME}" --arg d "${DIGEST}" '.tag=$t | .digest=$d' "$f" > "$f.tmp" && mv "$f.tmp" "$f" - bash .github/docker/build-app-cli/check-image-pin.sh "$f" - br="build-container-pin-${GITHUB_REF_NAME}" - git switch -c "$br" - git add "$f" - git -c user.name=edgezero-ci -c user.email=ci@stackpop \ - commit -m "build container: pin ${GITHUB_REF_NAME} = ${DIGEST}" - git push -u origin "$br" - gh pr create --fill --base main --head "$br" \ - --title "Pin build container ${GITHUB_REF_NAME}" \ - --body "Digest verified by the publish workflow (single-manifest, anonymous pull, runtime smoke)." + Run it from the verified `G` checkout; it must pass before App-token creation. Require exactly one + protocol-prefixed record claiming the current run id/attempt, exact current challenge and `D`, and + approved state; any second or mismatched current-attempt record fails. The API reviewer login is + the evidence approver. Earlier attempts never satisfy the current `github.run_attempt`. + +- [ ] Implement and fixture-test `update-image-pin-pr.sh` in the gate. Let `I` be the + current protected-base source pin. Permit first pin, `I==S`, or `I` ancestor + of `S`; reject older or incomparable `S`. Fully enumerate open pin PRs and require the exact + authenticated App author id/login plus head repository `stackpop/edgezero`; an actor/repository + collision fails. Close older proposals when superseding them, make an older run a mutation-free superseded + success, and fail malformed, incomparable, multiple-same-source, or ambiguous state. +- [ ] Test absent branch, exact remote OID, force-with-lease race, one open PR, closed-unmerged exact PR, + already-merged exact record, same-`S`/new-`D`, forward source, older source, + incomparable source, newer existing PR, stale PR after newer merge, missing head repository, + wrong author/repository collision, pagination/API/reopen failure, and idempotent rerun. No test + may depend on the real network. +- [ ] Write `check-build-container-publisher.test.sh` and then its structural checker. It rejects + changes to the gate-owned publisher topology, permissions, concurrency group, action versions, + gate checkout, helper paths, output set, token ordering, package deletion/admin scope, build secret + exposure, missing/late release-state and rotation checks, predictable/hard-coded/pre-verification + challenge generation, or pin mutation outside the trusted updater. Apply the same exact + concurrency/group/queue checks to the gate-rotation workflow and reject any third workflow using + that group or either approved workflow using a different one. +- [ ] Create gate-owned `publish-build-container.yml`. It triggers only protected + `build-container-v*` tags and uses exact concurrency group + `edgezero-build-container-publication` with `cancel-in-progress: false` and `queue: max`. + Document and test one running plus at most 100 pending runs; a run rejected at capacity publishes + no pin and must be rerun. Run it through the exact structurally validated actionlint + compatibility wrapper. +- [ ] Create gate-owned `rotate-build-container-gate.yml`, dispatched only from protected main at old + `G`, with the same exact workflow-level concurrency contract. Its unprivileged acquire job runs + after older publishers; its second job waits on secret-free + `build-container-gate-rotation-lock` with `deployment:false`, parses the exact current-run + rotation approval/evidence, and releases only after activated or rolled-back final state. Add a + live prerequisite fixture proving a publisher queued behind the waiting lock starts no build or + push. A canceled/expired lock may release concurrency only after the release-state variable and + absent tag policy keep later publishers fail-closed. +- [ ] Split the publisher into `build-and-verify` and `update-pin`. + `build-and-verify` has no environment, permissions only contents/read and packages/write, + no App key, and after anonymous verification generates 32 OS-CSPRNG bytes, exposes the 64-lowercase- + hex `approval-challenge`, and writes exact `{challenge,S,D,tag,run-id,run-attempt}` to its job + summary. Its outputs are only `{S,D,protocol,tag,approval-challenge}`. `update-pin` has the protected environment, no image build, initial + actions/read and contents/read, and executes approval/updater helpers only from a separate checkout + of repository variable `EDGEZERO_BUILD_CONTAINER_GATE_SHA`, whose value is `G`. + `build-and-verify` separately checks out `S` only to validate the isolated release request and + manifested-byte equality, then uses `stage-build-context.sh` to build solely from copied `G` + inputs. After acquiring shared concurrency and before registry login/build, require release state + exact `enabled`, active gate consistency, and no active rotation-lock run. A candidate Dockerfile + or unmanifested path is never in the Docker context. Each job invokes exactly one fixed + `assert-exact-publisher-context` step from `G` before sensitive work and verifies tag event/ref, + `github.sha==github.workflow_sha==S`, run id/attempt/tag, active gate, and enabled release state. +- [ ] Pin checkout to `actions/checkout@v7.0.1` and App-token creation to + `actions/create-github-app-token@v3.2.0`. + The App token is requested only after approval-gate success and only for repository `edgezero` with explicit contents/write + and pull-requests/write. Require returned installation id to equal the protected variable and rely + on mandatory post-step revocation. +- [ ] Add a static workflow test rejecting package-delete endpoints, `delete:packages`, + package-admin tokens, cleanup jobs, candidate helper execution, and any token before anonymous + image verification. + +### 6.5 Land and configure `G` + +- [ ] Run all Task 0-2 focused tests, Rust checks, the pinned actionlint `1.7.12` compatibility + wrapper, shellcheck, and + `zizmor --offline` from a clean checkout. Run every current format/test CI matrix job. +- [ ] Obtain an independent security review of the exact gate-owned path manifest, workflow source, + API allowlists, App-token ordering, and fail-closed/no-op markers. +- [ ] Merge the gate-only PR through the repository's existing protected process. Record the exact + default-branch commit as `G`. This bootstrap is a human-reviewed trust-root operation; + candidate-controlled checks are not evidence for it. + +**Gate checkpoint:** stop before opening the source candidate. + +- [ ] Set repository variable `EDGEZERO_BUILD_CONTAINER_GATE_SHA` to full SHA `G`. + Set `EDGEZERO_BUILD_CONTAINER_RELEASE_STATE` to exact `enabled`. Configure secret-free + environment `build-container-gate-rotation-lock` for protected main only, with required + reviewers, self-review and administrator bypass disabled, no custom protection App, and no + workflow reference to any environment secret or variable. + Set repository variables `EDGEZERO_BUILD_CONTAINER_PUBLISHER_APP_ID`, + `EDGEZERO_BUILD_CONTAINER_PUBLISHER_BOT_ID`, and + `EDGEZERO_BUILD_CONTAINER_PUBLISHER_BOT_LOGIN` to the independently verified dedicated App and + bot identity; require the App id to equal the protected-environment App id and pin-branch + ruleset Integration actor. + Configure active organization ruleset `edgezero-build-container-required-workflow` with target + `branch`, no bypass actors, repository-id condition exactly `[]`, ref-name include + exactly `["refs/heads/main"]`, no ref excludes, and exactly one `workflows` rule containing + `do_not_enforce_on_create=false` and exactly one required-workflow descriptor + `{repository_id:,path:".github/workflows/build-container-ci.yml",sha:G}`, + with no `ref`. Enable repository immutable releases, then require the versioned REST endpoint + to return HTTP 200 with `enabled` exactly boolean `true` and `enforced_by_owner` present as a + boolean; record both fields without requiring a byte-exact or one-field JSON object. +- [ ] Configure repository ruleset `edgezero-build-container-main` at target `branch`, enforcement + `active`, no bypass actors, include exactly `refs/heads/main`, exclude none, and exactly two + rules. Its pull-request rule allows only squash, dismisses stale reviews, requires code owners, + requires a distinct last-push approval, two approvals, and resolved threads. Its merge queue is + exactly `check_response_timeout_minutes:60`, `grouping_strategy:ALLGREEN`, + `max_entries_to_build:1`, `max_entries_to_merge:1`, `merge_method:SQUASH`, + `min_entries_to_merge:1`, and `min_entries_to_merge_wait_minutes:0`. Add fake payloads for every + missing/wrong parameter and prove each fails. Configure both image-tag and action-version-tag + creation-only/team-bypass and update/delete/no-bypass ruleset pairs with exact names, repository + source, tag target, ref include/no-exclude conditions, rule arrays, and actors; protected environment; dedicated GitHub + App; App-only canonical pin-branch ruleset; publisher App/bot repository variables; audit + identities; and final package policy from design Section 8. +- [ ] From a clean detached checkout at exact `G`, run the prerequisite verifier in configuration-only mode and preserve independently reviewed + evidence for the exact ruleset payload, repository variable, merge queue, environment, App, + audit-token screens, and team. Reopen or synchronize the later source PR after activation so its + authoritative required-workflow run is not stale. + +### 6.6 Prove gate rotation and recovery before release + +- [ ] Add fixture/integration tests for a gate-update PR. Old `G` must require the protected base tree + to equal old `G`, classify exactly `mode=gate-update`, accept changes only in the union of old and + candidate manifests, validate canonical candidate manifest and CODEOWNERS coverage as inert data, + reject mixed release-request/pin/non-gate changes, and never execute a candidate helper. +- [ ] Exercise the activation state machine with fake configuration APIs: quiesce publication, remove + by dispatching the old-`G` rotation workflow and waiting until it holds publication concurrency; + set release state `disabled::`, remove the sole tag deployment policy, merge + through the one-entry queue, record `G'`, run the clean + detached `G'` suite and exact post-merge assertion, update the repository variable and + organization descriptor after updating the marker with `G'`, verify both plus the base manifested + tree, restore the tag policy, produce fresh evidence, set state enabled, and approve the lock with + the exact evidence-bound comment. Every intermediate pointer mismatch, publisher that starts + behind the lock, or malformed state/comment fails closed. +- [ ] Exercise rollback at every activation step. Release stays disabled; both pointers restore to old + `G`; ordinary work remains blocked while base contains `G'`; and a separately reviewed old-`G` + `mode=gate-rollback` must restore the manifested tree before release policy returns. Require + current base to be a valid failed `G'` tree, proposed manifested bytes to equal old `G`, diff to + stay within the two-manifest union, no release/pin/non-gate change, both pointers already old + `G`, release disabled, queue merge, and generic exact-head push evidence. If old `G` cannot + validate either side, require a new manual trust-root review with release disabled. Add fixtures + for every interrupted pointer/policy state. No bypass or mixed-pointer operating mode is + permitted. + +**Gate:** do not tag or publish from `G`. The next task cannot alter a gate-owned path. + +## 7. Task 3: Build, merge, and publish source revision `S` + +**Source-candidate files:** + +- Create only `.github/docker/build-app-cli/release-request.json`. +- No gate-owned, image-context, pin, or unrelated file changes. The Dockerfile, complete context, + publisher, and all credential-bearing helpers already exist at `G`. + +### 7.1 Qualify the isolated release request + +- [ ] Create byte-canonical RFC 8785 JCS with no trailing newline. The file contains this one data + line; the Markdown fence line break is not file content: + +```text +{"gate-sha":"","provenance-protocol":1,"release-tag":"build-container-v"} ``` -The publish thus **pushes → inspects by digest → verifies single-manifest + anonymous pull + the runtime smoke → then opens a reviewable `image.json` PR** — the pin the rest of the feature keys on is never recorded until it has been proven against the actual pushed digest. - -- [ ] **Step 2: Actionlint the workflow** - -Run: `actionlint .github/workflows/publish-build-container.yml` -Expected: no output. - -- [ ] **Step 3: Commit** +- [ ] From a clean detached `G`, use the gate-owned staging helper to create the canonical fresh + context, build the local image with the exact Dockerfile/arguments and revision label `G`, and + capture local image/config identity `L` through BuildKit `--iidfile`. Run the full gate-owned + leaf-platform, label, protocol, validator, and toolchain verifier against `L`. This bootstrap + image is local only, is not published, and is not the future digest `D`; no preexisting pinned + `G` image is assumed. +- [ ] Invoke only `write-release-request` by immutable local identity `L`, under the exact + networkless/credential-free/read-only `release-request-write` profile, with full active `G` and + the next never-moved tag. Copy its sole output into the candidate branch and remove its fresh + output parent. Test duplicate/extra/reordered keys, whitespace/newline, wrong G/protocol/tag, + leading-zero release number, existing output, cleanup failure, wrong/tagged local image, and any + second changed path. No shell or `jq` is a second canonical encoder. + +- [ ] Require the organization workflow at exact `G` to run both stable jobs. Verify its workflow + repository/path/SHA and candidate head SHA. The local job must verify the candidate's complete + gate/context tree equals `G`, stage context only from clean `G`, build with the candidate head as + revision label, and run the trusted verifier suite. Candidate Dockerfile, helper, and context + paths are never read. +- [ ] Require linux/amd64 leaf config, exact labels, checksum-pinned Fastly/sccache bytes, exact + installed command versions/paths, actual wasm compile, validator self-test, deterministic + archive bytes, every malformed fixture, controlled startup-loader behavior, and read-only/non- + root runtime with no network/capabilities and bounded memory/pids. +- [ ] Run the exact `write-expected`, package, validate, and binary-smoke mount profiles from + design Section 5. No shell or `jq` may produce expected identity. No + `/work/package` mount may appear. +- [ ] Execute the bounded credential smoke while protected main is still `G`: temporarily add literal + `main` as the sole extra environment deployment policy, dispatch with body `ref:"main"` and the + candidate PR number, exact head repository, and current full head SHA, approve and complete the + smoke, then remove `main`. Require the exact candidate-bound `run-name`, run event + `workflow_dispatch`, path, `head_sha=G`, and exactly one successful + `assert-exact-g-dispatch-context` step that resolves the PR API and compares all three inputs; + then require final tag-only policy and unchanged credential metadata. A new candidate commit + makes this evidence stale. + +### 7.2 Qualify and merge `S` + +- [ ] After policy restoration, an independent maintainer captures the administrator-bypass/final-policy + PNG. Enter the candidate in the mandatory merge queue. Require the final `merge_group` + execution of the organization workflow from `G` to pass. The queue payload must still have + single-entry build/merge limits and every exact review parameter. +- [ ] Merge only through that queue. Record the resulting default-branch full SHA as `S`. + Wait for the repository-local `build-container-ci.yml` latest-attempt push run. Its run API + record must have event `push`, exact path, and `head_sha=S`; both stable jobs must have + `head_sha=S`, success, and exactly one successful `assert-exact-main-push-context` step. The + immutable step internally proves `refs/heads/main`, `event.after==github.sha==S`, + `github.workflow_sha==S`, and active gate `G`. +- [ ] From a clean detached checkout at `G`, run the full release-prerequisite verifier after merge using the candidate PR number and exact + `G`/`S`. Attach canonical evidence and the byte-identical PNG to the merged PR. + A maintainer other than the verifier and screenshot reviewer recomputes the digest, reviews all + API evidence, and authorizes tag creation. + +**Release checkpoint 1:** no tag exists until exact-`S` push evidence and the three-person +preflight review pass. + +### 7.3 Publish digest `D` and propose the pin + +- [ ] A preflight-verified active member of the creation ruleset's sole bypass team + `edgezero-build-container-releasers` creates protected tag + `build-container-vN` at exactly `S`. +- [ ] `build-and-verify` checks out full history without persisted credentials; proves + `HEAD==S`, clean tracked/index/untracked/submodule state immediately before and after + release-request validation. It separately checks out clean exact `G`, proves every manifested + byte at `S` equals `G`, and stages a fresh context containing only canonical files copied from + `G`. Build with that context and gate-owned Dockerfile, `--platform linux/amd64`, exact + source/protocol args, + `--provenance=false`, `--sbom=false`, and BuildKit metadata output. +- [ ] Authenticate GHCR only through a fresh `DOCKER_CONFIG` outside build context. Pipe + `GITHUB_TOKEN` to login; never pass it as build arg, secret mount, image environment, or + context file. Parse `D` only from + `containerimage.digest` in the metadata file. +- [ ] Run trusted `G` verification while authenticated, remove local credentials/reference, + then pull and run `repository@D` with a new empty Docker config. The anonymous check must + issue a registry request. A private package fails before `update-pin`. +- [ ] On first publication, stop at the expected private-package failure. An operator makes the GHCR + package public, then reruns the exact `G` prerequisite verifier in package-present mode and + attaches its public-visibility/repository-link evidence before rerunning the same tag. Evidence + from the failed run attempt does not carry forward. + +**Release checkpoint 2:** after the image is public and anonymously verified, the environment approver +captures a fresh PNG and enters the exact design comment for the current +`{challenge,D,run-id,run-attempt,S,tag,png-sha256,reviewed-at}` before approving `update-pin`. + +- [ ] Check out exact `G` without persisted credentials, then require `release-approval-gate.sh` to + pass before App-token minting. + Then mint the exact repository-scoped App token, verify installation id, and invoke only + `G/update-image-pin-pr.sh`. +- [ ] Generate the exact five-field image record plus canonical JCS release-evidence record through + gate-owned typed writers. Use branch `edgezero-build-container-pin/` and exact title from the + design. Enforce protected-base and every-open-PR source ancestry. Use recorded remote OID and + exact force-with-lease; never blind force or accept caller-provided evidence bytes. +- [ ] Include `S`, `D`, protocol, platform, anonymous result, approval run attempt, + and evidence link in the PR body. Never include credential material or an AI byline. +- [ ] Require pin CI from protected `G` to recompute current-base ancestry; verify both files are the + only changed paths and agree; verify exact App bot author/id, head repository, protected branch, + PR title, and pin-branch ruleset; then poll and verify the bound publisher run, attempt, both + exact context steps, and current approval comment against the evidence file. Only afterward + anonymously verify the exact digest against labels, platform, tools, validator, target, + protocol, and package visibility. Candidate scripts, PR-body claims, and mutable tags are + forbidden. + +**Release checkpoint 3:** confirm the pin PR changes only the image/evidence pair, was authored and +pushed through the dedicated App's protected source branch, binds the successful publisher run and +approval, proposes a source not older or incomparable with the current base or another proposal, and +passed both protected jobs on its final merge-queue candidate. + +## 8. Task 4: Merge and verify pin baseline `B` + +- [ ] Merge the pin-only PR through the mandatory queue and record the resulting full commit as + baseline `B`, not final action revision `P`. +- [ ] Confirm the App push caused the required organization workflow to materialize and every latest + attempt passed. Confirm deletion or a syntactically valid but unverifiable/older/incomparable pin + fails in dedicated test PRs. +- [ ] From a clean checkout at `B`, rerun all Task 0-3 suites: ```bash -git add .github/workflows/publish-build-container.yml -git commit -m "build-cache container: GHCR publish workflow recording the manifest digest" +cargo test --manifest-path .github/tools/edgezero-provenance-validator/Cargo.toml +cargo fmt --manifest-path .github/tools/edgezero-provenance-validator/Cargo.toml --all -- --check +cargo clippy --manifest-path .github/tools/edgezero-provenance-validator/Cargo.toml \ + --workspace --all-targets --all-features -- -D warnings +cargo fmt --all -- --check +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test --workspace --all-targets +cargo check --workspace --all-targets --features "fastly cloudflare spin" +bash .github/actions/deploy-core/tests/run.sh +.github/actions/deploy-core/tests/check-action-pins.sh +.github/actions/deploy-core/tests/check-doc-action-pins.sh +ACTIONLINT_VERSION=1.7.12 scripts/install-actionlint.sh 1.7.12 +scripts/run-actionlint.sh +zizmor --offline .github/workflows .github/actions ``` -- [ ] **Step 4: Publish (operator step, out of band)** - -Tag `build-container-v1` and push it. The workflow pushes the image, **verifies it by digest** (single-manifest, anonymous pull, runtime smoke), and **opens a PR** updating `image.json` to the real `sha256` digest. Review and merge that PR — the digest is the pin the rest of the feature keys on, and it is only recorded after passing verification against the actual pushed image. - -**One-time GHCR visibility + retention (operator):** GHCR packages are **private on first publish** and there is no clean REST endpoint to flip a container package public, so set the package `edgezero-build-app-cli` to **public** in its GHCR package settings (or set the org's default package visibility) so consumers can **anonymously** pull by digest (spec §3.7), and enable a retention policy that never prunes a digest referenced by a committed `image.json`. Verify anonymous access: -```bash -docker logout ghcr.io -docker pull "ghcr.io/stackpop/edgezero-build-app-cli@$(jq -r .digest .github/docker/build-app-cli/image.json)" -``` -Expected: the pull succeeds without credentials. - ---- - -### Task 4: Wire the digest pin into the pin gate - -**Files:** -- Modify: `.github/actions/deploy-core/tests/run.sh` (add the validator suite) -- Modify: `.github/actions/deploy-core/tests/check-image-pin.test.sh` (add a reject-placeholder case) - -**Interfaces:** -- Consumes: `check-image-pin.sh`, `image.json`. -- Produces: a CI gate that fails if the build container is not digest-pinned (or is the all-zero placeholder), alongside the existing action-pin gate. - -- [ ] **Step 1: Add the failing placeholder-rejection test** - -Append to `check-image-pin.test.sh` (before the summary), a case asserting the real repo `image.json` is not the all-zero placeholder: - -```bash -REAL="$DIR/../../../docker/build-app-cli/image.json" -zero="sha256:$(printf '%064d' 0)" -if [ "$(jq -r '.digest' "$REAL")" = "$zero" ]; then - no "committed image.json is still the all-zero placeholder" -else - ok "committed image.json carries a real digest" -fi -``` - -- [ ] **Step 2: Run it to verify it fails** - -Run: `bash .github/actions/deploy-core/tests/check-image-pin.test.sh` -Expected: FAIL on "committed image.json carries a real digest" until Task 3's publish lands a real digest. - -- [ ] **Step 3: Invoke the suite from the contract runner** - -Add to `.github/actions/deploy-core/tests/run.sh` (near the other suite invocations): - -```bash -bash "$(dirname -- "${BASH_SOURCE[0]}")/check-image-pin.test.sh" -``` - -- [ ] **Step 4: Run the full suite** - -Run: `bash .github/actions/deploy-core/tests/run.sh` -Expected: the image-pin cases run and (after Task 3) pass. - -- [ ] **Step 5: Commit** - -```bash -git add .github/actions/deploy-core/tests/run.sh .github/actions/deploy-core/tests/check-image-pin.test.sh -git commit -m "build-cache container: gate the build-container digest pin in the contract suite" -``` - ---- - -## Self-Review - -- **Spec coverage (container scope only):** §3.7 image contract → Tasks 2/3; digest = `platform-id` → Tasks 2/3; single-manifest amd64 → Task 3 (`--platform linux/amd64`, single-arch); baked toolchain `1.95.0` → Task 2 + verify; digest pinned/checked (§5) → Tasks 1/4. The *use* of the container (reusable workflow, launcher, provenance) is sub-plans 2–4, out of scope here. -- **Placeholder scan:** the only intentional placeholder is the all-zero digest, which Task 3 overwrites and Task 4 forbids in a release — flagged, not silent. -- **Type consistency:** `check-image-pin.sh ` contract is used identically in Tasks 1, 3, 4; the `image.json` keys (`repository`/`tag`/`digest`) match across Tasks 1–4. - -## Downstream sub-plans (not written yet) - -2. Cached build path (reusable workflow + `prepare`/`compile` split + **owned `actions/cache` restore+save with the four-root prune** + config/source closure, spec §3.4/§3.8). 3. Provenance (JSON Schema + procedural validation, `validate-app-cli-provenance`, `compute-app-cli-identity`, `ExpectedIdentity`). 4. Consumer integration (`active-version-fastly`, per-consumer `ExpectedIdentity` inputs, the Docker launcher, production-only recovery). Each is its own plan; sub-plan 2 consumes this container's digest as `platform-id`. +- [ ] Require every current hosted format/test matrix job, including all wasm clippy/test legs. Local + commands do not replace runner-backed checks. +- [ ] Pull `repository@digest` anonymously after merge and rerun the complete image verifier + from the committed record. +- [ ] Record `{G,S,D,B,protocol,tag}`, workflow/ruleset ids, action versions plus their reviewed + resolved commits, tool checksums, and + evidence digests in the release record. + +**Gate:** downstream plans build on `B` and never use `S` as an action ref or derive +a digest from the mutable tag. + +## 9. Task 5: Runbook and downstream-plan handoff + +- [ ] Document publication concurrency accurately: one running and at most 100 pending runs under + `queue: max`; a run rejected at capacity publishes no pin and must be rerun. +- [ ] For every `update-pin` attempt, record run id, run attempt, exact approval comment, API + reviewer login, challenge, image digest, source, tag, review time, PNG basename/digest, and final evidence attachment. + Earlier-attempt or pre-`S` evidence is invalid. +- [ ] Document that GHCR has no enforceable per-version retention lock and repository automation has no + package-deletion credential or endpoint. Manual administrator deletion is accepted operational + risk; recovery is a new source/image verification/pin release. +- [ ] Document rollback as selecting an earlier reviewed exact action version containing its + corresponding pin. + Never regress protected-main `image.json` and never move a release tag. +- [ ] Review and approve these four sibling plans before implementing post-`B` behavior: + `2026-08-20-build-cache-actions.md` (cache key, restore/save authorization truth table, + sccache lifecycle/audit), `2026-08-20-build-cache-provenance.md` (typed expected producer, + package/validate handoff and two-job topology), + `2026-08-20-build-cache-launcher-providers.md` (container profiles, source freeze, + nested-project Fastly `bin`/`pkg` output and cleanup, provider lifecycle), and + `2026-08-20-build-cache-consumer-adoption.md` (consumer workflow, docs, migration, and + final `P`/`V` qualification plus documentation revision `R`). +- [ ] Each follow-on plan must assign the design's deferred fixtures and tests before final action + revision `P`: workspace/suffix vectors, format-independent cache tree bounds and entry count, + exact cache action versions, cache lookup/save truth tables, sccache response-loss semantics, + mount/environment matrices, empty Cargo-config policy, path confinement, implicit nested + `bin`/`pkg` ownership and cleanup, exact app-env allow/deny boundaries, controlled-loader argv, + artifact identity, and consumer recomputation. +- [ ] In the consumer plan, keep the four prepublication documents at the gated placeholder while + candidate `H` is tested through exact version `C`. Designate `P` only after the exact-main local + suite and immutable candidate-version hosted suite pass, then select and publish immutable `V` + at `P`. Only after the literal-`V` smoke passes, merge documentation-only `R` adding the exact + `{V,P}` record and replacing every placeholder with `V`; the preinstalled dual-state scanner + then remains permanently in released mode. Every concrete third-party and final EdgeZero + `uses:` ref is a reviewed exact patch version; no major/minor tag, branch, or commit SHA appears. + +## 10. Completion review + +Before declaring this plan complete, run two independent reviews: + +1. **Contract review:** compare every file and test with design v6.26 Sections 2 through 10. Verify one + expected/package/validate wire authority, protected gate and image source `G`, isolated release + request `S`, staged gate-only Docker context, API-visible exact post-merge `S` proof, forward-only + pin ancestry, no tag runtime pull, no placeholder image digest/checksum, no `/work/package` + convention, and no legacy `--stage` guidance. +2. **Release-adversary review:** test candidate workflow/helper substitution, forbidden + major/minor/branch/SHA action refs, third-party exact-tag movement as recorded risk, immutable + EdgeZero release enforcement, private + package state, stale/older/incomparable pin PRs, malformed BuildKit metadata, indexes, wrong + platform/labels/versions/protocol, deleted pin, classifier/completion failure, workflow-source + mismatch, missing bypass fields, API redirect/path/header/version confusion, approval reruns, token + ordering, gate rotation/recovery, actionlint queue compatibility, publication queue overflow, and + concurrent release attempts. + +The container plan is complete only when protected gate `G`, source `S`, verified +digest `D`, and pin baseline `B` are recorded and all repository gates pass. The +four remaining plans may then implement caching and designate final action revision `P` only +after their complete contract suites pass. diff --git a/docs/superpowers/plans/2026-08-20-build-cache-launcher-providers.md b/docs/superpowers/plans/2026-08-20-build-cache-launcher-providers.md new file mode 100644 index 00000000..a7e6acb1 --- /dev/null +++ b/docs/superpowers/plans/2026-08-20-build-cache-launcher-providers.md @@ -0,0 +1,207 @@ +# Build Cache Launcher and Provider Implementation Plan (plan 4 of 5) + +> **Execution:** Start after plan 3 is merged and its provenance gate revision is active. Add the +> launcher/provider adversarial tests to a new gate revision before changing action behavior. + +**Goal:** Run the validated app CLI through closed container profiles, freeze every source byte that +can reach a credentialed provider command, and preserve the parent deploy lifecycle without ambient +host state or the legacy `--stage` spelling. + +**Spec:** `docs/superpowers/specs/2026-08-20-edgezero-deploy-build-caching-design.md` v6.26 Sections +2, 5, 6.1, 6.6, 7.2, and 9. The parent deploy spec remains normative where the addendum does not +expressly replace it. + +## 1. File and ownership boundaries + +- Modify the provider composites under `.github/actions/{deploy-fastly,healthcheck-fastly, +rollback-fastly,config-push-fastly}` and their shared `.github/actions/deploy-core` helpers. +- Create `.github/actions/active-version-fastly/action.yml`; preserve plan 3's no-output + `.github/actions/validate-app-cli-provenance/action.yml` interface while extending its shared runner + with provider profiles. +- Keep JSON/environment validation, source inventory, output-root ownership, container argv, binary + recheck, token-ordering, and cleanup in shared typed or narrowly scoped helpers. Provider composites + may select profiles; they may not reconstruct mount lists or environments ad hoc. +- Do not modify the protocol JSON/archive encoder in this plan. A protocol change returns to plan 3 + and requires a new image/gate/pin sequence. + +## 2. Gate update first + +- [ ] Add structural fixtures for every operation/profile pair, including exact image digest, network, + uid/gid, read-only root, capabilities, security options, tmpfs, mounts, environment names, + resource limits, timeout, command, and token presence. +- [ ] Add hostile source/output fixtures for deleted or modified tracked paths, submodule drift, + escaping symlinks, mount substitution, overlapping roots, tracked descendants, hardlinks, + sparse files, special files, nested mounts, inode replacement, cleanup races, and undeclared + generated output. +- [ ] Add provider lifecycle fixtures for production, staging, first deploy, unhealthy deploy, + rollback, stale rollback refusal, lost version, cancellation, config push, and mutation output. +- [ ] Prove candidate changes cannot weaken the profile tables, source-freeze checks, binary recheck, + token boundary, or exclusive `--staging` spelling, then land and activate the new gate revision + using plan 1's rotation procedure. + +## 3. Closed input and environment construction + +- [ ] Consume plan 2's sole duplicate-rejecting `app-env` decoder with the exact v1 limits; do not + introduce a provider-local parser. Require JSON object only, + valid UTF-8 and at most 65,536 raw bytes before parsing, at most 64 entries, at most 32,768 + aggregate UTF-8 bytes, 1..127-byte ASCII names matching + `[A-Za-z_][A-Za-z0-9_]*`, values at most 8,192 UTF-8 bytes with no NUL/C0/DEL, and + ASCII-case-insensitive exact/prefix/target-tool deny rules from the design. +- [ ] Start every operation with `env -i`; emit only the fixed `PATH`, action-owned `HOME`/`TMPDIR`, + the operation's exact Rust/Cargo variables, validated `app-env`, selected typed `EDGEZERO_*` + values, and the one provider token required by that profile. Reject duplicate final names and + prove caller `PATH`, shell startup variables, wrappers, flags, and ambient workflow variables do + not survive. +- [ ] Consume plan 2's shared empty Cargo-config and toolchain checks over the cwd, every ancestor through git root, + all copied enclosing workspace directories, and fresh `CARGO_HOME`. Reject either Cargo config + filename and legacy credentials before app code starts. +- [ ] Require the explicit `rust-toolchain` input to equal the pinned image toolchain and confine all + path dependencies beneath git root. Test exact, missing, inferred-only, nested-workspace, and + mismatch cases. + +## 4. Runner and mount profiles + +- [ ] Refactor `run-app-cli-in-container` to accept a closed operation enum, not caller-provided + Docker flags, mounts, environment names, network settings, entrypoint, or command prefix. Build + argv as an array and reject newline/NUL/control-bearing scalar inputs before logging. +- [ ] Serialize each validated operation environment to one mode-0600 single-link private env file; + pass only its path to `docker create`, remove and verify the file before `docker start --attach`, + and remove the uniquely named container on every exit. Never place a token or app-env value in + Docker argv. Test create/start/attach failure, env-file replacement, newline/NUL values, + container-name collision, inspectable lifetime, and mandatory removal/reconciliation. +- [ ] Implement the exact mount table from design Section 5.3. Never mount all of `RUNNER_TEMP`, the + original checkout writable, the Docker socket, host credential directories, GitHub file-command + files, or a cache in a token-bearing operation. +- [ ] Enforce the fixed hardened runtime: pinned digest, linux/amd64, uid/gid 1001, read-only root, + dropped capabilities, `no-new-privileges`, operation-local tmpfs, and the exact design table's + bridge/none network, memory/no-extra-swap, pid, and wall-time values. Reject Docker host, + container-sharing, caller-selected network, resource, and timeout flags. +- [ ] Before every app-binary launch, reopen and verify the confined regular path, device/inode, + SHA-256, size, mode 0755, and link count one. Dynamic binaries run only by direct argv through + `/lib64/ld-linux-x86-64.so.2 --inhibit-cache --glibc-hwcaps-mask '' --library-path +/opt/edgezero/runtime-lib `; static binaries execute directly. Never use a shell, + `PATH` lookup, implicit kernel interpreter launch, `ld.so.cache`, default library directory, + preload file, or hardware-capability substitution. +- [ ] Cover static/dynamic success, wrong interpreter, dependency replacement, hwcaps/default/cache/ + preload substitution, inode swap, symlink/hardlink replacement, malformed ELF, and the explicit + non-claim for post-startup `dlopen` and child-process behavior. + +## 5. Source freeze and generated outputs + +- [ ] Define `deploy-fastly` and `config-push-fastly` as the only source-bearing public actions. Give + each the design's exact repository/ref/id/workspace/cwd/package/bin/toolchain inputs plus required + sensitive string `app-checkout-token`, supplied from a GitHub secret and masked before use. At + action start, independently materialize one action-private authority with plan 2's trusted + object-first helper, verify all five supplied + `CallerExpectedIdentity` fields, and remove the checkout credential channel before application + code, provider-token creation, or provider-token injection. Never accept or output an authority + path, descriptor, or opaque handle from another action. Source-free actions accept none of these + materialization inputs and no checkout token. +- [ ] For `deploy-fastly`, build Copy B with plan 2's trusted exporter from that action-local, + credential-free authority. It is a faithful, private, `.git`-free, non-hardlinked, recursive, + non-sparse copy containing only tracked files and initialized submodules. Keep the authority + read-only and verify repository id, exact HEAD, index/worktree cleanliness, gitlinks, permitted + no-filter or pinned-LFS materialization, modes, symlink targets, and full inventory before and + after app-controlled work. Create one Copy B per invocation; reuse it only among that + `deploy-fastly` invocation's internal operations, and never share it or its output roots with + another action. +- [ ] Make `config-push-fastly` the explicit no-Copy-B exception. It executes no app code and mounts + only its independently materialized credential-free frozen authority read-only. Record and + verify authority HEAD, index/worktree state, full inventory, and selected tracked + manifest/file-config identity before token creation, immediately before container start, and + after the command; apply equivalent identity checks to an action-owned inline config. Add + substitution/race fixtures at every boundary and fail on any authority, manifest, or config + change. +- [ ] Parse `generated-output-paths` as the exact bounded canonical JSON array. Reject raw input over + 65,536 bytes, duplicates, overlap, root/dot/git paths, tracked path ancestors, existing roots, + non-UTF-8 or noncanonical + segments, components over 255 bytes, joined host paths over 4,096 bytes, escaping parents, and + any parent reached through a symlink. +- [ ] Resolve the selected `fastly.toml` before app code runs and add exactly the implicit + `/bin` and `/pkg` roots. Apply the same collision, + absence, parent-confinement, and ownership rules; do not substitute workspace-root paths. +- [ ] Create every root empty with mode 0700, record its device/inode, and expose repository write + access only through the corresponding nested writable bind mount beneath read-only + `/work/repo`. Audit after every app-controlled command and immediately before each token-bearing + command. +- [ ] Accept beneath roots only real directories and non-sparse regular uid/gid-1001 single-link + files. Reject symlinks, hardlinks, sparse files, devices, sockets, FIFOs, mounts, ownership + changes, root replacement, and new paths elsewhere. Across all roots enforce checked totals of + at most 2,147,483,648 logical bytes and 100,000 descendants plus the component/path bounds. + Compare all other Copy B bytes/modes/gitlinks to the frozen authority. +- [ ] Implement descriptor-relative, no-follow cleanup on success and failure. Remove only recorded + action-owned trees, then verify roots are absent and both original and Copy B satisfy their final + inventories. Cleanup or post-cleanup failure is fatal; a preexisting root is never adopted. + +## 6. Credential-free app build and parent target cache + +- [ ] Under `build-mode: always`, run exactly one credential-free app-build profile before provider + deploy. It receives the validated binary, Copy B, fresh Cargo home, action-owned target path, + validated app environment, and declared/implicit output roots, but no provider token or sccache. +- [ ] If the parent `deploy-fastly.cache` option is enabled, restore its exact-key Cargo target cache + only after plan 2's lookup-eligibility predicate passes, audit it under the parent's contract, + and save it after successful credential-free app-build and source/output audit only when plan + 2's full protected-event save predicate passes. Cross-repository use without disclosure + acknowledgement fails before restore. Save must finish before token minting or injection and is + never retried or attempted after any token-bearing command. Commit independent lookup/save + truth-table integration tests for this cache family. +- [ ] Under `build-mode: never`, perform no target-cache restore/save and no credential-free build. + Provider deploy may still compile with the token for either mode; never claim app-build prevents + that compile and never expose its token-bearing outputs to a cache. +- [ ] Test cache hit/miss/save warning, build failure, audit failure, cancellation, and token-order + traces. Every path either saves before token introduction or performs no save. + +## 7. Provider lifecycle actions + +- [ ] Make every public provider action accept the named artifact, trusted producer + `action-version`, and complete `CallerExpectedIdentity`; require its own action repository/ref to + equal that version, derive PlatformIdentity locally, independently download/validate/smoke the + artifact, and remove its private workspace with `if: always()`. The two source-bearing actions + also accept exactly the action-local materialization inputs from Section 5 and independently + verify caller identity before use. No action accepts a host binary or authority path, exposes + one as a public output, or trusts another action's platform or materialization state. The + validation-only action has no outputs and removes its extracted binary before success returns. +- [ ] `active-version-fastly` is source-free and token-bearing. Return an empty version only when the + provider response confirms a first production deploy; reject malformed, ambiguous, or absent + state otherwise. +- [ ] `deploy-fastly` validates one binary and reuses it only within that action invocation. Preserve + the parent's production/staging sequence, rollback target capture, healthcheck order, + reconciliation, and output semantics. Production probes are tokenless; staging probes receive + only the token required for the staged endpoint. +- [ ] `healthcheck-fastly` remains source-free. Split production and staging token profiles and reject + an unexpected token in production rather than silently ignoring it. Enforce retry 1..20, delay + 0..300 seconds, per-attempt timeout 1..300 seconds, checked total-budget arithmetic, and the + 3,600-second maximum before launch. +- [ ] `rollback-fastly` remains source-free, validates the rollback target against current provider + state, publishes `mutation-attempted` immediately before launch, and reconciles cancellation or + lost output according to the parent contract. +- [ ] `config-push-fastly` mounts its action-local frozen repository read-only, confines the selected + manifest and file-backed config directly from that credential-free authority without creating + Copy B, or creates exactly one action-owned read-only inline file. Derive only the typed named + config overlay; `no-env` exposes none. Assign the pre-token, pre-start, and post-command + authority/manifest/config identity checks from Section 5 to this action and prove replacement + races fail. +- [ ] Every mutating action sets `mutation-attempted` host-side before starting the mutating CLI, + names the container, forwards termination within bounded time, and runs post-cancellation + reconciliation. No mutation can precede provenance, binary, source, output-root, and token-order + checks. +- [ ] Remove every implementation, fixture, and document path that accepts or emits legacy `--stage`. + Add a repository-wide negative test and use only `--staging` for staged CLI invocations. + +## 8. Verification and merge + +- [ ] Run deploy-core and every provider action suite; protocol and image tests; source/output hostile + fixtures; shellcheck; `scripts/run-actionlint.sh`; zizmor; repository Rust tests; and docs/pin + checks. +- [ ] Run Docker-backed integration tests for every profile and provider lifecycle on linux/amd64, + including read-only root/non-root behavior, exact mount/environment snapshots, controlled + loader argv, network denial, signal handling, cleanup, and mutation reconciliation. +- [ ] Review logs, summaries, outputs, cache contents, and artifacts for token/app-env/path leakage. + Redaction is not proof: tests assert the sensitive bytes were never supplied to disallowed + processes or persistence surfaces. +- [ ] Merge through the one-entry queue, record the resulting commit and active gate revision, and + hand both to plan 5. Do not designate final action revision `P` yet. + +**Gate:** provider actions execute only an independently validated artifact under a closed profile; +no credentialed command can consume source or generated output that escaped the frozen inventory, +and no post-token byte can enter either cache family. diff --git a/docs/superpowers/plans/2026-08-20-build-cache-provenance.md b/docs/superpowers/plans/2026-08-20-build-cache-provenance.md new file mode 100644 index 00000000..070982be --- /dev/null +++ b/docs/superpowers/plans/2026-08-20-build-cache-provenance.md @@ -0,0 +1,179 @@ +# Build Cache Provenance Implementation Plan (plan 3 of 5) + +> **Execution:** Start after plan 2 is merged and its gate revision is active. Rotate the gate before +> landing any new protected test/helper, then keep implementation changes separate. + +**Goal:** Make the reusable workflow the sole build-only producer of a deterministic protocol-1 app +CLI artifact and make every consumer independently validate exact caller and platform identity before +the binary can execute. + +**Spec:** `docs/superpowers/specs/2026-08-20-edgezero-deploy-build-caching-design.md` v6.26 Sections +3, 5.3, 6, 7, and 9. + +## 1. Fixed actions and boundaries + +- Pin upload to exact `actions/upload-artifact@v7.0.1` and download to exact + `actions/download-artifact@v8.0.1`. +- The artifact payload is the validator-produced deterministic ustar file. Artifact service wrapping + is transport only and is never parsed as provenance. +- The producer accepts no provider token/input and performs no provider mutation. The app checkout + token is host-only and absent before any container starts. +- `expected.json` has one typed producer: baked `edgezero-provenance-validator write-expected`. + Shell, jq, workflow expressions, and generic JSON writers do not encode either protocol document. +- Validation remains two invocations: trusted parse/extract, host output check, then hardened + credential-free binary smoke. +- Plan 1 already implemented and baked the protocol owner, schema, canonical golden/malformed + fixtures, and compiled fixture manifest into pinned image `D`. This plan integrates those bytes and + may add only host-side workflow/action fixtures outside the canonical image context. It must not + change the protocol crate, schema, baked fixtures, Dockerfile, image-context manifest, or `image.json`. + A defect in any of those stops this plan and requires a new gate/source/image/pin cycle before work + resumes. + +## 2. Gate update and integration fixtures + +- [ ] Hash and consume the already reviewed independent golden bytes for exact `expected.json`, + `app-cli-meta.json`, deterministic ustar, extracted binary, and every schema/duplicate/JCS/path/ + size/ELF failure. Add host-side integration assertions that do not call the production encoder as + their oracle and do not edit or duplicate the baked fixture authority. +- [ ] Add workflow/action structural tests for build-only permissions, exact-version action pins, checkout + credential removal, artifact-name uniqueness, no platform output, typed identity output, exact + EdgeZero/app checkout separation, no app-relative local action resolution, exact mount split, + cleanup on every exit, and no provider input or secret. +- [ ] Add substitution fixtures: caller-supplied expected JSON, alternate schema/fixtures, candidate + validator, shell-generated JSON, tar implementation, binary execution in parser container, + writable parser input, and a second downloaded artifact must all fail. +- [ ] Land and activate this gate update using the plan-1 rotation and rollback procedure before + modifying producer or consumer actions. Prove its changed paths are outside + `image-context-paths.txt`; otherwise stop for a new image release. + +## 3. Caller identity and authority export + +- [ ] Consume plan 2's sole identity-calculation helper over a validated authority + checkout; do not add another identity implementation. Require exact + repository ID from authenticated GitHub API, full lowercase source SHA, workspace/cwd + containment, tracked regular lockfile, and credential-free locked Cargo metadata agreement. + Derive the package version from that same metadata result; no caller input may override it. +- [ ] Produce exact bounded package/bin names and the design's length-framed workspace hash. Reject + non-UTF-8/escaping paths, symlink roots, malformed repository ID/SHA, duplicate outputs, and + submodule mismatch. +- [ ] Invoke plan 2's trusted authority materializer: fetch exact app/submodule commits without a + worktree, inspect all committed filter/config/submodule policy before it can execute, then + checkout and materialize only through the absolute verified Git LFS binary. Remove credentials + and their host channel before Copy A or any container exists. Produce tracked/submodule-only, + `.git`-free, non-hardlinked Copy A. Prove forbidden filters/hooks/origins are never reached and + the token never appears in Copy A, argv, environment, mounted data, logs, artifacts, or cache; + reverify the read-only authority after compilation. + +## 4. Expected identity and package + +- [ ] Derive `PlatformIdentity` only from the invoking action revision's validated `image.json`. + Reject tags, indexes, malformed records, mismatched protocol, and caller overrides. +- [ ] Invoke `write-expected` in the exact expected-write profile with only fresh `/work/expected` and + tmpfs. Require canonical `/work`, the baked schema, create-new/no-replace publication, exactly one + regular output, and complete host cleanup after abnormal exit. +- [ ] Select `cached-compile` only when validated boolean `cache:true`; select `uncached-compile` for + the default `cache:false`. Compile the named app CLI in that selected profile and retain its + exact host digest, size, mode, path, and single-link identity. Invoke `package` with either + profile's output plus read-only expected input and a fresh `/work/packaged`; require exactly one + deterministic `artifact.tar`. Test cache-on and cache-off producer/package paths independently. +- [ ] Run `package` twice over identical inputs and compare bytes. Validate archive member order, + headers, modes, owner fields, checksums, padding, end blocks, size caps, metadata digest/ELF + closure, and no extra filesystem entry. +- [ ] Validate `app-cli-artifact` as 1..128 ASCII bytes matching the design regex and unique in the + run. Upload exactly the literal file path with `archive:true`, `compression-level:0`, + `include-hidden-files:false`, `if-no-files-found:error`, `overwrite:false`, and + `retention-days:1`; require nonempty artifact id/digest outputs and no wildcard/multiple path. + +## 5. Reusable workflow interface + +- [ ] Implement the exact design input set and defaults. Validate booleans and numeric + `timeout-minutes` as an integer in 1..120 before checkout. Require the hosted runner/workflow identity contract + with exact stable `action-version` plus resolved action SHA, and one action version across all + EdgeZero calls. +- [ ] Declare required string inputs for repository/ref/id/workspace/package/bin/artifact, optional + string defaults for working directory/suffix/app env, boolean defaults for cache/disclosure, the + required `rust-toolchain`, numeric timeout default, and the required checkout secret exactly as specified. Parse the + received numeric timeout as an integer in 1..120 and reject every undeclared compatibility, + platform, provider, ambient-environment, Cargo, and arbitrary-argument surface. +- [ ] Require `job.workflow_ref` to name canonical exact stable version `V` in published use or the + distinct exact patch version `C` only in the disposable release fixture; `C` has no prerelease + suffix and its GitHub Release must report `prerelease:true`. Require `job.workflow_sha` to be + the ref's resolved full SHA. Use exact `actions/checkout@v7.0.1` with persisted credentials + disabled to check out only `stackpop/edgezero` at ref `job.workflow_sha` into a fixed private + action-source root. Use plan 2's prevalidated object-first materializer for the app at full + `app-ref` in a distinct authority root. Verify EdgeZero repository identity/HEAD/clean tree, + authority and Copy A export contracts, root separation, and that every local composite/helper + path resolves beneath the EdgeZero root rather than app data. +- [ ] Wire plan 2's already gated cache primitive and exact restore/save action versions into this + workflow. Add the first public cache-off, cold, warm, corrupt-restore, save-denied, and + warning-only save hosted runs; preserve evidence for exactly one Cargo compile/build invocation + after metadata preflight and for token absence. +- [ ] Expose only `artifact-name`, trusted `action-version`, resolved `action-revision`, and + CallerExpectedIdentity fields. Never expose the host artifact + path, container ref, platform digest/protocol, checkout token, cache path, or provider state. +- [ ] Test matrix legs with unique artifact names and independent identity comparison. Reject shared + aggregate outputs, duplicate names in one run, empty names, and cross-leg identity reuse. + +## 6. Consumer identity and action validation primitives + +- [ ] Create `.github/actions/compute-app-cli-identity/action.yml` as the public identity action that + wraps plan 2's sole identity-calculation helper. Create + `.github/actions/validate-app-cli-provenance/action.yml` as the no-output public validation + action. Implement the shared private artifact/expected/parse/extract/recheck/smoke helpers that + later provider actions call. Implement only the expected-write, provenance-package, + provenance-validate, and binary-smoke runner profiles here; plan 4 adds provider profiles. +- [ ] Give `compute-app-cli-identity` exactly the required string inputs `action-version`, + `app-repository`, `app-ref`, `app-repo-id`, `workspace-root`, `app-cli-package`, `app-cli-bin`, + and `rust-toolchain`, optional string `working-directory` default `.`, and required sensitive + string input `app-checkout-token`, supplied by the caller from a GitHub secret and masked before + use. Require its runner action repository/ref to equal the supplied exact version. Materialize + one action-private authority with plan 2's gated object-first helper, remove the credential + channel, recompute identity, and clean the authority on every exit. Output exactly + `app-repo-id`, `source-revision`, `app-cli-package`, `app-cli-bin`, and `workspace-id`; never + output a path, descriptor, opaque authority handle, token, or platform field. +- [ ] In each consuming action, create a private action workspace, derive local PlatformIdentity, + download exactly one named artifact with current repository/run id, `merge-multiple:false`, + `skip-decompress:false`, and `digest-mismatch:error`, and reject an invalid name, token, pattern, + artifact id, foreign run/repository selection, or second payload. Require the destination to + contain exactly one regular single-link `artifact.tar` before validation. +- [ ] In each consuming action, invoke baked `write-expected` into a fresh expected directory from + only the consumer-verified caller fields and locally derived platform fields before invoking + `validate`. Reject caller-supplied JSON/platform values, stale or preexisting output, duplicate + output, substitution, and cross-action expected-file reuse; clean the directory on every exit. +- [ ] In the consumer job, invoke `compute-app-cli-identity` once with the exact source inputs and + checkout-token secret, then compare every typed output with the reusable-workflow outputs before + invoking any provider action. Pass those verified caller fields to later actions; do not retain + or pass an authority path because the identity action destroys its authority before returning. + Source-free actions do not materialize source or mount Copy B; each compares artifact metadata + with the supplied verified caller fields and recomputes PlatformIdentity locally, which no + workflow output can substitute. Pass producer `action-version`; every action requires its own + `github.action_repository/ref` to equal `stackpop/edgezero@` before work. +- [ ] Run `validate` with read-only tar/expected/schema, fresh `/work/validated`, no network/credential/ + repository/Cargo/cache/binary execution, and the exact design memory/pid/10-minute limits. + Host-check exactly one + mode-0755 regular single-link output with recorded digest and size. +- [ ] Start a new container for binary smoke with no network or credential and only the validated + binary plus tmpfs. Use direct controlled-loader argv for dynamic binaries and direct execution + for static binaries. Recheck path/device/inode/digest/size/mode/link count before every later use. +- [ ] Ensure `if: always()` cleanup removes the private workspace and operation output parents without + following links. Any cleanup or post-cleanup verification failure is fatal. +- [ ] Keep the extracted path, digest, size, mode, device, and inode in action-private step state only; + expose no host binary path as a reusable-workflow or composite-action output. Make + `validate-app-cli-provenance` validation-only with no outputs and prove its path is absent after + successful cleanup. + +## 7. Verification and merge + +- [ ] Run all protocol crate tests, malformed/golden fixtures, producer/consumer contract tests, + artifact upload/download tests, shellcheck, `scripts/run-actionlint.sh`, zizmor, and repository + Rust/docs checks. +- [ ] Exercise corrupted transport, missing artifact, wrong artifact, replayed CallerExpectedIdentity, + locally changed image pin, parser crash/timeout/SIGKILL, output replacement race, and smoke + timeout. Every failure occurs before provider mutation. +- [ ] Merge through the one-entry queue and record the commit plus active gate revision for plan 4. + Do not designate `P` until launcher/provider and adoption plans pass. + +**Gate:** one deterministic archive and one typed expected identity cross the job boundary; the +consumer job recomputes caller identity without exporting authority state, and every consuming action +independently derives platform identity, checks both groups, and extracts without executing untrusted +bytes. diff --git a/docs/superpowers/specs/2026-08-20-edgezero-deploy-build-caching-design.md b/docs/superpowers/specs/2026-08-20-edgezero-deploy-build-caching-design.md new file mode 100644 index 00000000..adfaa0b8 --- /dev/null +++ b/docs/superpowers/specs/2026-08-20-edgezero-deploy-build-caching-design.md @@ -0,0 +1,2063 @@ +# EdgeZero Deploy Actions - Build Caching Spec + +**Status:** Design (proposed) - v6.26 + +**Related:** `docs/specs/edgezero-deploy-github-action.md`, +`docs/specs/edgezero-deploy-action-implementation-plan.md`, +`docs/specs/edgezero-deploy-adoption-guide.md`, `docs/guide/deploy-github-actions.md` + +## 1. Problem + +`build-app-cli` compiles the application's native CLI without caching. A cross-repository deployer +therefore recompiles the full dependency graph on every run. The solution must also work for real +EdgeZero applications whose crates are public Git dependencies, not only crates.io packages. + +The design must preserve the existing deploy, staged deploy, healthcheck, rollback, and config-push +contracts. It must not expose provider credentials to app CLI compilation or to restored cache data. + +## 2. Scope and trust model + +- The application repository and the app code being compiled are trusted. This includes `build.rs`, + proc macros, manifest commands, and any native tools they invoke. +- Cache writes run only for authorized deployer events and protected refs. The deployer repository + owns the repository-scoped GitHub Actions cache and trusts every workflow allowed to write its + default-branch cache scope. +- The app checkout token and provider token are trusted credentials, but they have disjoint uses. + The checkout token is host-only. The provider token exists only in the minimum provider operation + that requires it. Neither credential enters the cached-compile container or `SCCACHE_DIR`. +- The reusable workflow is the only supported artifact producer. It is build-only: it accepts no + provider inputs and performs no provider mutation. +- Artifact provenance is a consistency and loadability check. It is not producer authentication or + an attestation. A malicious producer can create a self-consistent archive. Attestation remains out + of scope. +- Caching has an accepted correctness risk: sccache can miss undeclared filesystem or environment + inputs, including changed `app-env` values, read by `build.rs` or proc macros. A stale object can + pass digest, ELF, and smoke checks. + `cache: true` explicitly accepts this risk; v1 does not and cannot generally detect it. +- v1 supports GitHub-hosted `linux/amd64` runners only. It fails closed on self-hosted runners and + does not target GitHub Enterprise Server. +- Image publication trusts the digest-pinned official Rust base, Rustup's manifest/checksum chain, + Debian's signed package archive, and checksum-pinned Fastly/sccache release assets. Candidate source + cannot change their coordinates or verification steps. The build is not claimed byte-reproducible + across time: captured leaf digest `D` plus post-build tool/capability verification is the release + identity. +- Host source materialization trusts the exact checksum-pinned official Git LFS release asset and the + bounded GitHub release redirect path defined in Section 5.2. It never trusts a runner-preinstalled + Git LFS binary or a repository-selected download coordinate. + +## 3. Terminology and identity + +### 3.1 Caller and platform identity + +`CallerExpectedIdentity` is the caller-controlled identity that both producer and consumer verify: + +- `app-repo-id`: canonical decimal GitHub repository id, verified against `app-repository` through + the GitHub REST API. +- `source-revision`: the full lowercase 40-hex commit SHA checked out from the app repository. +- `app-cli-package`: Cargo package name. +- `app-cli-bin`: binary name. +- `workspace-id`: the canonical workspace identity described below. + +`PlatformIdentity` is action-controlled: + +- `platform-id`: the `sha256:<64-lowercase-hex>` image manifest digest from the local action + revision's `.github/docker/build-app-cli/image.json`. +- `container-ref`: `@` from that same file. +- `provenance-protocol`: the exact protocol integer from that same file. + +Every EdgeZero action derives `PlatformIdentity` locally. Callers cannot provide or override it, and +the reusable workflow does not expose it as an output. Artifact metadata contains both identity +groups so the consumer action can compare caller values and its locally derived platform values. + +The app checkout token is used host-side by the reusable producer, the public +`compute-app-cli-identity` action, and each source-bearing provider action to verify and materialize a +private repository independently. It is never copied into a container, exported working copy, +artifact, or cache, and no materialized authority path or handle crosses a public action boundary. + +### 3.2 Canonical hashes + +Identity hashes use SHA-256 over a fixed-order, length-framed byte encoding. Each UTF-8 field is +encoded as `:`, where the length is ASCII decimal with no leading zeroes. + +Paths are relative to `git-root`, `/`-separated, have no empty, `.`, or `..` segment, and have no +trailing slash. The root is represented by the single byte `.`. Paths are hashed byte-exactly with +no Unicode normalization; non-UTF-8 paths fail closed. + +`workspace-root` must canonicalize beneath `git-root`; `working-directory` must canonicalize beneath +`workspace-root`; and credential-free `cargo metadata --locked` from `working-directory` must report +that exact workspace root. Its `Cargo.lock` must be a tracked regular file. A caller-provided root is +never trusted without those checks. + +- `workspace-id` is SHA-256 over the concatenation + `::` with no separator or + newline, and is rendered as `sha256:<64-lowercase-hex>`. +- `cache-key-suffix` is valid UTF-8 from 0 through 255 bytes with no Unicode control character. + `suffix-hash` is SHA-256 over `:` with no newline and is rendered as + 64 lowercase hexadecimal characters without a `sha256:` prefix. + +Committed golden vectors cover framing, both digest renderings, the root representation, empty and +255-byte suffixes, rejected oversized/control-character suffixes, and byte-distinct NFC and NFD paths +that must produce different hashes. + +### 3.3 Workflow and action revisions + +`app-ref` must be a full lowercase 40-hex commit SHA. Branches, tags, abbreviated SHAs, and the +legacy `--stage` spelling are unsupported. Staged provider operations use only `--staging`. + +Every non-local external action and reusable workflow reference in this repository and in documented +consumer workflows must use an exact patch-version tag of the form +`v..`, with canonical decimal components and no leading zero except the value +zero itself. Major-only tags, minor-only tags, prereleases, build metadata, branches, commit SHAs, +abbreviated SHAs, and floating names such as `main` or `latest` are not accepted. Docker action refs +remain digest-only. All EdgeZero references in one consumer workflow use one exact action version. +Third-party refs and released EdgeZero repository/documentation surfaces must name stable releases. +Before the first stable EdgeZero release exists, the four prepublication adoption documents may use +literal `` under the dual-state documentation gate defined below; those +examples are intentionally non-runnable until documentation revision `R`. The plan-5 disposable +release fixture alone may use distinct candidate `C`, which has the same exact patch-version grammar +but belongs to a GitHub Release whose `prerelease` field is `true`; `C` never contains a SemVer +prerelease suffix. That exception qualifies candidate commit `H` before final `V` is published. + +In this document, pinning a non-local GitHub action or reusable workflow always means the exact +patch-version tag above, never a commit SHA. Commit SHAs remain mandatory only where they identify +source or execution provenance: `app-ref`, `job.workflow_sha`, `action-revision`, protected-head +commit `Q`, rollout commits `G`/`S`/`B`/`H`/`P`/`R`, and the organization required-workflow descriptor +bound to gate commit `G`. +SHA-256 values remain content identities for images, archives, binaries, fixtures, and hashes; they +are not GitHub `uses:` refs. + +This is an explicit usability tradeoff, not a claim that ordinary Git tags are immutable. A +third-party publisher can move or delete a version tag or create a same-named branch that introduces +symbolic-ref ambiguity; those supply-chain risks are accepted for the separately reviewed, trusted +actions listed by the implementation plans. Initial review must still prove a published stable +release tag exists, no same-named branch exists, and the recorded resolved commit is the reviewed +release commit. EdgeZero's own `V` is +stronger: repository immutable releases must be enabled, `V` must be a published immutable release +whose target is executable commit `P`, and the no-bypass action-version tag ruleset must prohibit +update and deletion. A release version is never silently retargeted; a correction receives a new +patch version. + +Inside the called workflow: + +- `job.workflow_repository` and `job.workflow_file_path` must identify the expected EdgeZero + reusable workflow. +- `job.workflow_ref` must be exactly + `stackpop/edgezero/.github/workflows/build-app-cli.yml@refs/tags/` where the action + version is stable `V` or, only in the release fixture, candidate `C`; +- `job.workflow_sha` must be a full lowercase 40-hex commit SHA and is the resolved executable + revision for that invocation (`H` under `C`, final `P` under `V`). It is not compared textually with the tag-bearing + `job.workflow_ref`. + +These hosted-runner context properties identify the workflow that defines the current job. They are +part of the hosted-only v1 floor. + +`Q` denotes the exact protected-default-branch commit whose post-merge `push` run is being examined. +It is generic: during this rollout it may be a gate candidate `G'`, release source `S`, pin baseline +`B`, an implementation commit, or final action revision `P`. The generic main-push assertion proves +workflow/context identity at `Q`; a release check separately proves that `Q=S` when qualifying the +image source. `H` is reserved for the final action candidate defined in Section 8. + +After those checks, the reusable job uses `actions/checkout@v7.0.1` only to place repository +`stackpop/edgezero` at `ref: job.workflow_sha` in a fixed private action-source directory with +`persist-credentials:false`. The trusted Section 5.2 materializer fetches the application at +`app-ref` into a distinct fixed private authority root without initially creating a worktree, proves +the committed filter/submodule policy, and only then checks out materialized bytes. The EdgeZero +checkout must have exact HEAD `job.workflow_sha`, repository id/name, clean state, and no submodule, +LFS, sparse, or untracked content. Every local composite/helper invocation resolves beneath that +verified EdgeZero root. No `./...` action or helper path may resolve against the application +authority, and neither root may overlap, contain, or symlink into the other. The app token is removed +from Git configuration and the credential channel before the workflow exports tracked files and +supported submodules into non-hardlinked Copy A. The authority remains read-only and is verified +before and after use; Copy A contains no `.git`, checkout credential, ignored file, or untracked file. + +## 4. Cache design + +### 4.1 Cached data and fixed paths + +For the reusable workflow's native `build-app-cli` compile, `CARGO_TARGET_DIR` is fresh on every run +and is never cached; only `SCCACHE_DIR` is archived. The pinned image supplies +`/usr/local/bin/sccache` v0.10.0, and cached compilation sets its absolute path as `RUSTC_WRAPPER`. + +This section defines `build-app-cli.cache`, the reusable workflow's native CLI compilation cache. +It does not replace the parent's distinct `deploy-fastly.cache`: under `build-mode: always`, the +consumer may restore and save that exact-key Cargo target cache only around the credential-free +`app-build` profile below, before any provider token is introduced. `build-mode: never` receives no +target-cache restore or save. + +The host cache path is the fixed `${RUNNER_TEMP}/edgezero-sccache-v1`, emptied before restore. It is +mounted at the constant `/work/sccache`. The fixed host path is required because `actions/cache` +includes the archived path in its cache version. The fixed in-container path and `/work/repo` cwd +also avoid path-only misses in sccache keys. + +The cache contains compiled outputs, indexes, and replayable compiler stdout/stderr. Diagnostics can +contain paths, source excerpts, warnings, and compile-time values. Dependency sources, Cargo registry +or Git checkouts, `.crate` archives, credentials, and `CARGO_HOME/bin` are not intentionally cached. + +The compile environment has no dependency credentials. Both cached and uncached builds therefore +support only anonymously fetchable crates.io and public Git dependencies. `cache:false` selects a +separate `uncached-compile` profile: after the required metadata preflight, Cargo has exactly one +compile/build invocation with a fresh target/Cargo home and +no sccache mount, process, wrapper, socket, or `SCCACHE_*` variable. It is not the cached profile with +cache steps merely skipped. + +### 4.2 Keys, restore, and save + +The cache family is exactly: + +```text +edgezero-sccache-v1-- +``` + +The primary key is `-`, where generation is `job.check_run_id`. The only restore +prefix is `-`. `app-cli-artifact` does not affect cache identity; it is unique only because +GitHub artifact names share a run-level namespace. + +Each successful writer creates a new immutable entry. Concurrent jobs in one family restore the +newest available entry and fork from it. Their results are not merged, so only one lineage may remain +the newest. This lost warmth is accepted. + +There is no cache reservation or fail-closed save protocol. Standard `actions/cache/save` is +best-effort and save failures are warnings. GitHub cache restore, cache absence, and cache save +availability never determine build success. A failure of the compiler-wrapper process itself can +still fail compilation as described below. + +GitHub cache storage and eviction are repository-global. The rolling generations can evict unrelated +workflow caches. Entries not accessed for seven days may be removed. This cost and eviction behavior +is accepted; v1 performs no cache deletion. + +### 4.3 Restore and runtime failure contracts + +For `cache:true`, the sequence is: + +1. Authenticate `app-repository`, verify its actual repository id and the authority checkout, then + validate cache eligibility from that verified id, including the cross-repository disclosure + acknowledgement, before creating the cache root or invoking a cache action. The unverified + `app-repo-id` input can never obtain the same-repository exemption. +2. Empty the stable host cache directory. +3. Restore the newest matching cache. +4. Audit restored data. On restore or audit failure, clear the directory and continue cold. +5. Start sccache, zero its statistics, and compile once. +6. Capture `sccache --show-stats --stats-format=json` and stop the server. +7. Audit the stopped directory again. +8. Save only when the compile succeeded, stop succeeded, the final audit passed, the captured + `cache_write_errors` count is zero, and the save-authorization predicate below is true. + +`cache: true` authorizes lookup and restore, not publication. Saving is permitted only when all of the +following action-derived conditions hold: the event is exactly `push` or `workflow_dispatch`, +`github.ref_protected` is the boolean `true`, `github.event.repository.fork` is the boolean `false`, +the event repository id equals `github.repository_id`, the workflow identity checks in Section 3.3 +passed, and the disclosure requirement in Section 4.4 passed. Pull-request events +(including `pull_request_target`), `merge_group`, forks, +unprotected refs, missing context, malformed values, and caller-supplied substitutes are restore-only +only after lookup eligibility passed. A cross-repository request without +`disclosure-acknowledged:true` is an input error before restore, not a restore-only row. The cache plan +commits separate lookup-eligibility and save-authorization truth tables; no input can override either +computed decision. + +The captured sccache statistics wire is the single JSON value emitted by exact v0.10.0 command +`sccache --show-stats --stats-format=json`. The parser rejects duplicate keys, trailing data, +non-UTF-8, non-object roots, unknown/missing top-level or `stats` fields, wrong types, negative or +non-integer counters, and any integer outside `u64`. Its closed v0.10.0 schema is independently +derived from the pinned `ServerInfo`/`ServerStats` source. The top level has exactly `stats`, +`cache_location`, `cache_size`, `max_cache_size`, `use_preprocessor_cache_mode`, and `version`. +`stats` has exactly these 22 keys: + +```text +compile_requests +requests_unsupported_compiler +requests_not_compile +requests_not_cacheable +requests_executed +cache_errors +cache_hits +cache_misses +cache_timeouts +cache_read_errors +non_cacheable_compilations +forced_recaches +cache_write_errors +cache_writes +cache_write_duration +cache_read_hit_duration +compilations +compiler_write_duration +compile_fails +not_cached +dist_compiles +dist_errors +``` + +`cache_errors`, `cache_hits`, and `cache_misses` are objects with exactly `counts` and `adv_counts`; +each is a map from a 1..255-byte UTF-8 key without Unicode controls to a `u64`. `not_cached` and +`dist_compiles` are maps with the same key and value bounds. The three duration fields are objects +with exactly canonical nonnegative integer `secs` and `nanos`, where both fit `u64` and +`nanos<1_000_000_000`. Every other `stats` field is a `u64`. + +For this managed local-disk profile, `cache_location` is exactly +`Local disk: "/work/sccache"`, `cache_size` is a `u64` no greater than 2,147,483,648, +`max_cache_size` is integer `2147483648`, `use_preprocessor_cache_mode` is false, and `version` is +exactly `0.10.0`; null optional-size values are rejected after a successful compile. A committed +schema and golden output enumerate the same fields and bounds. The implementation reads +`stats.cache_write_errors` only after the entire closed document validates. + +Storage lookup and decompression failures that sccache v0.10 treats as misses remain misses. +`SCCACHE_IGNORE_SERVER_IO_ERROR=1` is set because it covers selected client/server response failures; +it is not described as covering startup, connection, extraction, or every backend error. Any other +sccache error follows pinned v0.10 behavior. An ordinary compiler failure is surfaced once and is +never retried by the cache layer. + +If `sccache --stop-server` fails, the action skips save with a warning. If cache write errors are +non-zero, the build may still succeed but save is skipped with a warning. Restore, save, and cache +absence never cause an action-level retry. After metadata preflight, the action invokes exactly one +Cargo compile/build command. Pinned sccache v0.10 may itself fall back to a local compiler invocation +after `CompileStarted` when the server response is lost; this internal fallback is accepted +pinned-client behavior and is not described or tested as single compiler-process execution. + +### 4.4 Cache audit and disclosure + +`SCCACHE_CACHE_SIZE=2G` is the managed sccache capacity. The independent hard tree bound is +2,147,483,648 bytes: the audit sums `st_size` for every regular file with checked integer arithmetic +and rejects a greater total. It also rejects sparse files (`st_blocks * 512 < st_size`), more than +100,000 descendants, a path longer than 4,096 bytes, or a single path component longer than 255 +bytes. Directory `st_size` values do not contribute. These are filesystem-tree limits, not a claim +about `actions/cache`'s host-selected tar, zstd, framing, or wire size. The selected cache action and +host archiver are outside the trusted data parser; an upload-size or archiver failure remains the +warning-only save failure defined above. The cache implementation plan pins +`actions/cache/restore@v6.1.0` and `actions/cache/save@v6.1.0` and tests these format-independent tree +bounds. + +Before use after restore and before save, the audit requires: + +- the canonical audited root is exactly `SCCACHE_DIR`; it is the same recorded device/inode as the + action-created mode-0700 real directory, is owned by uid/gid 1001, and is not itself a mount; +- every entry is a regular file or directory beneath that root; +- no symlink, socket, FIFO, device, mount escape, or special file exists, and every regular file has + `nlink == 1` (directory link counts are not constrained); +- ownership is the expected container uid/gid; +- layout and record names match the pinned v0.10 format fixtures; +- the logical-byte, non-sparse-file, path-length, and entry-count bounds above all pass. + +The application is trusted, but cached compilation shares a writable uid and `SCCACHE_DIR` with app +code. App code can therefore place arbitrary bytes in that directory. The audit constrains shape and +size, not authorship or semantic content. The cache is not content-authenticated and the disclosure +acknowledgement covers the entire archived directory, compiler diagnostics, and app-written bytes +that satisfy the audit. + +Every cross-repository cached build requires `disclosure-acknowledged: true` before either cache +family is restored or saved; equality between the authenticated application repository id and event +repository id is the only exemption. The +parent `deploy-fastly.cache` target cache uses the same lookup-eligibility rule and the same +action-derived protected-event save predicate as the sccache family. A denied parent save remains +restore-only only when lookup was eligible. Its existing key/content/audit contract remains owned by +the parent deploy spec. + +At invocation start, the fixed host cache root must be absent beneath a real, non-symlinked +`${RUNNER_TEMP}`; the action creates it, records it, and never adopts a preexisting path. After the +single save attempt or any earlier terminal path, descriptor-relative no-follow cleanup removes that +recorded tree and verifies absence. Cleanup failure fails the action even when restore/save failure +itself was warning-only, because a dirty fixed root could contaminate another invocation in the job. + +## 5. Container execution + +### 5.1 Image and runner + +The EdgeZero image is public and anonymously pullable by digest and is a leaf `linux/amd64` image +manifest rather than an OCI index. It is built from a digest-pinned base and contains: + +- the exact Rust toolchain from `.tool-versions` and an installed `wasm32-wasip1` target; +- exact pinned Fastly CLI and sccache versions with checksum-verified downloads; +- `git`, `jq`, `tar`, `curl`, CA certificates, and a C toolchain; +- the project-owned provenance validator and its protocol/schema assets. + +Runtime containers use a read-only root filesystem, uid/gid 1001, dropped capabilities, +`no-new-privileges`, no GitHub file-command channels, explicit mounts, and operation-specific network, +memory, pid, and timeout limits. + +### 5.2 Working-copy topology + +There are two independent exported-copy roles because GitHub jobs do not share filesystems. The +producer workflow and every consumer-side public action that needs repository source independently +create a private **authority checkout** at the exact app SHA through the object-first trusted +materializer defined below. The public identity action also creates its own short-lived authority for +identity calculation. The helper may use the app token only through its bounded host credential +channel; no application-controlled command, filter, hook, or worktree operation runs before policy +validation. No authority directory, descriptor, path, or opaque handle is accepted from or returned +to another public action. After credential removal and source validation, a trusted exporter creates +the applicable action-local copy from that authority checkout: + +- **Copy A, producer build job:** a private faithful copy used only for cached native CLI + compilation. It is mounted read-only; Cargo target, Cargo home, sccache, home, and temporary output + live in separate action-owned paths. The reusable workflow uploads its CLI artifact; Copy A is then + discarded. +- **Copy B, source-bearing consumer action:** a fresh private faithful copy exported from that + action's independently materialized authority. Its repository view is mounted read-only except for + the action-created nested output-root mounts, so generated files can flow from app build to + `fastly compute deploy` without granting general source write access. `deploy-fastly` creates one + Copy B at invocation start and may reuse it only between its internal app-build and deploy + operations; its active-version operation does not mount the copy. No authority, Copy B, or generated + root is shared across public action invocations. Source-free actions create no authority or Copy B. + Copy A never crosses into the consumer job. + +`config-push-fastly` is the sole source-bearing Copy-B exception: it executes no application code and +creates no Copy B. It independently materializes its own credential-free frozen authority and mounts +that authority read-only so its selected tracked manifest and file-backed config retain +repository-relative semantics. Before token creation and again immediately before container start, +the host records and verifies authority HEAD, index/worktree state, full tracked/submodule inventory, +and the selected manifest/config device, inode, digest, size, mode, and link count. It repeats those +checks after the command and before cleanup. Inline config is action-owned and receives the same +identity checks. Any authority or selected-file change, replacement, or race fails; fixtures exercise +replacement between every check and launch boundary. + +Each copy preserves the entire repository layout, enclosing workspaces, parent Cargo config, sibling +path dependencies, file modes, symlink targets, and initialized submodule state. It includes tracked +files and initialized submodules only; ignored and untracked detritus, `.git` directories/files, LFS +object stores, checkout credentials, and action metadata are absent. Every regular file has a newly +created inode, so hardlinks to the authority are forbidden. The authority is made read-only +after export and remains the freeze authority. + +Protocol 1 permits only paths with no Git filter or `filter=lfs`; any custom clean/smudge/process +filter, required filter other than LFS, or submodule using one fails. Before authority checkout, a +trusted host helper installs Git LFS 3.7.1 from exact asset +`git-lfs-linux-amd64-v3.7.1.tar.gz`, whose SHA-256 is +`1c0b6ee5200ca708c5cebebb18fdeb0e1c98f1af5c1a9cba205a4c0ab5a5ec08`. The closed canonical +gate-owned `.github/actions/deploy-core/host-tools.json` contains the following single data line with +no terminating newline; the Markdown fence line break is not file content: + +```text +{"git-lfs":{"asset":"git-lfs-linux-amd64-v3.7.1.tar.gz","sha256":"1c0b6ee5200ca708c5cebebb18fdeb0e1c98f1af5c1a9cba205a4c0ab5a5ec08","size":5524590,"version":"3.7.1"},"schema-version":1} +``` + +The download URL is constructed in trusted code as +`https://github.com/git-lfs/git-lfs/releases/download/v3.7.1/git-lfs-linux-amd64-v3.7.1.tar.gz`, not +read from data. Every hop is HTTPS, the initial host is exactly `github.com`, redirects are bounded +to three and may terminate only at `release-assets.githubusercontent.com`, and no credential is sent +on the public download. The final response must be HTTP 200 with identity content encoding and exactly +one decimal `Content-Length: 5524590`; the streaming receiver rejects an absent, duplicate, malformed, +or different length, more than 5,524,590 received bytes, early EOF, or trailing data. An unexpected +host, downgrade, redirect count, checksum, archive layout, or installed `git-lfs version` also fails +before authority materialization. + +The trusted materializer creates fresh Git object repositories with system/global configuration, +configuration includes, credential helpers, template hooks, and hook execution disabled. While no +application-controlled process is running, it uses the app token only through a noninteractive, +non-logging, host-only credential channel scoped to each canonical GitHub repository origin and +fetches exact app/submodule commits into object databases without creating a worktree or initializing +a submodule. Before any checkout, it recursively inspects the committed trees, `.gitattributes`, +`.lfsconfig`, `.gitmodules`, gitlinks, and submodule target trees using trusted Git plumbing. It +rejects `.lfsconfig`; any custom clean/smudge/process filter; any required filter other than LFS; any +repository/local/global/system `lfs.*` URL or transfer override; non-GitHub origin/submodule URLs; and +inconsistent, missing, or unlisted submodule commits. This validation may fetch a verified canonical +submodule origin but never creates its worktree or runs a filter, hook, or repository command. + +Only after the complete recursive object graph passes does the helper create the authority worktrees +with every filter disabled, including automatic LFS smudging. It invokes the absolute verified Git +LFS 3.7.1 binary directly to fetch and materialize the exact permitted LFS objects, checks out exact +submodule commits, runs `git lfs fsck --objects` in each repository, and rejects any worktree file +that remains a valid LFS pointer. It then removes the credential channel and every credential before +export. The exporter copies materialized worktree bytes, not pointer blobs. Tests prove forbidden +filter commands, hooks, and non-GitHub origins are never contacted or executed and cover absent/ +corrupt objects, pointer residue, nested submodules, credential cleanup, and configuration races. + +### 5.3 Mount profiles + +`run-app-cli-in-container` has a maximum allowlist and a closed profile for each operation. It never +mounts all of `RUNNER_TEMP`. + +| In-container path | Mode | Allowed operations | Source | +| --------------------------- | --------------- | ------------------------------------------------------------ | ----------------------------------------------- | +| `/work/repo` | read-only | cached-compile, uncached-compile, app-build, provider-deploy | Copy A or Copy B | +| `/work/repo` | read-only | config-push | credential-free frozen authority; no Copy B | +| `/work/repo/` | writable | app-build, provider-deploy | action-created declared/implicit directory | +| `/work/target` | writable | cached-compile, uncached-compile, app-build, provider-deploy | fresh or parent target cache as specified below | +| `/work/cargo-home` | writable, fresh | cached-compile, uncached-compile, app-build, provider-deploy | operation-specific directory | +| `/work/sccache` | writable | cached-compile only | stable host cache directory | +| `/work/input/app-cli` | read-only | provenance-package only | exact binary produced by cached-compile | +| `/work/input/artifact.tar` | read-only | provenance-validate only | downloaded artifact | +| `/work/expected` | writable, fresh | expected-write | empty host expected-identity output directory | +| `/work/release` | writable, fresh | release-request-write | empty host release-request output directory | +| `/work/input/expected.json` | read-only | provenance-package, provenance-validate | validator-generated expected identity | +| `/work/packaged` | writable, fresh | provenance-package only | empty host archive-output directory | +| `/work/validated` | writable, fresh | provenance-validate only | empty host output directory | +| `/work/bin/app-cli` | read-only | binary-smoke and provider operations | validated binary | +| `/work/config/inline.toml` | read-only | config-push only | optional action-owned inline config file | +| `/work/home`, `/work/tmp` | writable tmpfs | all operations | operation-local tmpfs | + +Profiles: + +- `cached-compile`: Copy A, fresh target and Cargo home, sccache, tmpfs; no token. +- `uncached-compile`: Copy A, fresh target and Cargo home, and tmpfs; no token, sccache mount, + wrapper, socket, or sccache process. +- `app-build`: validated CLI, Copy B, fresh Cargo home, and the parent + `deploy-fastly.cache` target directory when enabled. It runs ` build` for + `build-mode: always`, has no provider token and no sccache mount, and saves the parent target cache + before any provider operation. +- `provider-deploy`: Copy B, fresh target/Cargo home, validated CLI, tmpfs, provider token; + never sccache and never a writable cache. Fastly deploy may compile application source with the + token for both `build-mode` values. A prior `app-build` is a credential-free validation/prebuild and + does not claim to suppress this recompile; its parent target cache was already saved before the + token appeared and is never saved again afterward. +- `expected-write`: trusted baked validator, fresh writable `/work/expected`, and tmpfs; no + repository, app binary, target, Cargo, cache, network, or token. It converts typed identity scalars + into the only supported `expected.json` encoding. +- `release-request-write`: trusted baked validator, fresh writable `/work/release`, and tmpfs; no + repository, app binary, target, Cargo, cache, network, or token. It converts typed release scalars + into the only supported `release-request.json` encoding. +- `provenance-package`: trusted baked validator, the exact compiled binary read-only at + `/work/input/app-cli`, read-only expected-identity JSON, fresh writable `/work/packaged`, and tmpfs; + no repository, target, Cargo, package, cache, app-binary execution, network, or token. +- `provenance-validate`: trusted baked validator, read-only tar, fresh writable output directory, + read-only expected-identity JSON, and tmpfs; no repository, app-binary execution, token, Cargo, + target, package, or cache mount. +- `binary-smoke`: validated binary only plus tmpfs; no network, token, repository, Cargo, target, + package, cache, or validator output write access. +- `self-test`: no host bind mount. It reads only the image-owned validator, schema, and exact fixture + directory and writes only to `/work/home` and `/work/tmp` tmpfs; no network, repository, app + binary, token, Cargo, target, package, cache, or output bind mount. +- `provider-read`: validated binary and tmpfs. `active-version` receives the provider token. + Production healthcheck receives no token; staging healthcheck receives the token needed for the + staged endpoint. +- `provider-rollback`: validated binary and tmpfs plus the provider token; no repository, Cargo, + target, package, or cache mount. +- `config-push`: validated binary, frozen repository read-only, tmpfs, provider token, and the + enumerated app-config overlay. A selected manifest and file-backed app config must canonicalize + beneath the frozen repository; inline config is one fresh host file mounted at the exact path + above. It receives no writable repository, package, Cargo, target, or sccache mount. + +The parent deploy spec remains normative for production/staging lifecycle semantics, rollback target +capture, mutation signaling, healthcheck ordering, and recovery. This addendum expressly supersedes +the parent's app-CLI metadata shape, caller override, archive member naming/ordering, system-tar +packaging/extraction, and artifact-validation rules, in addition to changing isolation and mounting. +Every staged CLI invocation uses `--staging`, never `--stage`. + +Network and resource limits are closed by operation. An enabled network is Docker's ordinary isolated +bridge, never host or another container's namespace. Memory and memory-plus-swap limits are equal, so +no operation receives additional swap: + +| Operations | Network | Memory | Pids | Hard wall timeout | +| -------------------------------------------------- | ------- | ------- | ---- | ----------------- | +| cached-compile, uncached-compile | bridge | 6 GiB | 512 | `timeout-minutes` | +| app-build, provider-deploy | bridge | 6 GiB | 512 | 60 minutes | +| expected-write, release-request-write | none | 256 MiB | 32 | 60 seconds | +| provenance-package, provenance-validate, self-test | none | 2 GiB | 64 | 10 minutes | +| binary-smoke | none | 512 MiB | 64 | 60 seconds | +| active-version, provider-rollback, config-push | bridge | 1 GiB | 128 | 10 minutes | +| production/staging healthcheck | bridge | 1 GiB | 128 | computed below | + +`timeout-minutes` is a canonical decimal integer from 1 through 120 and defaults to 30. Healthcheck +`retry` is 1..20, `retry-delay` is 0..300 seconds, and per-attempt `timeout` is 1..300 seconds. Checked +arithmetic computes `retry * timeout + (retry - 1) * retry-delay + 30` seconds; the value must be at +most 3,600 and becomes the container hard wall timeout. The supervisor sends TERM on expiry or runner +cancellation, waits at most 10 seconds, then sends KILL and enters the parent reconciliation path. +Timeout, OOM, resource-limit, and forced-kill outcomes are failures; they never relax cleanup, +mutation, cache-save, or reconciliation rules. + +### 5.4 Constructed environments + +Every operation starts with `env -i` and a closed allowlist. `PATH` is +`/usr/local/bin:/usr/local/cargo/bin:/usr/bin:/bin`. + +- cached compile: `PATH`, `RUSTUP_HOME=/usr/local/rustup`, `RUSTUP_TOOLCHAIN`, fresh `CARGO_HOME`, + fresh `CARGO_TARGET_DIR`, `RUSTC_WRAPPER=/usr/local/bin/sccache`, `SCCACHE_DIR`, + `SCCACHE_CACHE_SIZE=2G`, `SCCACHE_IGNORE_SERVER_IO_ERROR=1`, `CARGO_INCREMENTAL=0`, empty + `CARGO_ENCODED_RUSTFLAGS`, `HOME`, `TMPDIR`, and validated `app-env`. +- uncached compile: the same Rustup/Cargo, `HOME`, `TMPDIR`, and validated `app-env` values, but no + `RUSTC_WRAPPER`, `SCCACHE_*`, or sccache socket variable. +- app build: the Rustup/Cargo variables above except every sccache variable and wrapper, the + operation's action-owned target paths, `HOME`, `TMPDIR`, the validated `app-env` map, and + validated `EDGEZERO_MANIFEST` when selected; no provider token. +- provider deploy: the Rustup/Cargo variables above except every sccache variable and wrapper, plus + `FASTLY_API_TOKEN`, the operation's enumerated `EDGEZERO_*` variables, validated `app-env`, and + validated `EDGEZERO_MANIFEST` when the caller selected a manifest. +- expected writing, release-request writing, provenance packaging, provenance validation, binary smoke, and self-test: + `PATH`, `HOME`, `TMPDIR` only. `self-test` argv is exactly + `edgezero-provenance-validator self-test --fixtures /usr/local/share/edgezero/provenance-fixtures`. +- provider operations: `PATH`, `HOME`, `TMPDIR`, only the token required by that operation, and only + explicitly named `EDGEZERO_*` variables plus validated `app-env`. Config push also receives its + selected validated overlay names unless `no-env` was selected. + +Non-credential application configuration is explicit rather than ambient. `app-env` is a JSON object +input (default `{}`). Its raw representation is valid UTF-8 and at most 65,536 bytes before parsing, +and duplicate keys are rejected before object construction. It has at most 64 entries and at most +32,768 UTF-8 bytes across names and values. Every value is a JSON string; numbers, booleans, null, +arrays, and objects fail. A name is 1..127 ASCII bytes matching +`[A-Za-z_][A-Za-z0-9_]*`; a value is at most 8,192 UTF-8 bytes and contains no NUL, C0 control, or DEL. +Reserved-name comparison is ASCII-case-insensitive. The exact deny set is: + +- exact names `PATH`, `HOME`, `TMPDIR`, `TMP`, `TEMP`, `PWD`, `OLDPWD`, `SHELL`, `BASH_ENV`, `ENV`, + `CDPATH`, `IFS`, `GLOBIGNORE`, `SHELLOPTS`, `BASHOPTS`, `CC`, `CXX`, `AR`, `AS`, `LD`, `NM`, + `OBJCOPY`, `OBJDUMP`, `RANLIB`, `STRIP`, `CFLAGS`, `CXXFLAGS`, `CPPFLAGS`, `LDFLAGS`, `RUSTFLAGS`, + `RUSTDOCFLAGS`, `RUSTC`, `RUSTDOC`, `ARFLAGS`, `CXXSTDLIB`, `CXXSTDLIB_STATIC`, + `CRATE_CC_NO_DEFAULTS`, `CC_KNOWN_WRAPPER_CUSTOM`, `CC_SHELL_ESCAPED_FLAGS`, + `CC_ENABLE_DEBUG_OUTPUT`, `NUM_JOBS`, `MAKEFLAGS`, `MFLAGS`, `LIBRARY_PATH`, `CPATH`, + `C_INCLUDE_PATH`, `CPLUS_INCLUDE_PATH`, `GCC_EXEC_PREFIX`, `COMPILER_PATH`, `PKG_CONFIG`, + `PKG_CONFIG_PATH`, `PKG_CONFIG_LIBDIR`, and `PKG_CONFIG_SYSROOT_DIR`; +- prefixes `GITHUB_`, `RUNNER_`, `ACTIONS_`, `EDGEZERO_`, `FASTLY_`, `CARGO_`, `RUST_`, `RUSTC_`, + `RUSTDOC_`, `RUSTUP_`, `SCCACHE_`, `CRATE_CC_`, `CXXSTDLIB_`, `LD_`, `DYLD_`, and `BASH_FUNC_`; and +- target- or build-kind-qualified native-tool names matching either + `(CC|CXX|AR|AS|LD|NM|OBJCOPY|OBJDUMP|RANLIB|STRIP|ARFLAGS|CFLAGS|CXXFLAGS|CPPFLAGS|LDFLAGS)_.+` or + `.+_(CC|CXX|AR|AS|LD|NM|OBJCOPY|OBJDUMP|RANLIB|STRIP|ARFLAGS|CFLAGS|CXXFLAGS|CPPFLAGS|LDFLAGS)`. + This includes cc-rs forms such as `CC_`, `_CC`, `HOST_CC`, `TARGET_CC`, and their + target/build-kind flag equivalents. + +The caller is responsible for passing no credential under an otherwise allowed application name; +cross-repository cache disclosure covers compile-time values. Only the exact validated names are +added to operations that execute app code or the app CLI. Config-push's separately derived typed- +config overlay remains subject to its own prefix and `no-env` rules. This explicit input replaces the +parent's ambient workflow-`env` behavior and is a documented adoption migration. + +Caller `PATH`, `RUSTC`, `RUSTDOC`, compiler wrappers, Rust flags, native-tool variables, ambient +application variables, and unlisted `EDGEZERO_*` variables are absent rather than scrubbed after +inheritance. Cached and uncached profiles both prove those variables cannot be reintroduced through +`app-env`. + +No environment value is placed literally in Docker CLI argv. The action writes the already validated +operation environment to one fresh mode-0600, single-link, action-owned env file outside every +checkout/cache/output root, invokes `docker create --env-file ` for a uniquely named container, +and deletes and verifies absence of the env file before `docker start --attach`. File serialization +rejects newline and NUL in every name/value; provider inputs whose token format permits either are +therefore invalid. For token-bearing profiles, the final source/output/binary checks occur immediately +before this env file is created. Create/start/attach failure still triggers named-container removal, +env-file removal, private-workspace cleanup, and any required mutation reconciliation. The token may +exist in the isolated runner's Docker container metadata while that container exists; no other +container receives Docker-socket access, and removal is mandatory before the action completes. + +Protocol 1's Cargo-config allowlist is empty. Before compilation, the action rejects `.cargo/config`, +`.cargo/config.toml`, and legacy `.cargo/credentials*` at the cwd, every ancestor through `git-root`, +and every enclosing workspace directory copied into `/work/repo`; the fresh action-owned +`CARGO_HOME` must contain none of those files. Cargo environment controls are absent under the closed +environment above. `Cargo.lock` must be a tracked regular file. Path dependencies may resolve +anywhere beneath `git-root` and must not escape it. The parent toolchain resolver still runs, but v1 +requires its result (including an explicit `rust-toolchain` input) to equal the exact toolchain baked +in `image.json`'s image; a mismatch fails before container launch. Alternate toolchains or Cargo +configuration require a separate protocol revision and remain out of scope. + +## 6. Source freezing and provenance + +### 6.1 Freeze and pre-token verification + +The authority checkout must be full, recursive, non-sparse, satisfy the no-filter-or-pinned-LFS +contract in Section 5.2, and start clean: `HEAD` equals `source-revision`, no tracked/index or +untracked modification exists, and every initialized submodule is clean at its recorded gitlink. +Credentials are removed before export, and Copy A or Copy B must pass the trusted export-inventory +comparison before any application command. Before and after app-controlled commands, the authority's +repository id, HEAD, clean state, filter policy, LFS object/materialization state, and recursive +submodule state must remain unchanged. + +Immediately before any token-bearing operation that mounts Copy B, executes repository source, or +consumes its derived package, compare the complete Copy B inventory with the frozen source. This runs +whether or not `app-build` ran and, when it did, runs after that credential-free build: + +- every tracked file's bytes and executable mode, every symlink target, and every gitlink commit must + match, including deletion detection; +- no new path may exist except at or beneath a validated declared output root; +- each output root must canonicalize beneath the repository, must not be `.`, `.git`, or a symlink, + and must not equal or be an ancestor of any tracked path; +- output roots must not overlap each other, and each parent segment must remain confined beneath the + repository; +- every output-root entry must be a directory or regular file owned by the container uid/gid; regular + files must have `nlink == 1` and must not be sparse (`st_blocks * 512 < st_size` fails), and + symlinks, sockets, FIFOs, devices, mounts, and hardlinks fail; +- across all output roots, checked arithmetic permits at most 2,147,483,648 summed regular-file + `st_size` bytes and 100,000 descendants. Existing component and joined-path bounds remain in force. + +The declaration authority is the protected caller's `generated-output-paths` JSON-array input to +`deploy-fastly` (default `[]`). The raw input is valid UTF-8 and at most 65,536 bytes before parsing; +the array has at most 32 unique entries. Each entry is 1..1,024 UTF-8 +bytes, uses `/` separators, is already in repository-relative lexical normal form, and has no empty, +`.`, `..`, NUL, control-character, or backslash component; every component is at most 255 UTF-8 bytes. +The joined absolute host path is at most 4,096 bytes. Each nearest existing parent is resolved before +app code runs, must be a real directory reached without a symlink, and must canonicalize beneath the +repository. Declared roots must be pairwise nonoverlapping, contain no tracked path, and be absent +initially. The action creates each as an empty mode-0700 directory, records its device/inode as action- +owned, and grants Copy B its only repository write access through those directory mounts. It audits +the closed file-type, ownership, hardlink, and confinement rules above after every app-controlled +command and immediately before every token-bearing operation. + +Action-owned target and Cargo-home paths outside the repository are implicit and cannot be +overridden. The current Fastly CLI changes cwd to the resolved project directory containing the +selected `fastly.toml` and writes both `bin/main.wasm` and `pkg/.tar.gz` there. Therefore +`/bin` and `/pkg`, not workspace-root paths, are two +additional implicit output roots. The manifest is resolved before any app process; both roots obey +the same absent-before-create, mount, audit, and ownership contract and callers do not repeat them. +The action keeps all output roots only until the last operation that consumes them, then a trusted +descriptor-relative cleanup routine removes each recorded action-owned tree without following links +and verifies the original checkout and Copy B again. Cleanup runs on success and ordinary failure; +cleanup or post-cleanup verification failure fails the action. A preexisting root fails before any +provider mutation, so cleanup never adopts or deletes caller-owned content. Any application whose +credential-free build writes elsewhere must declare every additional root; the action never infers +permission from an observed mutation. + +This permits declared generated output while preventing a credential-free build step from rewriting +source that a later token-bearing compile would execute. Source-free lifecycle actions such as +healthcheck and rollback do not receive Copy B and do not run this inventory comparison; they verify +artifact/caller/platform identity instead. Config-push verifies repository id, HEAD, cleanliness, and +confined selected files on its read-only checkout. The consumer repeats the checks applicable to each +mounted source profile before and after provider commands. + +### 6.2 Protocol-1 JSON contract + +Protocol 1 has two closed JSON documents. Both are UTF-8 RFC 8785 JCS bytes with no BOM, surrounding +whitespace, or trailing newline. Duplicate object keys are rejected while parsing, before an object +or generic JSON value is constructed. Unknown and missing fields, wrong JSON types, noncanonical JCS +bytes, and values outside the bounds below fail closed. The committed JSON Schema 2020-12 file checks +the local shape; procedural validation enforces canonical bytes, duplicate rejection, cross-field +relationships, and exact expected-versus-observed identity. + +`expected.json` contains exactly the identity the protected caller and local action computed: + +```json +{ + "caller": { + "app-cli-bin": "edgezero", + "app-cli-package": "edgezero-cli", + "app-repo-id": "123456", + "source-revision": "<40-lowercase-hex>", + "workspace-id": "sha256:<64-lowercase-hex>" + }, + "platform": { + "container-ref": "ghcr.io/stackpop/edgezero-build-app-cli@sha256:<64-lowercase-hex>", + "platform-id": "sha256:<64-lowercase-hex>", + "provenance-protocol": 1 + }, + "schema-version": 1 +} +``` + +`app-cli-meta.json` contains exactly the same identity plus observed binary data: + +```json +{ + "abi": { + "interpreter": "/lib64/ld-linux-x86-64.so.2", + "machine": "x86_64", + "needed": ["libc.so.6"] + }, + "app-cli-version": "0.1.0", + "binary-sha256": "sha256:<64-lowercase-hex>", + "binary-size": 123, + "caller": { + "app-cli-bin": "edgezero", + "app-cli-package": "edgezero-cli", + "app-repo-id": "123456", + "source-revision": "<40-lowercase-hex>", + "workspace-id": "sha256:<64-lowercase-hex>" + }, + "platform": { + "container-ref": "ghcr.io/stackpop/edgezero-build-app-cli@sha256:<64-lowercase-hex>", + "platform-id": "sha256:<64-lowercase-hex>", + "provenance-protocol": 1 + }, + "schema-version": 1 +} +``` + +The examples are line-wrapped for review; the wire fixtures contain compact JCS bytes. Field rules +are exact: + +- `schema-version` and `provenance-protocol` are JSON integers equal to `1`. Protocol 1 does not + evolve them independently; an incompatible JSON, archive, or ELF rule requires both to change. +- `app-repo-id` is a string containing the canonical nonzero decimal representation of a `u64`: no + sign and no leading zero. +- `source-revision` is a nonzero full lowercase 40-hex commit SHA. +- `app-cli-package`, `app-cli-bin`, and `app-cli-version` are 1 through 255 UTF-8 bytes, contain no + Unicode control character, and contain neither `/` nor `\\`. The package and binary values must + equal the validated Cargo package and target names; `app-cli-version` must equal that package's + version from the same credential-free locked Cargo metadata result. Before the host mounts the + compiled file at the fixed `/work/input/app-cli` path, it requires the source basename to equal + `app-cli-bin`. +- `workspace-id`, `platform-id`, and `binary-sha256` use + `sha256:<64-lowercase-hex>` and reject the all-zero digest. +- `container-ref` is exactly + `ghcr.io/stackpop/edgezero-build-app-cli@`; no tag or alternate repository is valid. +- `binary-size` is a JSON integer from 1 through 536,870,912 and equals the exact + `app-cli-bin` member payload length. `binary-sha256` equals SHA-256 over those exact payload bytes, + with no header or padding bytes included. +- `abi.machine` is exactly `x86_64`; `abi.interpreter` is either the exact string defined in Section + 6.4 or JSON null; and `abi.needed` preserves every direct `DT_NEEDED` occurrence, including + duplicates, sorted by UTF-8 bytes. Each entry is 1 through 255 bytes and is a basename containing + no slash, backslash, dollar sign, NUL, or control character. + +`expected.json` is at most 16 KiB and `app-cli-meta.json` is at most 64 KiB. The validator compares +the complete `caller`, `platform`, and `schema-version` values for equality. The artifact is a +consistency record, not producer authentication. + +### 6.3 Protocol-1 ustar contract + +The protocol crate is the only archive encoder and decoder. The producer must not construct metadata +with `jq` or archives with system `tar` or a general-purpose tar library. It emits deterministic POSIX +ustar with exactly two regular members in order: literal `app-cli-meta.json`, then literal +`app-cli-bin`. The caller's binary name remains in metadata and is not used as an archive path. + +Every 512-byte header is byte-exact: + +- `name` is the member name followed by NUL bytes to width 100; `prefix`, `linkname`, `uname`, and + `gname` are all NUL bytes; +- `mode` is `0000644\0` for metadata and `0000755\0` for the binary; +- `uid`, `gid`, `devmajor`, and `devminor` are `0000000\0`; `mtime` is `00000000000\0`; +- `size` is eleven lowercase octal digits with leading zeroes followed by NUL; +- `chksum` is six lowercase octal digits with leading zeroes, NUL, and space; its unsigned-byte sum + is computed with all eight checksum bytes replaced by spaces; +- `typeflag` is ASCII `0`, `magic` is `ustar\0`, `version` is `00`, and bytes 500 through 511 are NUL. + +Base-256 numbers, alternate octal padding, embedded-NUL garbage, PAX/GNU extensions, sparse records, +links, special files, extra or duplicate members, renamed paths, and traversal are rejected. Payload +padding through the next 512-byte boundary is all zero. Exactly two all-zero end blocks follow the +binary payload, followed immediately by EOF; extra zero blocks or any trailing byte fail. The sum of +the two logical payload sizes is at most 512 MiB, and the metadata payload is nonempty and at most 64 +KiB. Overflow in any size, offset, padding, or checksum calculation fails before reading or writing. + +### 6.4 Protocol-1 ELF and loader profile + +Protocol 1 intentionally models one conservative immutable runtime rather than general Linux loader +behavior. The primary app binary and every parsed dependency must be ELF64, little-endian, and +`EM_X86_64`; metadata records the machine as `x86_64`. The primary is `ET_EXEC` or `ET_DYN` and may +contain at most one `PT_INTERP`. If it has an interpreter or any `DT_NEEDED`, it must use exactly +`/lib64/ld-linux-x86-64.so.2`; it is treated as static only when both are absent. A resolved library +must be `ET_DYN`, must not contain `PT_INTERP`, and may have its own `DT_NEEDED` entries. + +Program headers are the sole loader-visible authority. In addition to the ELF magic, `EI_CLASS` is +`ELFCLASS64`, `EI_DATA` is `ELFDATA2LSB`, `EI_VERSION` and `e_version` are `EV_CURRENT`, `e_ehsize` +is exactly 64, `EI_OSABI` is `ELFOSABI_SYSV`, `EI_ABIVERSION` is zero, every `EI_PAD` byte is zero, +`e_flags` is zero, and `e_phentsize` is exactly 56. `e_phnum` is nonzero and is not `PN_XNUM`; extended +program-header numbering is rejected rather than consulting section header zero. ELF and +program-header sizes, counts, offsets, virtual-address mappings, additions, and multiplications are +checked before access. Section headers may be absent and never affect validation; conflicting section +data is ignored because the runtime loader does not use it for this contract. A static primary has no +`PT_DYNAMIC`. Every dynamic primary, +interpreter, and library has exactly one bounded `PT_DYNAMIC`; multiple segments fail. Its entry width +is the ELF64 width, it contains a terminating `DT_NULL`, and every remaining byte in that segment is +zero. Missing termination or a nonzero trailing entry fails. + +Protocol 1 defines loader-visible string tags as exactly `DT_NEEDED`, `DT_SONAME`, `DT_RPATH`, +`DT_RUNPATH`, `DT_AUDIT`, `DT_DEPAUDIT`, `DT_CONFIG`, `DT_AUXILIARY`, and `DT_FILTER`. If any of these +tags exists, the table has exactly one `DT_STRTAB` and one `DT_STRSZ`. Their complete nonempty range +must map into exactly one readable `PT_LOAD` file range. Duplicate or conflicting table tags, +unmapped/overlapping ranges, an out-of-range string offset, or a string without NUL before `DT_STRSZ` +fails. `PT_INTERP` follows the same bounded-range rules, contains exactly one trailing NUL, and +contains no interior NUL. Every accepted dynamic string is valid UTF-8 and has no NUL or control +character before its terminator. + +Only `DT_NEEDED` may induce a library lookup. `DT_SONAME` is accepted only as nonempty descriptive +metadata, is bounded to 255 UTF-8 bytes, and contains neither `/` nor `\\`; it never adds a dependency. +`DT_RPATH`, `DT_RUNPATH`, `DT_AUDIT`, `DT_DEPAUDIT`, `DT_CONFIG`, `DT_AUXILIARY`, `DT_FILTER`, and +`DT_POSFLAG_1` are always rejected. + +The accepted dynamic-tag vocabulary is numeric and closed; symbolic constants are labels only. It is +exactly core values `0..14`, `16..28`, `30`, and `32..37`; GNU values `0x6ffffef5` +(`DT_GNU_HASH`), `0x6ffffef6` (`DT_TLSDESC_PLT`), `0x6ffffef7` (`DT_TLSDESC_GOT`), `0x6ffffff0` +(`DT_VERSYM`), and `0x6ffffff9..0x6fffffff` (`DT_RELACOUNT` through `DT_VERNEEDNUM`); and x86-64 +values `0x70000000`, `0x70000001`, and `0x70000003` (`DT_X86_64_PLT`, `DT_X86_64_PLTSZ`, and +`DT_X86_64_PLTENT`). Rejected string/acquisition tags above remain rejected even though their values +fall outside this allowlist. Every other value, including future standard, OS-specific, GNU, or +processor-specific tags, fails until a protocol revision explicitly adds it. + +For accepted `DT_FLAGS` (value `30`), no bit outside mask `0x0000001e` may be set; this allows only +`DF_SYMBOLIC`, `DF_TEXTREL`, `DF_BIND_NOW`, and `DF_STATIC_TLS`. For accepted `DT_FLAGS_1` +(`0x6ffffffb`), no bit outside mask `0x5eff976f` may be set. This mask deliberately excludes +`DF_1_LOADFLTR`, `DF_1_ORIGIN`, `DF_1_NODEFLIB`, `DF_1_CONFALT`, `DF_1_ENDFILTEE`, +`DF_1_GLOBAUDIT`, and `DF_1_WEAKFILTER`; every undefined bit also fails. Multiple `DT_FLAGS` or +`DT_FLAGS_1` entries fail rather than combining masks. Apart from repeatable `DT_NEEDED` and the +all-zero bytes after the first `DT_NULL`, every accepted tag appears at most once; duplicate +`DT_SONAME`, table, size, relocation, version, flag, initialization, hash, or x86-64 tags fail. +Accepted non-string tags describe relocation, symbol, version, initialization, or hash tables but do +not participate in protocol identity or dependency discovery. + +Protocol 1 metadata describes the primary's load-time ELF closure: its exact `PT_INTERP` and the +recursively traversed `DT_NEEDED` entries. It does not certify later application-directed `dlopen` or +child-process behavior, and the security model does not represent an application binary as trusted +merely because this structural profile passes. `DT_NEEDED` values containing `/`, `\\`, or `$` fail. +Rejecting `$` prevents glibc's `$ORIGIN`, `${ORIGIN}`, `$LIB`, `${LIB}`, `$PLATFORM`, and +`${PLATFORM}` expansion from making startup resolution differ from the validator's literal lookup. + +The image build copies the complete reviewed x86-64 startup-library closure into the flat directory +`/opt/edgezero/runtime-lib`; that directory contains regular files only and no subdirectory, symlink, +or duplicate basename. A dynamic primary's `PT_INTERP` is exactly +`/lib64/ld-linux-x86-64.so.2`. The validator preserves duplicate direct `DT_NEEDED` values for +metadata, sorts them bytewise, and resolves every dependency basename only as +`/opt/edgezero/runtime-lib/`. A missing, escaping, non-regular, multiply linked, or duplicate +candidate fails. Recursive inspection uses a device/inode visited set so dependency cycles terminate, +and every transitive library satisfies this same profile. The interpreter resolves inside the +immutable image root, is a regular `ET_DYN` file with no `PT_INTERP`, and is parsed and recorded as a +member of the validator's visited runtime closure but is not added to the primary's +`abi.needed` metadata array. + +The final image has no `/etc/ld.so.preload`. Every dynamic `binary-smoke` and provider-action launch +uses the container runtime's argv/entrypoint API directly, without a shell, to invoke that validated +interpreter with exact arguments `--inhibit-cache`, `--glibc-hwcaps-mask`, the empty-string mask, +`--library-path`, `/opt/edgezero/runtime-lib`, then `/work/bin/app-cli` and the validated operation +arguments. Thus `/etc/ld.so.cache`, default-directory precedence, and hardware-capability +subdirectories cannot select a different object for the validated startup closure. A static primary +is launched directly and has no `PT_INTERP` or `PT_DYNAMIC`. The verifier never invokes `ldd` and +never infers trust from loader output. Tests include preload presence, cache-only libraries, +hardware-capability alternates, default-directory duplicates, wrong interpreter, missing flat-closure +members, and an application-directed `dlopen` fixture demonstrating that such runtime behavior is +outside the metadata claim rather than silently certified. + +### 6.5 Protocol-owner CLI + +The synchronous `edgezero-provenance-validator` binary owns both encoding and validation. It has no +Tokio dependency and never executes an app binary. Its stable credential-free interface is: + +```text +edgezero-provenance-validator write-expected \ + --work-root /work \ + --app-repo-id \ + --source-revision <40-lowercase-hex> \ + --app-cli-package \ + --app-cli-bin \ + --workspace-id sha256:<64-lowercase-hex> \ + --platform-id sha256:<64-lowercase-hex> \ + --provenance-protocol 1 \ + --output /work/expected/expected.json + +edgezero-provenance-validator write-release-request \ + --work-root /work \ + --gate-sha <40-lowercase-hex> \ + --provenance-protocol 1 \ + --release-tag build-container-v \ + --output /work/release/release-request.json + +edgezero-provenance-validator package \ + --work-root /work \ + --binary /work/input/app-cli \ + --schema /usr/local/share/edgezero/provenance.schema.json \ + --expected /work/input/expected.json \ + --app-cli-version \ + --archive /work/packaged/artifact.tar + +edgezero-provenance-validator validate \ + --work-root /work \ + --archive /work/input/artifact.tar \ + --schema /usr/local/share/edgezero/provenance.schema.json \ + --expected /work/input/expected.json \ + --output /work/validated/app-cli + +edgezero-provenance-validator self-test \ + --fixtures /usr/local/share/edgezero/provenance-fixtures +``` + +`--work-root` is required for output-producing commands and must canonicalize to `/work` in the +container. Every caller-selected input and output parent for those commands must canonicalize beneath +it, except the trusted baked schema path, which must be the exact literal path shown above and must +resolve to the image-owned regular file. `self-test` accepts only the exact baked fixture path shown +above, which must resolve to the image-owned fixture directory. `write-expected` accepts only the typed bounded scalars shown above, fixes `schema-version` to +`1`, derives `container-ref` from the fixed repository plus `platform-id`, and atomically publishes +canonical `expected.json`; shell, `jq`, and generic JSON encoders are not supported producers. +`write-release-request` accepts only the exact typed gate SHA, protocol, and release-tag scalars, +fixes the three-key JCS shape from Section 8, and create-new publishes only the literal basename +`release-request.json` in a fresh `/work/release` output directory. It is the sole supported producer +of that file. +`package` validates canonical expected identity, inspects and resolves the source ELF inside the +pinned image, generates canonical metadata, and atomically publishes the deterministic archive. +`validate` performs the inverse checks and atomically publishes exactly one mode-0755 regular output +file. Each output parent is a fresh canonical directory, must be writable and empty, and the final +file must have link count one. + +The implementation writes a create-new temporary sibling, flushes and validates it, then performs a +Linux no-replace atomic rename to the final basename. Handled errors remove the temporary file before +return. SIGKILL, OOM, runner cancellation, or a container timeout may prevent in-process cleanup; the +host therefore removes the entire action-owned output parent after every abnormal/nonzero exit and +verifies it is absent before reporting failure or retrying. On success the host requires exactly the +one final file and no temporary sibling. `self-test` verifies a compiled manifest of exact fixture +paths, SHA-256 values, and expected valid/invalid outcomes; missing, extra, or changed fixtures fail. + +### 6.6 Split validation boundary + +Validation is deliberately two container invocations: + +1. **Trusted parse/extract:** `provenance-validate` runs the baked project-owned validator. It strictly + parses ustar and JSON, validates schema/JCS/duplicates, verifies identity/digest/size/ELF metadata, + proves required libraries resolve in the image, and extracts exactly one binary to + `/work/validated/app-cli`. The host then verifies the output directory contains only that regular, + non-linked file with the expected mode, size, and digest. +2. **Untrusted execution:** `binary-smoke` starts a new hardened container with only the verified + binary mounted read-only and tmpfs. It runs `--help` with no network or credentials and bounded + memory, pids, and wall time. + +The untrusted app binary never shares a writable mount with the parser/extractor. Within one composite +action invocation, the trusted validation step records the host path, digest, size, mode, device, and +inode only in action-private state for its later steps. No public action or reusable-workflow output +exposes a host binary path; the path becomes invalid when the mandatory end-of-action cleanup removes +the private workspace. + +The validator, schema, malformed fixtures, valid golden archive, and all required capabilities must +exist and pass before any image digest can be published. Golden tests cover both JSON documents, +JCS, duplicate keys, schema rejection, byte-exact ustar encoding and parsing, traversal/link/special- +file rejection, header and padding normalization, size limits, ELF inspection, dependency resolution, +exact extraction, and output-directory confinement. Repeated `package` runs over the same inputs must +produce byte-identical archives, and `validate` must accept that golden output. + +## 7. Reusable workflow and action contract + +### 7.1 Reusable workflow + +Inputs: + +- `app-repository`, `app-ref`, `app-repo-id`, `working-directory` (default `.`), `workspace-root`, + `app-cli-package`, `app-cli-bin`, `app-cli-artifact`, `rust-toolchain`; +- `cache` (default `false`), `cache-key-suffix`, `disclosure-acknowledged`, and `timeout-minutes` + (default 30), plus `app-env` (default `{}`); +- secret `app-checkout-token`. + +The reusable-workflow input schema is exact. `app-repository`, `app-ref`, `app-repo-id`, +`workspace-root`, `app-cli-package`, `app-cli-bin`, `app-cli-artifact`, and `rust-toolchain` are +required strings. +`working-directory`, `cache-key-suffix`, and `app-env` are optional strings with defaults `.`, the +empty string, and `{}`. `cache` and `disclosure-acknowledged` are boolean inputs defaulting to false. +`timeout-minutes` is a number input defaulting to 30 and must be an integer in the range already +defined. `app-checkout-token` is a required secret. No compatibility alias, platform/container input, +provider input, generic environment map, or arbitrary Cargo/action argument is accepted. + +The workflow verifies its hosted identity and materializes its resolved action source before it +processes application input. The EdgeZero source checkout, app authority checkout, and Copy A use +three distinct fixed children of a fresh workspace. After token removal, the trusted exporter creates +Copy A from the validated authority checkout under Section 5.2; checkout never writes directly into +Copy A. Every local action/helper is invoked only from the verified EdgeZero checkout at +`job.workflow_sha`; application paths cannot shadow action code. + +`app-cli-artifact` is 1..128 ASCII bytes matching +`[A-Za-z0-9][A-Za-z0-9._-]{0,127}` and must be unique among artifact uploads in the workflow run. It +does not partition the cache. The workflow has no provider inputs. Checkout persists no credentials. + +The producer pins `actions/upload-artifact@v7.0.1`, uploads the one literal `artifact.tar` path with +`archive:true`, `compression-level:0`, `include-hidden-files:false`, `if-no-files-found:error`, +`overwrite:false`, and `retention-days:1`, and requires nonempty artifact id/digest outputs. The ZIP +wrapper and service digest are transport checks, not protocol provenance. Each consumer pins +`actions/download-artifact@v8.0.1`, supplies exact `name`, private destination `path`, current repository and +run id, `merge-multiple:false`, `skip-decompress:false`, and `digest-mismatch:error`, and supplies no +cross-repository token, pattern, or artifact id. It then requires that the destination contains only +one regular single-link `artifact.tar`; archive parsing remains the protocol validator's job. + +Outputs are `artifact-name`, `action-version`, `action-revision`, plus every +`CallerExpectedIdentity` field: `app-repo-id`, `source-revision`, `app-cli-package`, `app-cli-bin`, +and `workspace-id`. `action-version` is exact `V` or release-fixture `C` parsed from +`job.workflow_ref`, and +`action-revision` is exact `job.workflow_sha`. It does not output `platform-id`, `container-ref`, or +protocol. + +The consumer job invokes public action `stackpop/edgezero/.github/actions/compute-app-cli-identity` +at the same exact `action-version` as the producer and every provider action. Its exact required +string inputs are `action-version`, `app-repository`, `app-ref`, `app-repo-id`, `workspace-root`, +`app-cli-package`, `app-cli-bin`, and `rust-toolchain`; `working-directory` is an optional string +defaulting to `.`, and `app-checkout-token` is a required sensitive string input supplied by the +caller from a GitHub secret and masked before use. The action requires +`github.action_repository==stackpop/edgezero` and `github.action_ref==action-version`, independently +materializes and validates a short-lived authority through Section 5.2, removes the credential +channel, computes identity, and cleans the authority on every exit. It outputs exactly the five +`CallerExpectedIdentity` fields and no authority path, descriptor, opaque handle, credential, +platform field, or other host state. The job compares every output with the reusable-workflow output +before invoking another EdgeZero action. + +Every later EdgeZero action receives `action-version`, requires the same runner-provided action +repository/ref equality, receives the consumer-verified `CallerExpectedIdentity`, and derives +`PlatformIdentity` from its local action files. Immutable release enforcement binds that version to +the producer's recorded `action-revision`; the consumer does not accept either value as a +caller-authored replacement. A source-bearing action does not trust the identity action's destroyed +authority: it independently repeats materialization and caller-identity verification under its own +action lifetime as defined below. + +Matrix callers use unique artifact names and compare identity per leg. Shared workflow outputs are +not used to aggregate matrix results. + +### 7.2 Provider actions + +Every provider action accepts `app-cli-artifact`, producer `action-version`, and +`CallerExpectedIdentity`, verifies its own repository/version context, derives local +`PlatformIdentity`, downloads exactly the named artifact into an action-private workspace, and runs +the full validation sequence itself. Before `validate`, it invokes the baked validator's +`write-expected` command with the supplied, already consumer-verified caller fields plus its locally +derived platform fields into a fresh action-private expected directory. It never accepts expected +JSON, a platform field, or a host binary path from the caller, and never reuses an expected file from +another action invocation. Before each subsequent container launch, the action rechecks the +validated path is the same confined regular file with the recorded digest, size, mode, and single +link. The private workspace is removed with `if: always()`. + +Every provider action also accepts the validated `app-env` JSON object (default `{}`); no provider +action inherits ambient application variables. + +The source-bearing actions are exactly `deploy-fastly` and `config-push-fastly`. In addition to the +common inputs, both require string inputs `app-repository`, `app-ref`, `app-repo-id`, +`workspace-root`, `app-cli-package`, `app-cli-bin`, and `rust-toolchain`, accept optional string +`working-directory` defaulting to `.`, and require sensitive string input `app-checkout-token`, which +the caller supplies from a GitHub secret and the action masks before use. Each uses those inputs to +independently materialize an action-local authority, removes the checkout credential channel before +any application code, provider-token creation, or provider-token injection, and recomputes all five +caller-identity fields. A mismatch with supplied `CallerExpectedIdentity` fails before artifact +execution or provider mutation. Cleanup removes and verifies the action-local authority and any Copy +B on every exit. No source-free action accepts repository-materialization inputs or +`app-checkout-token`. + +`deploy-fastly` additionally accepts `generated-output-paths`. It reuses one validated +binary for its `active-version`, optional credential-free `app-build`, and provider deploy operations +within that invocation. `active-version-fastly` is also a source-free action with inputs +`app-cli-artifact`, `CallerExpectedIdentity`, `fastly-service-id`, and `fastly-api-token`; it outputs +`version`, where an empty value is success only for a confirmed first production deploy. + +`validate-app-cli-provenance`, `deploy-fastly`, `active-version-fastly`, `healthcheck-fastly`, +`rollback-fastly`, and `config-push-fastly` all apply this handoff. An identity or path mismatch fails +before app code or provider mutation. `validate-app-cli-provenance` is validation-only and has no +outputs; success means that its independent download, parse/extract, host check, and binary smoke all +completed before its private workspace was removed. + +`config-push-fastly` validates and confines the selected repository/manifest/config file and derives +the exact named app-config environment overlay before container launch. Inline config is written to +one fresh host file and mounted read-only. `no-env` exposes no app-config overlay. + +Mutation actions publish `mutation-attempted` host-side before launching the mutating CLI. Named +containers receive bounded signal forwarding and post-cancellation reconciliation as specified by the +parent deploy contract. + +## 8. Image publication and compatibility + +Protocol 1 selects the official `rust:1.95.0-slim-bookworm` image and pins its `linux/amd64` leaf +manifest, not its multi-platform index. The digest resolved from the official registry on 2026-08-31 +is `sha256:6f9e63259f12e1e599296f5ecfed2bae46de4af0ee0525dd8b89c046e236d5c5`; implementation must +re-resolve and compare it immediately before committing the Dockerfile. The exact sccache asset is +`sccache-v0.10.0-x86_64-unknown-linux-musl.tar.gz` from the upstream v0.10.0 release, with upstream +checksum `1fbb35e135660d04a2d5e42b59c7874d39b3deb17de56330b25b713ec59f849b`. The v0.10.0 release has no +GNU Linux client asset; the static musl client is the reviewed Linux x86-64 artifact. Changing either +base digest or tool asset requires a new source revision and image digest. + +The validator lives at `.github/tools/edgezero-provenance-validator` as a self-contained Cargo +workspace: its package manifest contains its own `[workspace]`, it has its own committed lockfile, +and it has no path dependency outside that directory. The staged Docker context excludes the root +workspace `Cargo.toml` and `Cargo.lock`; it includes the complete validator directory plus the other +explicit image assets only. The Dockerfile invokes one exact +`cargo build --locked --release --manifest-path +.github/tools/edgezero-provenance-validator/Cargo.toml`. Gate tests run +`cargo metadata --locked --manifest-path` inside the staged context and then the exact Docker build, +so a manifest that names an absent workspace member or path dependency cannot pass. + +`image.json` is a reviewed record with exactly these typed fields: + +```json +{ + "repository": "ghcr.io/stackpop/edgezero-build-app-cli", + "tag": "build-container-v1", + "digest": "sha256:<64-lowercase-hex>", + "image-source-revision": "<40-lowercase-hex>", + "provenance-protocol": 1 +} +``` + +`tag` is informational. Runtime pulls use only `repository@digest`. + +The pin PR changes exactly `image.json` and canonical `image-release-evidence.json` together. The +latter is UTF-8 RFC 8785 JCS with no BOM, surrounding whitespace, or trailing newline. It contains +the following single data line; the Markdown fence line break is not file content: + +```text +{"approval-challenge":"<64-lowercase-hex>","approver-login":"","image-digest":"sha256:<64-lowercase-hex>","release-tag":"build-container-v","reviewed-at":"","run-attempt":"","run-id":"","schema-version":1,"screenshot-sha256":"sha256:<64-lowercase-hex>","source-revision":"<40-lowercase-hex>"} +``` + +Its values equal the current approval record and `image.json`; run identifiers are strings to avoid +JSON number precision loss. A gate-owned typed writer is the sole producer. Unknown/duplicate/missing +keys, wrong types/order/JCS bytes, invalid bounds, or cross-file mismatch fail. Runtime actions do not +read this evidence file. + +The rollout has five relevant commits and two action-release tags: + +- `G` is the protected gate and image-source baseline. It contains the protocol validator, schema, + golden/malformed fixtures, image verifier, fail-closed classifier, release-policy verifier, + approval gate, pin-PR updater, publisher contract checker, exact Dockerfile, root `.dockerignore`, + canonical image-context manifest, and both `.github/workflows/build-container-ci.yml` and + `.github/workflows/publish-build-container.yml`, plus the gate-rotation lock workflow and verifier. + Repository variable + `EDGEZERO_BUILD_CONTAINER_GATE_SHA` equals `G`. Organization ruleset + `edgezero-build-container-required-workflow` has target `branch`, active enforcement, no bypass + actors, conditions containing repository id `[]` and ref-name include + `["refs/heads/main"]` with no excludes, and exactly one `workflows` rule. That rule has + `do_not_enforce_on_create=false` and exactly one descriptor with the EdgeZero repository id, the + literal workflow path, full SHA `G`, and no `ref`. The workflow checks out `G` separately from + candidate source and executes only gate code from `G`; candidate files are data under test. A + change to a gate-owned or image-context path must land as a separately reviewed new `G`, and the + ruleset must be updated to that SHA, before any dependent release request. +- `S` is the full release-source commit. It descends from `G`, changes exactly the canonical + `release-request.json` described below, and leaves every gate-owned and image-context byte equal to + `G`. The publisher uses a trusted `G` helper to construct a fresh Docker context containing only the + manifested paths copied from the verified `G` checkout; it never builds a candidate Dockerfile or + admits an unmanifested repository path. The image has OCI label + `org.opencontainers.image.revision=S`, + `org.opencontainers.image.source=https://github.com/stackpop/edgezero`, and a protocol label + matching the baked validator. The final image overrides inherited source/revision labels, and + verification requires all three exact values. +- `B` is the baseline revision created after the pin PR commits the verified digest and `S` to + `image.json` plus its bound `image-release-evidence.json`, and permanent pin CI is enabled. +- `H` is the protected-main commit proposed as the final action revision. It contains the unchanged + reviewed pin plus the cache, provenance, launcher, and consumer implementation. The prepublication + adoption documents still contain only the explicit action-version placeholder allowed by the + bootstrap documentation gate. +- `C` is a unique canonical exact patch-version tag published as an immutable + GitHub Release at `H` with `prerelease:true`. Its tag has no prerelease suffix. The complete local + suite runs from a clean detached `H`; the complete hosted cross-repository/provider suite invokes + `C`. A failure leaves `C` immutable and requires a new commit plus a newly selected unused `C`. +- `P` is defined as `H` only after the exact-`H` local suite and complete hosted suite through `C` + pass. Before that point, no text may call `H` final revision `P`. +- `V` is a canonical exact stable action version `v..`, distinct from `C`, + selected only after `H` qualifies as `P`. An immutable stable release `V` is published at `P`, then + a final hosted identity and consumer smoke uses literal `V`. The release API and remote refs must + both resolve `C` and `V` to `P`. +- `R` is the later protected-main documentation revision. It changes only tracked Markdown adoption + surfaces and `docs/.edgezero-action-release.json`, replacing the bootstrap placeholder with literal + `V` and activating the documentation gate's released state. It does not alter action/workflow code + and is not a new action revision; published consumers pin `V`, never `P`, `C`, a major/minor tag, or + a branch. + +`docs/.edgezero-action-release.json` is absent through `P`. Revision `R` adds it as UTF-8 RFC 8785 +JCS with no BOM, surrounding whitespace, or trailing newline. It contains the following single data +line; the Markdown fence line break is not file content: + +```text +{"action-revision":"

","action-version":"","schema-version":1} +``` + +The action revision and version obey their exact grammars and must match the already published stable +immutable release and peeled remote tag. Unknown, duplicate, missing, reordered, or mistyped fields +fail. + +`release-request.json` is UTF-8 RFC 8785 JCS with no BOM, surrounding whitespace, or trailing +newline. It contains the following single data line; the Markdown fence line break is not file +content: + +```text +{"gate-sha":"","provenance-protocol":1,"release-tag":"build-container-v"} +``` + +`gate-sha` is lowercase full hex and must equal the active gate variable and required-workflow SHA; +the decimal has no sign or leading zero. The source PR changes this file and no other path. Its tag is +unused until the reviewed operator creates that exact protected tag at `S`. A changed Dockerfile, +`.dockerignore`, image manifest, validator input, workspace lockfile, or other repository image-context +byte is a gate update, never an ordinary `S` candidate. + +The reviewed operator creates this file using a local image built solely from the canonical context +staged from clean `G`. The build records local image/config identity `L` from BuildKit's `--iidfile`, +and the gate-owned verifier requires `L` to be a leaf `linux/amd64` image with revision label `G`, the +expected protocol, and the complete validator/toolchain contract. The operator then invokes +`write-release-request` by immutable local identity `L`, with no network, credentials, repository +mount, or tag lookup and with only the closed `release-request-write` profile. This local bootstrap +image is not published and is not digest `D`; no already-published or pinned `G` image is assumed. + +There is no literal same-commit requirement between image source and pin record. Compatibility is +enforced by digest, image labels, and exact `provenance-protocol`. Changing the validator/archive +contract requires a protocol bump and a new image before the actions using that protocol are pinned. + +The gate owns a canonical path manifest and `.github/CODEOWNERS`; the latter assigns every manifested +path and itself to `@stackpop/edgezero-build-container-gate-reviewers`. The no-bypass default-branch +ruleset requires code-owner review, at least two approving reviews, dismissal of stale approvals, and +the merge queue. A gate-update PR changes only paths in the union of the old and candidate canonical +manifests. Old `G` validates the candidate manifest's canonical sorted form, validates candidate +`CODEOWNERS` coverage, classifies the change as `mode=gate-update`, executes only old-`G` static and +subject-data checks, and never runs candidate gate code. A mixed gate/non-gate change fails. If that PR +passes the old gate and required human reviews, merging it creates candidate `G'`, not `S`. Until +activation, the protected base's manifested bytes differ from active `G`, so every ordinary candidate +and release preflight fails. + +To activate `G'`, dispatch the gate-owned rotation-lock workflow from protected main while main and +both gate pointers still equal old `G`. It uses the exact repository-global +`edgezero-build-container-publication` concurrency group with `cancel-in-progress:false` and +`queue:max`; after all older publishers finish, its unprivileged acquire job records its run id and its +second job waits for independent approval on environment `build-container-gate-rotation-lock`. The +waiting workflow holds the concurrency group. Before first use, a live fixture must prove that a +publisher dispatched behind this waiting job remains pending and starts no build/push step. + +While the lock is held, the operator verifies no older publication is active or pending ahead of it, +sets repository variable `EDGEZERO_BUILD_CONTAINER_RELEASE_STATE` from `enabled` to +`disabled::`, removes the release environment's tag policy, and verifies release is +disabled. The operator then merges the gate update through the one-entry queue. From clean detached +`G'`, run the full gate suite and require the generic post-merge main-push assertion at `Q=G'`; update +the disabled marker to `disabled:::`; then update both +`EDGEZERO_BUILD_CONTAINER_GATE_SHA` and the organization required-workflow descriptor SHA to `G'`. +Either intermediate mismatch fails all required runs. Verify the base's complete manifested tree +equals `G'`, restore the sole tag policy, produce new independently reviewed prerequisite evidence, +and set release state back to exact `enabled`. Only then may a reviewer enter the canonical rotation +evidence comment and approve the waiting lock job, whose old-`G` helper verifies its own run/context, +the main head, public repository variables, and evidence digest before releasing concurrency. No +ruleset bypass is used for a gate update. + +If activation fails before both pointers and all post-activation checks agree, keep release disabled +and restore both pointers to old `G` while the lock remains held. Because the base then still contains +`G'`, ordinary work remains blocked. Old `G` has a distinct `mode=gate-rollback`: both active pointers +must equal old `G` and release must remain disabled; the current base must be exactly the failed `G'` +tree that old `G` validates as a canonical gate update; the proposed head's complete manifested tree +must be byte-identical to old `G`; changed paths must be confined to the union of the `G'` and old-`G` +manifests; and no release request, pin record, or non-gate path may change. Merge that separately +reviewed rollback through the one-entry queue, require generic main-push evidence at the resulting +head `Q`, verify the base tree and both pointers equal old `G`, and only then restore release policy. +If old `G` cannot validate either the current `G'` tree or the exact restoration, recovery is a manual +trust-root operation requiring the same independent review as bootstrap, and release remains disabled +throughout. No mixed `{variable, descriptor, base manifest}` state is a degraded operating mode. +If manual recovery cannot finish before the lock run expires, leave release state disabled and the tag +policy absent before canceling it; every later publisher must acquire concurrency and fail closed on +the disabled state before image build or push. + +`build-container-gate-rotation-lock` has no secret or variable and no enabled GitHub App custom +protection rule. It permits only protected `main`, disables administrator bypass, requires a nonempty +reviewer set with self-review prevention, and is referenced with `deployment:false`. Its one approval +comment is exactly: + +```text +edgezero-gate-rotation-v1 {"evidence-sha256":"sha256:<64-lowercase-hex>","head-sha":"","lock-run-id":"","new-gate-sha":"","old-gate-sha":"","result":"activated|rolled-back","reviewed-at":""} +``` + +The compact JSON uses the shown key order/types and no extra whitespace. The reviewer differs from +the operator, `reviewed-at` uses the release approval's exact time grammar and 15-minute freshness +window, run id equals the lock run, and `head-sha` is the current protected head. The attached +canonical evidence proves the exact policy/pointer/base checks; the helper recomputes its digest and +requires exactly one current-run protocol comment. An activated result requires head tree and both +pointers at `G'`, restored tag policy, and release state enabled. A rolled-back result requires an +exact old-`G` tree, both pointers at old `G`, restored tag policy, and release state enabled. + +The dedicated repository ruleset `edgezero-build-container-main` has target `branch`, enforcement +`active`, no bypass actors, and ref-name conditions including exactly `refs/heads/main` with an empty +exclude list. Its rules are exactly: + +- `pull_request` with `allowed_merge_methods:["squash"]`, + `dismiss_stale_reviews_on_push:true`, `require_code_owner_review:true`, + `require_last_push_approval:true`, `required_approving_review_count:2`, and + `required_review_thread_resolution:true`; and +- `merge_queue` with `check_response_timeout_minutes:60`, `grouping_strategy:"ALLGREEN"`, + `max_entries_to_build:1`, `max_entries_to_merge:1`, `merge_method:"SQUASH"`, + `min_entries_to_merge:1`, and `min_entries_to_merge_wait_minutes:0`. + +The one-entry build and merge limits prevent a passing merge-group result from authorizing a +different batched tree. Missing, extra, or changed semantic rule fields fail the prerequisite audit. + +The two release-tag repository rulesets both have source type `Repository`, source +`stackpop/edgezero`, target `tag`, active enforcement, and ref-name conditions including exactly +`refs/tags/build-container-v*` with an empty exclude list. Ruleset +`edgezero-build-container-tag-immutability` has no bypass actors and exactly `update` with +`update_allows_fetch_and_merge:false` plus `deletion` rules. Ruleset +`edgezero-build-container-tag-creation` has exactly one bypass actor, the numeric team id for +`edgezero-build-container-releasers` with type `Team` and mode `always`, and exactly one `creation` +rule. It has no update or deletion rule. Missing, extra, defaulted, or changed source, target, +condition, actor, mode, parameter, or rule fails the prerequisite audit. + +Two additional repository rulesets apply the same creation/immutability split to action releases. +Both have source type `Repository`, source `stackpop/edgezero`, target `tag`, active enforcement, and +ref-name conditions including exactly `refs/tags/v*` with no excludes. +`edgezero-action-version-tag-immutability` has no bypass actors and exactly `update` with +`update_allows_fetch_and_merge:false` plus `deletion`; `edgezero-action-version-tag-creation` has only +the same reviewed releaser Team actor in `always` mode and exactly `creation`. Repository immutable +releases are enabled. The broader `v*` ruleset protects distinct exact patch versions `C` and `V`, +while the release procedure separately enforces their canonical grammars, absence, release states, +and commit targets. + +The action-release operator uses a short-lived fine-grained personal access token selected only for +repository `stackpop/edgezero`, expiring within 24 hours, with exactly repository `Contents:write` +and `Workflows:write`, implicit metadata read, and organization `Members:read`; every other repository +or organization grant is disabled. A classic PAT, installation token, `GITHUB_TOKEN`, broader +repository selection, extra grant, or token shared with Actions is invalid. The operator preserves +the fine-grained token settings as review evidence, supplies the token to a local non-logging helper +through a private descriptor rather than argv or environment, and destroys it after release. + +That helper permits no redirect and only requests with +`Accept: application/vnd.github+json`, `X-GitHub-Api-Version: 2026-03-10`, +`User-Agent: edgezero-action-release/1`, and its authorization header: +`GET /user`, `GET /organizations//team//memberships/`, +`POST /repos/stackpop/edgezero/releases`, `PATCH /repos/stackpop/edgezero/releases/`, and +`GET /repos/stackpop/edgezero/releases/`. The membership response must be active. POST +creates a draft with exact tag, full target commit, no assets, and required prerelease boolean; PATCH +changes only `draft` to false. GET verifies author, tag, target, draft/prerelease/immutable state, and +release id. Any other method, host, path, query, body field, redirect, credential type, or response +shape fails. Anonymous remote-ref resolution and release-attestation verification are separate +read-only checks. + +Repository ruleset `edgezero-build-container-pin-branches` has source type `Repository`, source +`stackpop/edgezero`, target `branch`, active enforcement, conditions including exactly +`refs/heads/edgezero-build-container-pin/*` with no excludes, and exactly one bypass actor: the +dedicated publisher App's numeric integration id, type `Integration`, mode `always`. Its rules are +exactly `creation`, `update` with `update_allows_fetch_and_merge:false`, and `deletion`. Thus only that +App can create, move, or delete a canonical pin branch. A pin branch is exactly +`edgezero-build-container-pin/` and its PR title is exactly +`chore(actions): pin build container for `; any different head repository, branch, author, +integration, title, or changed-path set fails required pin CI. + +Publication order is: + +1. Land `G`, then configure and verify the exact active organization ruleset and its sole + required-workflow descriptor + `{repository_id:,path:".github/workflows/build-container-ci.yml",sha:G}` + and an active default-branch ruleset that requires the merge queue with no bypass actor. Configure + the protected release and rotation-lock environments, split tag-creation/immutability rulesets, + dedicated GitHub App, exact release-state `enabled` and publisher App/bot identity repository + variables, and repository permissions. Prove the + rotation workflow holds publication concurrency before the first gate update. The one-time + bootstrap of `G` is explicitly a human-reviewed trust-root operation; + candidate-controlled checks are not represented as independent proof of `G`. +2. Open the isolated `release-request.json` candidate, run it through the organization-required + workflow from `G`, complete the credential smoke, and merge it only through the verified merge + queue. The resulting default-branch commit is `S`. Before tagging, require the repository-local + workflow's latest-attempt run API record to have event `push`, path + `.github/workflows/build-container-ci.yml`, and `head_sha=S`. Require both stable jobs to report + `head_sha=S`, success, and exactly one successful step named `assert-exact-main-push-context`. That + immutable step invokes only the `G` helper and internally asserts `github.ref==refs/heads/main`, + `github.event.after==github.sha==github.workflow_sha==S`, and active gate SHA `G`; the REST API + does not expose those context fields, so the step result is the external evidence. A PR or + `merge_group` result cannot substitute for this exact post-merge run. The first package may not + exist yet; its public-visibility gate occurs after its first push and before a pin PR. +3. The publisher verifies `S`'s manifested bytes equal `G`, constructs a fresh context solely from + the clean `G` checkout and canonical context manifest, builds with the gate-owned Dockerfile, pushes + by protected release tag, and captures digest `D` from BuildKit's metadata output. +4. Verify `D` is a leaf linux/amd64 image, labels identify `S` and protocol, exact tool versions and + target are installed, validator capability tests pass, and runtime works read-only/non-root. +5. Ensure the GHCR package is public and linked to `stackpop/edgezero`, then prove an anonymous pull + and smoke by `D`. The first release stops here until an operator changes package visibility, reruns + the `G` preflight in package-present mode, attaches its evidence, and reruns the same tag. +6. Open or update an idempotent App-authored PR on the exact protected pin branch, committing + `image.json = {D, S, protocol}` plus its canonical run/approval evidence record. Required pin CI + verifies App/branch/run/approval origin and re-verifies the image before merge; merging the passing + PR creates baseline `B`. +7. Select currently absent canonical patch version `C` for candidate qualification. Implement the + remaining executable plans on top of `B`; keep the four prepublication adoption documents at the + exact bootstrap placeholder. Merge the executable candidate through the queue and record the + resulting protected-main commit as candidate `H`. +8. From a clean detached checkout of exact `H`, rerun the complete pin, actionlint, zizmor, schema, + fixture, container, Rust, documentation, and contract suites. A locally authenticated active member + of the exact releaser team authorized by the creation ruleset creates a draft for `C` with exact + target `H`, no assets, and `prerelease:true`, then publishes it. Require release API fields + `draft:false`, `prerelease:true`, and `immutable:true`, remote peeled ref `C==H`, authenticated + operator identity equal to release author, and recorded team membership plus release attestation. + Run the complete hosted cross-repository/provider suite with every EdgeZero workflow/action ref + equal to literal `C`. Only after both suites pass, designate `H` as `P`, select a distinct currently + absent stable version `V`, and repeat the same actor/draft/publish/evidence procedure for `V` at + `P`, with `prerelease:false`. Verify API and remote-ref resolution and run a final hosted identity/ + consumer smoke with literal `V`. A candidate failure requires a new commit and unused `C`; no `V` + has yet been selected. A post-publication verification failure cannot retarget `V` and is corrected by a new patch + release. Deletion of the GitHub Release object, or mutation of its title, notes, prerelease, or + latest metadata by an actor with sufficient repository privilege, remains an accepted availability/ + discovery risk; the no-bypass tag rules still prevent tag deletion or retargeting, and GitHub's + immutable-release tombstone prevents tag-name reuse. Preserve both generated release attestations + in the release evidence and verify release existence and required state during the final audit. +9. Only after the literal-`V` smoke passes, open documentation-only revision `R`. It adds the exact + action-release record bound to `{V,P}`, replaces every bootstrap placeholder in tracked Markdown + with literal `V`, and changes no executable, workflow, action metadata, gate-owned, or non-document + path. The already active dual-state gate verifies the stable immutable release and remote ref, + applies final-mode documentation checks, and rejects deletion/downgrade of the release record. + Merge `R` through the queue, run the final documentation build/pin scan on protected main, and + record `R`; no action release or action revision changes at this step. + +Gate baseline `G` contains `.github/workflows/build-container-ci.yml`. The active organization ruleset +uses its exact repository id, path, and SHA `G`; it uses neither a branch nor a candidate-controlled +ref. A repository PR cannot substitute its own workflow or helper implementation. The workflow +supports `pull_request`, `merge_group`, protected-default-branch `push`, and a manual +`workflow_dispatch` credential-smoke mode, with no workflow-level path filter. It exposes two stable +required job names on every candidate and grants only workflow-level `contents:read`, `actions:read`, +and `pull-requests:read`; neither job references an environment or mutation credential: + +- `build-container-local` computes the documented image-input path set. It builds and smokes the local + image when relevant and otherwise runs an explicit successful not-applicable step. +- `build-container-pin` detects every add, change, or deletion of either pin-record file. When + relevant, both must exist and be the only changed paths; it validates both structures and their + equality, verifies the exact pin branch/PR title/head repository and dedicated App bot id/login, + validates the bound publisher run/attempt and approval evidence described below, anonymously pulls + the exact digest, and runs the complete published-image verifier. Otherwise it explicitly succeeds + as not applicable. + +Each organization-required job parses `github.workflow_ref` and requires its repository and path to be +exactly `stackpop/edgezero/.github/workflows/build-container-ci.yml`; it also requires +`github.workflow_sha` to equal repository variable `EDGEZERO_BUILD_CONTAINER_GATE_SHA`. The variable +is a full SHA and equals `G` and the ruleset descriptor SHA. On a protected-default-branch push, each +stable job instead requires exactly one successful `assert-exact-main-push-context` step whose trusted +helper proves `github.ref==refs/heads/main` and `github.event.after==github.sha==github.workflow_sha==Q`; +the job still checks out gate code at the variable's active `G`. Each job checks out +`G` and the candidate revision into distinct roots and invokes only the protected gate's classifier and +verification driver. Candidate scripts are never sourced or +executed as gate authority. Each job performs its own fail-closed change classification from the +candidate base and head so a failed shared classifier cannot skip a required job. The local-image set +is not a hand-maintained approximation of a root context: it is the release request plus the complete +canonical image-context manifest, and every manifested image path is also gate-owned. Classification +output is exactly two lines in fixed order: `mode=ordinary`, `mode=gate-update`, or +`mode=gate-rollback`, followed by `relevant=true` or `relevant=false`. Gate-update and gate-rollback +always report relevant true. An unconditional terminal assertion rejects missing, duplicate, or +malformed output and proves exactly one of the relevant ordinary, gate-update, gate-rollback, or not- +applicable branches ran; an invalid classifier can never make all conditional paths disappear behind +a green job. +Contract tests pin pull-request, merge-group, and push ranges, protected workflow/helper provenance, +event triggers, job names, context-manifest closure, deletion handling, output validation, and +explicit no-op behavior. The existing path-filtered +`deploy-action.yml` remains separate. Thus required checks always materialize without running Docker +on unrelated changes, and no later syntactically valid pin can bypass image, platform, label, +protocol, public-access, target, validator, or exact-version checks. + +For a pin candidate, the trusted `G` helper reads `image-release-evidence.json` and uses only the +required job's read-only `GITHUB_TOKEN`. It polls the identified same-repository workflow run at most +30 times with a fixed 10-second delay, then requires completion/success, event `push`, workflow path +`.github/workflows/publish-build-container.yml`, `head_sha=S`, `head_branch=`, and exact +run attempt. The run has successful `build-and-verify` and `update-pin` jobs, each with exactly one +successful `assert-exact-publisher-context` step; that trusted `G` step internally proves the protected +tag ref, `github.sha==github.workflow_sha==S`, active gate, release state enabled, run id/attempt, and +tag. The helper also reads the run's non-paginated approvals, requires exactly one current-attempt +release protocol record, and compares every comment field and API reviewer login with the evidence +file. Missing, pending, duplicate, stale, foreign, or contradictory evidence fails before image pull. +The pin check never accepts a candidate-provided check name or PR body as publisher evidence. + +For ordinary image or pin candidates, each job also compares every path in the active gate manifest +between the protected base and its `G` checkout and fails on any mismatch. Gate-update and the exact +recovery-only gate-rollback are the only exceptions. For gate update, old `G` must classify the +candidate's manifested-path change explicitly, verify the base +still equals old `G`, require every changed path to be in the union of old and candidate manifests, +require no changed release request or pin, run the protected static/subject-data gate-update checks, +and record that mode in its terminal marker. Candidate gate scripts are not executed in either gate +mode. Gate rollback obeys the exact failed-`G'`-to-old-`G` restoration contract above. An +ordinary release request must change exactly `release-request.json`; its `gate-sha` is `G`, its +protocol is 1, and every repository image-context input in `S` is byte-identical to `G`. + +The protected workflow also exposes non-required job `build-container-release-preflight` only for a +`workflow_dispatch` request whose body uses `ref:"main"` while protected `main` is exactly `G`. A +workflow dispatch ref is a branch or tag name, not a raw commit SHA. Its required typed inputs are a +same-repository candidate PR number, exact head repository, and full lowercase head SHA. Its +`run-name` is exactly +`build-container-release-preflight pr= repo= sha=<40-lowercase-hex>`, making the +claimed binding API-visible. The job uses environment +`{name: build-container-release, deployment: false}`, checks out only exact `G` into a private gate +root, and runs no candidate repository script. Its fixed step `assert-exact-g-dispatch-context` +invokes only the protected `G` helper, uses the read-only `GITHUB_TOKEN` to fetch the named PR, and +requires the three inputs to equal the current PR number/head repository/head SHA as well as event `workflow_dispatch`, `github.ref==refs/heads/main`, +`github.sha==github.workflow_sha==G`, and repository variable +`EDGEZERO_BUILD_CONTAINER_GATE_SHA==G`. After the environment reviewer approves it, +`actions/create-github-app-token@v3.2.0` consumes the exact stored +App variable and private-key secret with repository `edgezero` and explicit `contents:write` and +`pull_requests:write`. The job requires its installation-ID output to equal the stored expected ID, +reads only `stackpop/edgezero` with the token, and lets the action's mandatory post step revoke the +token. A successful check run from the GitHub Actions App proves the protected environment's stored credential, rather +than only an operator's local copy, can mint the publisher's exact token before `S`. + +The final environment policy is tag-only, so the smoke uses a bounded transition. An administrator +temporarily adds one custom branch deployment policy equal to the literal protected default-branch +name `main`, dispatches the workflow with body `ref:"main"` while `main==G`, then removes that branch +policy without changing the App variables or secret. The final preflight requires the environment to +be back to its sole `build-container-v*` tag policy and the successful workflow run API record to have +event `workflow_dispatch`, path `.github/workflows/build-container-ci.yml`, `head_sha=G`, and exact +display title/run-name for the current candidate. Its job +must contain exactly one successful `assert-exact-g-dispatch-context` step and identify the exact +candidate PR input, `stackpop/edgezero` head repository, and current PR head SHA. The step's success is +the API-visible evidence for the internal workflow-SHA assertion. Every App variable/secret +`updated_at` value is no later than that run's completion time. Any new candidate commit or credential +update invalidates the smoke and requires the bounded transition again. The temporary branch policy +is literal `main`, never a wildcard or caller-supplied branch. + +Repository-administrator bypass of environment protection is disabled. GitHub's documented REST +environment representation does not expose that switch, so neither the helper nor its fake-API tests +claim to verify it automatically. After the bounded credential smoke is complete and the final +tag-only policy is restored, an independent maintainer who is not the preflight verifier opens the +repository's `build-container-release` environment settings and captures a PNG showing the repository, +environment name, disabled administrator-bypass control, and final deployment-policy list. +The verifier supplies that file plus the reviewer's login and RFC 3339 review time to the preflight. +The helper rejects a non-PNG file, a reviewer equal to the verifier, a future review time, or evidence +whose recorded candidate head SHA differs from the current PR head; it records that SHA, the literal +basename, and `sha256:<64-lowercase-hex>` file digest under `environment.administrator-bypass` with +`allowed:false`, `verification:"manual-ui"`, `reviewer`, and `reviewed-at`. A separately +authenticated operator attaches the byte-identical PNG with the canonical evidence and digest to the +candidate PR. Release checkpoint 1 requires a maintainer other than the verifier and recorded reviewer +to recompute the attachment digest and confirm the screenshot visibly proves the disabled setting. +Any subsequent environment-policy change or new candidate commit invalidates this manual evidence. + +Before designating or tagging `S`, an operator runs the repository-owned preflight with a dedicated +policy-audit token, a separate package-audit token, the candidate PR number, the expected App and +installation IDs, and the App private key from a local file. The helper is executed only from a clean, +detached checkout whose `HEAD` is exact gate SHA `G`; it verifies that condition before reading a +credential. The policy-audit token is a short-lived fine-grained personal access token owned by the +verified active `stackpop` organization-owner login, selected for only `stackpop/edgezero`, with +repository permissions `Actions:read`, `Checks:read`, `Environments:read`, `Pull requests:read`, +`Variables:read`, implicit `Metadata:read`, and `Administration:write`, plus organization permissions `Members:read` +and `Administration:write`. The two administration grants are unavoidable for the reviewed GitHub API: +repository ruleset bypass actors are hidden without ruleset write access, and organization required- +workflow ruleset inspection requires organization administration access. This is an administrative +credential even though the helper is read-only; the contract does not claim that GitHub exposes a +machine-verifiable complete grant set for a supplied fine-grained PAT. + +Before use, a second operator records the token's selected repository, exact displayed grants, +expiration, and a screenshot digest in the prerequisite evidence. The helper authenticates the +expected verifier login and uses the policy token only through one wrapper whose literal allowlist is +`GET` on these routes, with only documented pagination and filter query keys: + +```text +/user +/orgs/stackpop/memberships/{verifier-login} +/orgs/stackpop/teams/edgezero-build-container-releasers/memberships/{verifier-login} +/orgs/stackpop/actions/permissions +/orgs/stackpop/rulesets +/orgs/stackpop/rulesets/{ruleset-id} +/repos/stackpop/edgezero +/repos/stackpop/edgezero/actions/permissions +/repos/stackpop/edgezero/immutable-releases +/users/{publisher-bot-login} +/repos/stackpop/edgezero/pulls/{candidate-pr} +/repos/stackpop/edgezero/rulesets +/repos/stackpop/edgezero/rulesets/{ruleset-id} +/repos/stackpop/edgezero/actions/variables/EDGEZERO_BUILD_CONTAINER_GATE_SHA +/repos/stackpop/edgezero/actions/variables/{approved-repository-variable-name} +/repos/stackpop/edgezero/environments/build-container-release +/repos/stackpop/edgezero/environments/build-container-release/deployment-branch-policies +/repos/stackpop/edgezero/environments/build-container-release/deployment_protection_rules +/repos/stackpop/edgezero/environments/build-container-release/variables/{approved-variable-name} +/repos/stackpop/edgezero/environments/build-container-release/secrets/EDGEZERO_BUILD_CONTAINER_APP_PRIVATE_KEY +/repos/stackpop/edgezero/commits/{candidate-sha}/check-runs +/repos/stackpop/edgezero/actions/runs/{run-id} +/repos/stackpop/edgezero/actions/runs/{run-id}/jobs +/repos/stackpop/edgezero/actions/runs/{run-id}/approvals +``` + +Every GitHub REST request made by a gate helper, including the approval gate and bounded App-token +test, sets exact non-authorization headers `Accept: application/vnd.github+json`, +`X-GitHub-Api-Version: 2026-03-10`, and `User-Agent: edgezero-build-container-gate/1`. A missing or +different value fails before network access. Authorization is added only by the credential-specific +wrapper. Responses must report the selected API version, use the expected JSON media type, and obey +the endpoint's exact success status; a redirect or silent fallback is failure. Specifically, every +response must contain `X-GitHub-Api-Version-Selected: 2026-03-10`. A response with a body must parse +its `Content-Type` to media type exactly `application/json`, with no charset or charset `utf-8`, and +must contain exactly one complete JSON value. GET succeeds only with 200, bounded test-token creation +only with 201, and token revocation only with 204 and an empty body; the 204 response has no JSON +media-type requirement. + +`{approved-variable-name}` is exactly one of `EDGEZERO_BUILD_CONTAINER_APP_ID`, +`EDGEZERO_BUILD_CONTAINER_APP_INSTALLATION_ID`, or +`EDGEZERO_BUILD_CONTAINER_RELEASE_TEAM_ID`. +`{approved-repository-variable-name}` is exactly one of +`EDGEZERO_BUILD_CONTAINER_RELEASE_STATE`, `EDGEZERO_BUILD_CONTAINER_PUBLISHER_APP_ID`, +`EDGEZERO_BUILD_CONTAINER_PUBLISHER_BOT_ID`, or `EDGEZERO_BUILD_CONTAINER_PUBLISHER_BOT_LOGIN`. +Release state obeys the exact state grammar above, App/Bot IDs are canonical positive decimals, and +the bot login equals the independently verified dedicated App bot account. The user lookup must +return that exact login, numeric id equal to `EDGEZERO_BUILD_CONTAINER_PUBLISHER_BOT_ID`, and +`type:"Bot"`. All other numeric +placeholders are canonical positive decimals; SHA and login placeholders must equal values already +validated from the candidate or authenticated API response. Paginated list calls require +`per_page=100` and a canonical positive `page`; check-run +calls also +fix the documented app/latest filters used by the verifier. No redirect is followed. A request with +any other credential, method, path, placeholder value, query key/value, or fixed header fails before +network access; fake-API and static tests cover the complete allowlist. The package-audit token +is a classic personal access token belonging to an active `stackpop` organization owner. Its granted +normalized OAuth-scope set is exactly `{read:org,read:packages}`; the helper verifies the +authenticated login, active owner membership, and returned `X-OAuth-Scopes` header before using that +same token for every package query. Neither local token is stored in GitHub Actions. They are supplied +only as `EDGEZERO_RELEASE_POLICY_AUDIT_TOKEN` and +`EDGEZERO_RELEASE_PACKAGE_AUDIT_TOKEN`, respectively. The helper rejects byte-equal token values +before making an API request and never logs either value. The policy- and package-token wrappers never +issue a mutation request. The helper's only non-GET requests are the bounded test-token creation and +revocation calls described below; it never mutates persistent repository settings, packages, pull +requests, rulesets, or comments. It requires all of the following and emits canonical evidence for a +separately authenticated operator to attach to the candidate PR: + +- environment `build-container-release` has administrator bypass disabled, a nonempty + `required_reviewers` rule with `prevent_self_review=true`, uses custom deployment policies, and has + no GitHub App custom deployment-protection rule and exactly one deployment policy, type `tag`, with + name `build-container-v*`. The protection-rules endpoint must return HTTP 200, `total_count:0`, and + an empty `custom_deployment_protection_rules` array; its separately supplied + administrator-bypass evidence satisfies the manual contract above; +- organization and repository Actions permission responses both have + `sha_pinning_required:false`; if an enterprise override still rejects exact version tags, the + hosted exact-version prerequisite fails and release is blocked. Consumer repositories must likewise + permit version-tag action refs; +- an active organization ruleset with no bypass actor has exactly one required-workflow descriptor: + the EdgeZero repository id, `.github/workflows/build-container-ci.yml`, full SHA `G`, and no ref; + `do_not_enforce_on_create` is false. Repository ruleset `edgezero-build-container-main` has the exact + target, enforcement, ref conditions, no-bypass state, pull-request parameters, and seven merge-queue + parameters defined above; no default or omitted field may weaken them. `.github/CODEOWNERS` assigns + the canonical gate-owned path manifest and every listed path to the exact gate-reviewer team. The + protected workflow's source repository, path, workflow SHA, and candidate SHA are recorded. The + exact App-only pin-branch ruleset is active and its Integration actor id equals the dedicated + publisher App id. The immutable-releases endpoint returns HTTP 200; parsed field `enabled` is + exactly boolean `true`, and `enforced_by_owner` is present as a boolean and recorded. Additional + response fields do not change this decision. Four active repository + tag rulesets have the exact source/target/ref/rule objects defined above: the image and action + creation/immutability pairs. Each immutability ruleset prohibits update and deletion and has no + bypass actor. Each creation ruleset prohibits creation and has exactly one bypass actor: team + `edgezero-build-container-releasers`, with the numeric ID stored in + `EDGEZERO_BUILD_CONTAINER_RELEASE_TEAM_ID` and bypass mode `always`; it contains no update or deletion + rule. The verifier actor is an active member of that team. The candidate's successful + required-workflow run is a `merge_group` + run for its final queue merge candidate, uses that exact protected workflow source, and contains + successful `build-container-local` and `build-container-pin` jobs from the GitHub Actions App; +- protected-environment variables `EDGEZERO_BUILD_CONTAINER_APP_ID` and + `EDGEZERO_BUILD_CONTAINER_APP_INSTALLATION_ID` equal the reviewed numeric IDs, environment variable + `EDGEZERO_BUILD_CONTAINER_RELEASE_TEAM_ID` equals the ruleset's reviewed team ID, and secret metadata + includes `EDGEZERO_BUILD_CONTAINER_APP_PRIVATE_KEY` without exposing its value. Their `updated_at` + values are no later than the successful credential-smoke completion time. Repository variable + `EDGEZERO_BUILD_CONTAINER_GATE_SHA` equals `G`, the ruleset descriptor SHA, and the successful + credential-smoke workflow SHA. Repository release state equals `enabled`; publisher App id equals + the environment App id and pin-branch bypass actor; and publisher bot id/login identify the verified + App bot account used for pin PRs; +- an App JWT made from that key identifies the expected dedicated App; the expected installation is + active on account `stackpop`, uses selected repositories, grants exactly `contents:write`, + `pull_requests:write`, and implicit `metadata:read`, and its repository list is exactly + `stackpop/edgezero`; +- an installation token can be minted for only the EdgeZero repository ID with explicit + `contents:write` and `pull_requests:write`, its response reports only those requested permissions + plus implicit metadata read, it can read `stackpop/edgezero`, and it is revoked before the helper + exits; and +- the candidate's `build-container-release-preflight` check run completed successfully, came from the + same protected workflow and GitHub Actions App integration, and belongs to a `workflow_dispatch` run + whose run record has exact path, `head_sha=G`, and exact candidate-bound display title; whose + separately supplied input PR, head repository, and head SHA are independently resolved through the + PR API by the trusted step and equal the current candidate values; and whose sole + `assert-exact-g-dispatch-context` step succeeded. It records the expected installation ID without + exposing a token. After queue merge, the repository-local workflow's latest-attempt `push` run for + exact `S` has the required event/path/head fields, both stable jobs succeeded for `head_sha=S`, and + each has exactly one successful `assert-exact-main-push-context` step before the release tag is created; + and +- repository/package identity and the absent-before-first-push or public-and-repository-linked package + state are the exact release state expected by the invocation. Absence is established only by a + successful, fully paginated organization-container-package listing made with the verified active + organization owner's package-audit token and containing no exact name match; a listing made by any + other identity, a GET 404, or an authorization failure is never absence. + +The package-audit token has its own GET-only wrapper limited to `/user`, +`/orgs/stackpop/memberships/{package-login}`, the fully paginated +`/orgs/stackpop/packages?package_type=container&per_page=100&page={page}` listing, and +`/orgs/stackpop/packages/container/edgezero-build-app-cli`. The App JWT wrapper permits only +`GET /app`, `GET /app/installations/{expected-installation-id}`, and +`POST /app/installations/{expected-installation-id}/access_tokens` with the exact repository id and +requested permission body. The resulting installation-token wrapper permits only +`GET /installation/repositories?per_page=100&page={page}`, `GET /repos/stackpop/edgezero`, and +`DELETE /installation/token`. The POST and DELETE create and revoke only the bounded test token; no credential +may call a persistent repository, organization, package, pull-request, comment, or ruleset mutation +endpoint. + +API failure, pagination truncation, ambiguity, extra bypass actor, extra repository or write +permission, credential failure, or evidence-post failure blocks release designation or publication. +After the first push creates the +package, publication stops until an operator makes it public and confirms it is linked to +`stackpop/edgezero` through a fresh package-present preflight from `G`. GHCR exposes no enforceable per-version retention lock, so this contract does not +claim one. Repository workflows contain no package-deletion endpoint or delete-scoped credential; +manual deletion by a package or organization administrator is an accepted operational risk that can +break existing digest-pinned consumers and requires an emergency rebuild plus new reviewed pin. The +workflow also verifies `S` is an ancestor of the protected default branch. All publication, +pin-record mutation, and gate rotation uses the exact repository-global concurrency group +`edgezero-build-container-publication` with `cancel-in-progress: false` and `queue: max`; therefore at +most one run executes the mutation path and up to 100 wait. A run rejected because that queue is full +publishes no pin and must be rerun after capacity is available. Pin branches remain exactly source- +derived as `edgezero-build-container-pin/` and updates are idempotent under explicit force-with- +lease. + +GitHub accepts `queue: max` and the four `job.workflow_*` reusable-workflow identity properties, but +pinned actionlint 1.7.12 predates both additions. The gate does not pretend the raw linter accepts +them. A gate-owned compatibility wrapper first uses pinned mikefarah yq 4.53.3 to require `queue` only +at workflow-level in exactly the publisher and rotation-lock workflows, with scalar `max`, exact +shared group, and literal `cancel-in-progress:false`. It also permits only +`job.workflow_repository`, `job.workflow_file_path`, `job.workflow_ref`, and `job.workflow_sha`, only +in the exact checked expressions/checkout ref locations of `.github/workflows/build-app-cli.yml`; a +misspelling, extra property, other workflow/location, alias, duplicate, or dynamic expression fails. + +After structural validation, the wrapper creates line-count-preserving temporary copies: it replaces +only those exact approved `job.workflow_*` scalar expressions with same-type constants and replaces +only the two approved `queue` lines with blank lines. It runs unfiltered actionlint 1.7.12 over those +copies and remaps any diagnostic to the real path/line; no `-ignore` rule or diagnostic filter is used. +Self-tests require raw 1.7.12 to emit exactly the reviewed unsupported-diagnostic set for canonical +queue and job-context fixtures, require the sanitized copies to pass, and require every malformed or +additional use to fail before sanitization. Remove each compatibility rewrite independently once a +reviewed actionlint release natively supports that syntax. + +After acquiring that group, every publisher requires +`EDGEZERO_BUILD_CONTAINER_RELEASE_STATE==enabled`, verifies the active gate variable and organization +descriptor agree through the already reviewed prerequisite evidence, and proves no rotation lock is +active before image build or registry authentication. The publisher has two jobs. `build-and-verify` +does not reference the protected environment; it checks out without persisted credentials, proves +`HEAD == S` and the recursive checkout is clean immediately +before the gate-context build, pushes and anonymously verifies `D`, and exports only non-secret +`{S,D,protocol,tag,approval-challenge}` job outputs. After anonymous verification it obtains 32 bytes +from the runner OS CSPRNG and renders `approval-challenge` as 64 lowercase hexadecimal characters. It +writes the exact challenge, `S`, `D`, tag, run id, and run attempt to the job summary so the approver +can inspect them; the challenge is public but unpredictable before this attempt reaches that point. It +builds only the freshly staged `G` context, which contains the reviewed `.dockerignore`. Only after +that job succeeds does `update-pin` start with +`environment: build-container-release`. That job does no image build, receives the non-secret outputs, +checks out without persisted credentials, mints the scoped App token, and performs only the pin branch +and PR mutation. Thus the environment's private key is unavailable to the gate-context build job. +Each publisher job has exactly one fixed `assert-exact-publisher-context` step, executed from the +active `G` checkout before sensitive work. It verifies tag event/ref, `github.sha==github.workflow_sha` +and equals release source `S`, exact run id/attempt/tag, active gate, and enabled release state; a +missing, duplicate, failed, candidate-resolved, or differently named assertion is fatal. +The publisher workflow itself is a gate-owned file unchanged from `G`; it checks out trusted helper +code at repository variable `EDGEZERO_BUILD_CONTAINER_GATE_SHA` into a separate root. Before image +build, the helper validates `S`'s isolated release request, verifies every gate/context path at `S` is +byte-identical to `G`, and copies only canonical manifested paths from the clean `G` root into a fresh +context outside both checkouts. The Dockerfile comes from `G`; candidate Dockerfiles, tools, +validators, context files, and post-install replacement steps are unreachable. The workflow executes +the approval gate and pin updater only from the same `G` root. The protected gate's structural +publisher checker rejects any candidate that changes this topology, permissions, ordering, action +pin, checkout source, context-construction source, or helper invocation. + +Pin branches and PRs use a short-lived, protected-environment GitHub App installation token requested +for repository `edgezero` with explicit `contents:write` and `pull_requests:write`. The publisher +requires the token action's installation-ID output to equal +`EDGEZERO_BUILD_CONTAINER_APP_INSTALLATION_ID` before use. They do not use `GITHUB_TOKEN`: its push +does not create a new workflow run, so it cannot guarantee the automatic protected pin-check path. The branch updater records the remote OID and +uses an explicit force-with-lease; ambiguous, closed, superseded, and already-merged PR states follow +the fixture-tested fail-closed state machine in the implementation plan. It writes both pin records +through gate-owned typed encoders, uses the exact protected branch/title, and records its own run and +current approval values; it cannot accept a caller-supplied evidence file. + +Let `I` be `image-source-revision` in the protected default branch's current `image.json`; absence is +the first-pin state. Normal publication may propose `S` only when `I` is absent, `I==S`, or `I` is an +ancestor of `S`. An older or incomparable `S` fails before mutation. The publisher fully paginates all +open pin PRs whose author id/login equals the authenticated dedicated App and whose head repository is +exactly `stackpop/edgezero`, and fails if a matching pin branch or title is owned by another actor or +repository. It compares every proposed source with `S`: it closes older-source PRs when +superseding them; updates one exact `{S,D}` PR idempotently; closes and replaces one same-`S`, different-`D` +PR; treats a run older than an existing proposal as superseded success without mutation; and fails on +incomparable, malformed, multiple-same-source, or otherwise ambiguous state. Required pin +CI recomputes the `I`-to-`S` ancestry relation against the PR's current merge-queue base, so a stale +older PR cannot merge after a newer pin. Operational rollback never regresses default-branch +`image.json`; consumers select an earlier reviewed exact action version containing its corresponding +pin. + +The administrator-bypass screenshot is repeated at the protected-secret boundary. For every workflow +run attempt in which `update-pin` is eligible, including every same-tag rerun, its environment approver +waits for `build-and-verify` to succeed, opens the environment settings, and captures a fresh PNG that +visibly includes the repository, environment, disabled administrator-bypass control, and sole final +tag policy. The +approver computes its digest and enters exactly one line as the environment review comment before +approving the job: + +```text +edgezero-release-evidence-v1 {"challenge":"<64-lowercase-hex>","image-digest":"","png-sha256":"sha256:<64-lowercase-hex>","release-tag":"","reviewed-at":"","run-attempt":"","run-id":"","source-revision":""} +``` + +The JSON is compact, uses the shown key order and string types with no extra key or whitespace, and every +placeholder obeys its already defined syntax. `RFC3339-UTC` here is exactly a valid calendar instant +in `YYYY-MM-DDTHH:MM:SSZ` form, with no fractional seconds or offset spelling. The exact `reviewed-at` instant is neither future nor +more than 15 minutes before the machine check. `update-pin` initially has only `actions:read` and +`contents:read`. After checking out exact gate SHA `G` without persisted credentials and before +App-token minting, its trusted gate helper uses the current `GITHUB_TOKEN` only for exact no-redirect +`GET /repos/stackpop/edgezero/actions/runs/{github.run_id}` and +`GET /repos/stackpop/edgezero/actions/runs/{github.run_id}/approvals`, with the fixed REST headers +defined above. The approval endpoint is +non-paginated; the helper requires one complete, valid HTTP 200 JSON array. It requires the API run id and +`run_attempt` to equal `github.run_id` and `github.run_attempt`; exactly one approved review for +`build-container-release` must have the exact current challenge, `D`, and remaining fields above, and +the API reviewer's login becomes the recorded approver. Every protocol-prefixed review claiming the +current run id and attempt is parsed: there must be exactly one, it must be approved and exact, and no +second current-attempt protocol record may exist. A rejected or mismatched current-attempt record, +missing or malformed history, environment bypass without the approval, stale or future time, or a +different challenge fails before any mutation credential exists. Records for earlier attempts remain +historical data but can never satisfy the current attempt; a prior reviewer cannot predeclare a useful +future-attempt approval because that attempt's CSPRNG challenge does not yet exist. + +Only after that gate passes may `actions/create-github-app-token@v3.2.0` mint the App token. The release operator then +attaches a canonical record containing the API reviewer login and exact comment plus the byte-identical +PNG to the release evidence. The comment cryptographically binds the reviewer attestation to the PNG +bytes and run attempt; the screenshot's visible meaning remains a required human review, not a claim +of machine image interpretation. The initial private-package stop and every canceled or rerun attempt +require a new capture and approval. This check is required because the API-invisible setting cannot be +proven current by the pre-`S` helper. + +The repository's zizmor policy uses `ref-pin` for every non-local action. The structural pin scanner +is the stronger authority: it accepts only canonical exact stable `v..` refs, +strict Docker `sha256` digests, and the exact scanned workflow/action/documentation surfaces. It +rejects commit SHAs as well as floating major/minor tags and branches. + +Gate `G` includes the permanent dual-state documentation scanner from the start. It parses fenced +YAML in every tracked Markdown file. Its trusted workflow selects the comparison range from this +closed event table; every SHA is a full lowercase 40-hex object present in the full subject checkout, +and any other event or malformed/inconsistent field fails closed: + +- **`pull_request`:** base is `github.event.pull_request.base.sha` and candidate is `github.sha`. + Require `github.event.pull_request.base.repo.full_name==stackpop/edgezero`, + `github.event.pull_request.base.ref==main`, + `github.ref==refs/pull//merge`, candidate first parent equal to the base SHA, and candidate + second parent equal to `github.event.pull_request.head.sha`; +- **`merge_group` `checks_requested`:** base is `github.event.merge_group.base_sha` and candidate is + `github.event.merge_group.head_sha`. Require base ref `refs/heads/main`, head ref equal to + `github.ref`, head ref beneath exact prefix `refs/heads/gh-readonly-queue/main/`, candidate equal to + `github.sha`, and base an ancestor of candidate; +- **protected-main `push`:** base is `github.event.before` and candidate is `github.event.after`. + Require `github.ref==refs/heads/main`, nonzero base, candidate equal to both `github.sha` and + `github.workflow_sha`, and base an ancestor of candidate. + +`workflow_dispatch` is a separate credential-smoke mode and invokes neither the change classifier nor +the documentation scanner. The “other event” rejection above applies whenever either range consumer +is invoked. + +The scanner compares the selected base and candidate states as follows: + +- **bootstrap:** `docs/.edgezero-action-release.json` is absent from both base and candidate. Only the + four named prepublication adoption documents may use literal `` for an + EdgeZero ref; every other external ref still obeys the exact stable-version rule; +- **transition:** the record is absent from base and added by the candidate. The candidate changes + only tracked Markdown plus that record, every EdgeZero ref equals its literal `V`, no placeholder + remains, and a fixed no-redirect versioned API/ref verifier proves `V` is a published + `draft:false`, `prerelease:false`, `immutable:true` release whose peeled tag equals recorded `P`; +- **released:** the record exists on the base and cannot be deleted. It is either byte-identical or a + documentation-only candidate atomically replaces it and all EdgeZero documentation refs with a + strictly greater canonical stable version whose immutable release/ref binding to its new `P` passes + the same hosted proof. Every candidate remains placeholder-free and every EdgeZero ref in each + fenced workflow equals the candidate record's `V`; downgrade, partial update, or non-document + change fails closed. + +The first transition therefore happens only in `R`, after `V` exists. Candidate `H` never puts an +unpublished or retired version into protected-main documentation, no later PR can return to +bootstrap mode, and later action releases repeat the same post-release atomic record/documentation +update. During the queue, “candidate” means the synthetic pull-request or merge-group commit selected +above; `R` names only the resulting protected-main commit after the push range passes. + +## 9. Testing + +Required automated coverage includes: + +- cold, warm, uncached, corrupt-restore, stop-failure, write-error, audit-failure, and save-warning + cache paths, plus separate complete lookup-eligibility and protected-event save-authorization truth + tables for both cache families; +- fixed host path restoration, cross-host-checkout-path hits, nested workspace and sibling path deps, + public Git dependencies, concurrent generations, seven-day expiry as documented behavior, exactly + one Cargo compile/build invocation after metadata preflight, no action-level retry, and pinned + sccache response-loss fallback behavior; +- cache audit type/owner/path/layout/logical-byte/non-sparse/path-length/entry-count checks and + arbitrary app-written regular data disclosure; +- full source inventory, deleted/modified tracked paths, gitlinks, escaping symlinks, overlapping or + tracked-containing output roots, nested-project implicit Fastly `bin` and `pkg` roots, absent-root + precreation, special-file/hardlink rejection, descriptor-relative cleanup, caller-declared generated + output, undeclared output rejection, source-free lifecycle bypass of Copy B checks, and unchanged + original checkout; +- every environment and mount profile, including token absence, production healthcheck tokenlessness, + staging token presence, credential-free `app-build`, every exact `app-env` name/value/count/size + boundary, empty Cargo-config policy, config-push repo/config confinement, and deploy-without-sccache; +- strict caller identity, full-SHA app refs, exact-version workflow/action refs, resolved workflow + SHA, immutable EdgeZero release enforcement, locally derived platform identity, matrix artifacts, + and consumer recomputation for private repositories; +- exact canonical metadata and expected JSON, typed `write-expected`, schema versions, duplicate keys, + byte-exact ustar + headers/padding/end blocks, deterministic package output, every accepted/rejected dynamic string and + object-acquisition tag, conservative ELF/loadability vectors, all provenance golden/malformed + fixtures, exact ELF header sizes/versions and extended-numbering rejection, dynamic-token rejection, + controlled direct-loader invocation, absent system preload, inhibited cache, flat runtime-library + closure, hardware-capability/default-path non-substitution, explicit `dlopen` non-claim, + every consumer independently writing fresh expected identity, provider actions independently + validating named artifacts and rechecking the binary + handoff, and the split parse/extract versus binary-smoke boundary; +- exact Rust/Fastly/sccache versions, installed wasm target plus a minimal wasm compile, image labels, + leaf-manifest platform checks, anonymous pulls, always-materialized required container jobs, + protected gate/workflow identity, gate-owned staged build context and post-install replacement + resistance, release-request isolation, required-workflow descriptor and bypass checks, exact + merge-queue payload and single-entry behavior, API-visible exact-`S` push assertion-step evidence, + image-pin deletion and source-ancestry ordering, environment + reviewer/self-review/deployment-policy checks, per-attempt approval comment and token-ordering checks, + policy-API method/path/header/version allowlisting, App installation/repository/permission/token- + scope checks, actionlint queue-compatibility isolation, publication concurrency and queue-overflow + cancellation, gate-rotation failure recovery, and release rerun/idempotency; +- production/staging deploy, active-version, healthcheck, rollback, config push, mutation signaling, + cancellation, and the exclusive `--staging` spelling. + +Cold evidence starts from an empty audited cache root and, after zeroing statistics, requires +`cache_misses.counts["Rust"]>=1`, `cache_writes>=1`, and zero write errors. Warm evidence runs in a +new job with fresh target/Cargo-home directories, restores the recorded cold generation through the +sole family prefix, zeros statistics, and requires `cache_hits.counts["Rust"]>=1`; the rebuilt binary +digest must equal the cold binary digest. Default-off evidence proves no cache action or sccache +process ran. Dependency fetching remains online because source archives are not cached. Wall-clock +improvement is telemetry, not a pass/fail condition. + +## 10. Rollout and migration + +To publish final action revision `P`, exact version `V`, and its adoption documentation: + +1. Migrate every existing non-local external action and reusable workflow reference in the repository + to a reviewed exact stable patch-version tag, change the repository-wide pin gate accordingly, and + retain zizmor `ref-pin` as defense in depth. Record the accepted third-party tag-movement risk. +2. Implement and separately land the validator, schema, fixtures, protected classifier/verifier + helpers, publisher contract checker, and required workflow as gate baseline `G`. Configure the + organization required-workflow rule directly to gate commit `G` (this is not a consumer `uses:` + ref) and the mandatory default-branch merge queue. +3. Include the exact Dockerfile and complete image-context closure in `G`. Open an isolated canonical + `release-request.json` candidate, run it through the protected gate, complete the credential smoke, + merge it only through the merge queue as `S`, and require the API-visible exact post-merge `S` push + assertion-step evidence before tagging. Build only from the freshly staged `G` context. +4. Publish and anonymously verify the image, then merge the ancestry-checked pin PR as baseline `B`. +5. Select unused canonical patch version `C`. Land the reusable workflow, cache, provenance, + launcher, and consumer integration while leaving prepublication adoption examples at the gated + placeholder. Record resulting main commit `H`. +6. Rerun the complete local suite from detached `H`; have a verified active releaser-team member use + a local credential to draft and publish immutable `C` at `H` with `prerelease:true`; and run the + complete hosted cross-repository/provider suite through literal `C`. On success designate `H=P`, + select unused stable `V`, use the same auditable actor procedure to draft and publish it at `P`, + verify both release/ref resolutions and attestations, and run the final literal-`V` hosted smoke. + A candidate failure follows the new-commit/new-`C` rules in Section 8. +7. After the literal-`V` smoke passes, merge documentation-only `R` through the protected queue. Add + the `{V,P}` action-release record and replace every gated placeholder with literal `V`; the + preinstalled dual-state gate proves the release/ref binding and permanently enters released mode. + Run the docs build and exact-version scan on protected main and record `R`. + +Caching remains off by default. Container execution and provenance validation are unconditional. + +## 11. Out of scope + +- Detecting sccache staleness from undeclared proc-macro or `build.rs` inputs. +- Authenticating the artifact producer or proving workflow-bound attestation. +- Caching dependency source archives, private dependency credentials, native-tool sccache wrapping, + self-hosted runners, alternate toolchains, non-default feature sets, or non-Fastly adapters. +- Cache lineage merging, family-local eviction, or action-managed cache deletion. +- Preventing a GHCR package administrator from manually deleting a supported image digest; GitHub does + not expose a per-version retention lock for this contract. + +## 12. History + +- **v6.17:** introduced the build-only reusable workflow, consumer deployment job, deploy-compile + profile, full working-copy verification, `job.check_run_id`, and explicit undeclared-input risk. +- **v6.18:** split trusted provenance extraction from untrusted binary execution; completed provider + mount/environment profiles; strengthened full-inventory source verification; made cache family and + warning-only saves coherent; corrected sccache error/size/audit contracts; made platform identity + action-derived; replaced impossible same-SHA publication with image source `S`, pin baseline `B`, + and final action revision `P`; made full-SHA external references normative; and made validator + capability fixtures a hard publication prerequisite. +- **v6.19:** froze the protocol-1 metadata and expected-identity JSON shapes, schema/version bounds, + byte-exact ustar encoding, conservative ELF/loader profile including exact dynamic-string and + object-acquisition semantics, and shared package/validate authority; selected the exact base and + sccache artifacts; moved release prerequisites before `S` with verifiable environment and + least-privilege App controls, including explicit manual evidence for the API-invisible administrator + bypass setting and an organization-owner package-audit identity; aligned zizmor with full-SHA policy; + removed unenforceable GHCR retention claims; and replaced path-filtered required image jobs with an + always-triggered workflow whose stable jobs explicitly succeed when not applicable. +- **v6.20:** made cache writes an action-derived protected-event decision; added the typed canonical + expected-identity producer; froze suffix and ELF edge cases; replaced candidate-controlled image + checks with an immutable-SHA organization required workflow and mandatory merge queue; required an + exact post-merge source check; split tag creation authority from no-bypass immutability; specified the policy-audit credential and request allowlist; bound + each protected-secret approval to a per-attempt CSPRNG challenge, image and screenshot digest before token minting; defined + forward-only pin ancestry and bounded publication queue semantics; and assigned current Fastly + `pkg` output to the source-freeze contract. +- **v6.21:** replaced archiver-dependent cache sizing with exact filesystem-tree bounds; froze the + app-environment, Cargo-config, generated-output, nested Fastly `bin`/`pkg`, cleanup, and controlled + loader contracts; moved every repository image-context input into gate `G` and made `S` an isolated release + request built from a staged trusted context; defined gate rotation and recovery, exact one-entry + merge-queue policy, API-visible dispatch/push assertion evidence, REST API headers/version, and the + narrowly scoped actionlint 1.7.12 `queue: max` compatibility check. +- **v6.22:** separated the self-referential action release from its documentation by defining + documentation-only revision `R`, whose concrete examples pin already-known full SHA `P`; froze the + reusable-workflow input, self-checkout, artifact transport, network/resource, and private binary- + state contracts; froze the organization required-workflow ruleset's repository/ref target; and made + the protected release environment's machine-verified absence of GitHub App custom deployment- + protection rules an explicit prerequisite for the non-deployment credential-smoke job. +- **v6.23:** replaced full-SHA action/workflow references with exact stable patch-version tags and an + immutable EdgeZero release process; removed the documentation-only self-reference workaround; + defined authority-checkout export and pinned-LFS rules, uncached compilation, pre-restore + disclosure, parent-cache authorization, closed sccache JSON statistics, consumer expected-file + production, candidate-bound credential-smoke evidence, standalone validator workspace/build + closure, exact release-request production, self-test isolation, and generated-output sparse/size + limits. +- **v6.24:** made exact version tags unambiguous for every non-local `uses:` reference while retaining + commit SHAs only for source/provenance identity; removed the last cache-action SHA-pinning + contradiction; assigned distinct notation to the current image source pin; froze the complete + sccache statistics schema and warm-evidence counters; and specified the exact checksummed host Git + LFS installation, authenticated materialization, and credential-removal sequence. +- **v6.25:** made release-candidate `uses:` refs exact patch versions without prerelease suffixes; + removed circular `P` qualification; matched the immutable-release settings response; distinguished + immutable tag identity from accepted privileged release-object deletion risk; and replaced + unprovable historical release-name absence with current ref/release absence plus creation success; + reserved `H` for the final action candidate and renamed the generic protected head `Q`; made every + normative JCS example a literal single line; closed Rust/native compiler overrides; moved filter/ + submodule validation before worktree creation; fixed Git LFS asset size/transfer bounds; defined + local bootstrap image `L`; assigned config-push authority checks, permanent documentation pin + enforcement, and the auditable action-release actor/procedure. +- **v6.26:** moved literal stable-version documentation into post-release revision `R` under a + one-way dual-state gate so failed candidates cannot strand unpublished refs on main; selected `V` + only after `H` qualifies as `P`; froze the local fine-grained release PAT permissions, API + allowlist, actor proof, and credential handling; and defined the public consumer identity action + plus per-invocation source materialization so no authority path crosses a public action boundary; + and closed the documentation scanner's pull-request, merge-group, and protected-push ranges. + +## 13. Deferred implementation mechanics + +Implementation plans may choose helper names and internal module boundaries. They must commit the +schema implementing Section 6.2, golden bytes and malformed fixtures for Sections 6.2 through 6.5, +sccache v0.10 layout/stats fixtures, exact cache tree-bound and entry-count vectors, release +versions/checksums, and command-level tests before exact action version `V` at final action revision +`P` is published. +Those are mechanics, not permission to weaken the contracts above.