diff --git a/.gitignore b/.gitignore index bae927b..9e66b9c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ EXPERIMENTS.md # claude code harness state (session locks, local config) .claude/ data-*/ + +# admission receipt logs (the receipts themselves are tracked) +receipts/logs-*/ diff --git a/justfile b/justfile index 799f2c0..ff75b8d 100644 --- a/justfile +++ b/justfile @@ -425,6 +425,17 @@ package: zig build -Doptimize=ReleaseSafe -Dtarget=x86_64-linux-gnu @ls -lh zig-out/bin/stream +# Offline: prove the deployment gate refuses every artifact it has not seen +# pass -- unknown digest, unpublished receipt, skipped suite, failed suite. +admission-contract: + python3 tests/admission_contract.py + +# Exact-artifact admission. `run` requires a clean tree, runs every suite, +# builds the linux/amd64 image and writes one receipt; `publish` pins that +# receipt to the pushed registry digest; `verify` is the deployment gate. +admit *ARGS: + ./scripts/admit {{ARGS}} + # build + push the deploy image from a FRESH CLONE — the image tag always # corresponds 1:1 to a pushed commit (provenance by construction; zlay's # dirty-tree lesson) diff --git a/scripts/admit b/scripts/admit new file mode 100755 index 0000000..001f5a3 --- /dev/null +++ b/scripts/admit @@ -0,0 +1,240 @@ +#!/usr/bin/env bash +# Exact-artifact admission for Stream. +# +# The previous whole-network experiment deployed image e1926f3, which was not +# the artifact its receipt covered. Nothing detected that. This binds one +# receipt to one image digest and gives deployment a way to refuse anything +# else. +# +# scripts/admit build build the exact linux/amd64 image, no publish +# scripts/admit run clean tree -> suites -> image -> receipt +# scripts/admit publish push the admitted image and pin the receipt +# to the registry digest +# scripts/admit verify exit non-zero unless is admitted +# +# Design rules, learned from the failure this replaces: +# - the receipt records the artifact's own digest, never a tag; tags move +# - a skipped suite is written into the receipt as "skipped", never omitted, +# so a partial run cannot read as a full one +# - any suite failure aborts before a receipt exists +# - the tree must be clean, so the receipt's revisions describe the source +# that actually produced the binary +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")/.." +ROOT="$PWD" +RECEIPTS="${STREAM_RECEIPTS_DIR:-$ROOT/receipts}" +IMAGE_REPO="${STREAM_IMAGE_REPO:-atcr.io/zat.dev/stream}" + +die() { printf 'admit: %s\n' "$*" >&2; exit 1; } +note() { printf '\033[1m==> %s\033[0m\n' "$*"; } + +require_clean_tree() { + [ -z "$(git status --porcelain)" ] || + die "worktree is dirty; the receipt must describe the source that built the artifact +$(git status --porcelain)" +} + +# Every pinned input that can change behavior. A receipt that does not name +# these cannot be checked later. +revisions_json() { + python3 - "$ROOT" <<'PY' +import json, re, subprocess, sys, pathlib +root = pathlib.Path(sys.argv[1]) +zon = (root / "build.zig.zon").read_text() +deps = dict(re.findall(r'\.(\w[\w-]*) = \.\{\s*\.url = "([^"]+)"', zon)) +def git(*a): + return subprocess.check_output(["git", *a], cwd=root, text=True).strip() +upstream = pathlib.Path.home() / "github.com/bluesky-social/jetstream" +try: + upstream_rev = subprocess.check_output( + ["git", "rev-parse", "HEAD"], cwd=upstream, text=True).strip() + upstream_dirty = bool(subprocess.check_output( + ["git", "status", "--porcelain", "--untracked-files=no"], cwd=upstream, text=True).strip()) +except Exception: + upstream_rev, upstream_dirty = None, None +print(json.dumps({ + "stream": git("rev-parse", "HEAD"), + "stream_describe": git("describe", "--always", "--dirty"), + "zig": subprocess.check_output(["zig", "version"], text=True).strip(), + "dependencies": deps, + "upstream_jetstream": upstream_rev, + "upstream_jetstream_dirty": upstream_dirty, + "dashboard_sha256": __import__("hashlib").sha256( + (root / "deploy/grafana/jetstream-upstream.json").read_bytes()).hexdigest(), +}, indent=2, sort_keys=True)) +PY +} + +# name|command. Negative suites come first: they are the ones that decide +# whether a green positive run means anything. +SUITES=( + "unit-debug|zig build test" + "unit-releasesafe|zig build test -Doptimize=ReleaseSafe" + "differential-oracle|just differential-oracle" + "listener-contract|just listener-contract" + "shutdown-contract|just shutdown-contract" + "logging-contract|just logging-contract" + "environment-contract|just environment-contract" + "dashboard-test|just dashboard-test" + "process-metrics-contract|just process-metrics-contract" + "http-metrics-contract|just http-metrics-contract" + "archive-contract|just archive-contract" + "plan-config-contract|just plan-config-contract" + "cursor-lookback-contract|just cursor-lookback-contract" + "compaction-config-contract|just compaction-config-contract" + "retry-config-contract|just retry-config-contract" + "subscribe-config-contract|just subscribe-config-contract" + "subscribe-read-batch-contract|just subscribe-read-batch-contract" + "status-contract|just status-contract" + "powerloss-oracle|just powerloss-oracle" +) + +run_suites() { + local out="$1" log_dir="$2" first=1 + mkdir -p "$log_dir" + printf '{\n' >"$out" + for entry in "${SUITES[@]}"; do + local name="${entry%%|*}" cmd="${entry#*|}" status started elapsed + # STREAM_ADMIT_SKIP is an escape hatch for an environment that genuinely + # cannot run a suite. It is recorded, not hidden. + if [[ ",${STREAM_ADMIT_SKIP:-}," == *",$name,"* ]]; then + status="skipped" + note "SKIP $name (STREAM_ADMIT_SKIP)" + else + note "$name" + started=$SECONDS + if (cd "$ROOT" && eval "$cmd") >"$log_dir/$name.log" 2>&1; then + status="pass" + else + status="fail" + fi + elapsed=$((SECONDS - started)) + if [ "$status" = fail ]; then + tail -30 "$log_dir/$name.log" >&2 + die "$name failed after ${elapsed}s; no receipt written" + fi + printf ' %s (%ss)\n' "$status" "$elapsed" + fi + [ $first -eq 1 ] || printf ',\n' >>"$out" + first=0 + printf ' "%s": "%s"' "$name" "$status" >>"$out" + done + printf '\n}\n' >>"$out" +} + +build_image() { + local tag="$1" + note "building linux/amd64 image $tag" + docker build --platform linux/amd64 -t "$tag" "$ROOT" >&2 + # The local image ID is the artifact's identity before it is pushed. The + # registry digest replaces it at publish time. + docker image inspect --format '{{.Id}}' "$tag" +} + +cmd_build() { + require_clean_tree + local sha; sha=$(git rev-parse --short HEAD) + build_image "$IMAGE_REPO:$sha" +} + +cmd_run() { + require_clean_tree + local sha stamp receipt log_dir suites_json revisions image_id tag + sha=$(git rev-parse --short HEAD) + stamp=$(date -u +%Y%m%dT%H%M%SZ) + log_dir="$RECEIPTS/logs-$sha-$stamp" + suites_json=$(mktemp) + trap 'rm -f "$suites_json"' EXIT + + run_suites "$suites_json" "$log_dir" + + tag="$IMAGE_REPO:$sha" + image_id=$(build_image "$tag") + revisions=$(revisions_json) + + # Re-check: a suite run can take long enough for someone to touch the tree. + require_clean_tree + + mkdir -p "$RECEIPTS" + receipt="$RECEIPTS/$sha.json" + python3 - "$receipt" "$image_id" "$tag" "$stamp" "$suites_json" <&2 + digest=$(docker image inspect --format '{{index .RepoDigests 0}}' "$tag" | cut -d@ -f2) + [ -n "$digest" ] || die "could not read the pushed digest" + + python3 - "$receipt" "$digest" <<'PY' +import json, sys +receipt, digest = sys.argv[1], sys.argv[2] +data = json.load(open(receipt)) +if data["image"]["registry_digest"] not in (None, digest): + raise SystemExit(f"admit: receipt already pinned to {data['image']['registry_digest']}") +data["image"]["registry_digest"] = digest +json.dump(data, open(receipt, "w"), indent=2, sort_keys=True) +open(receipt, "a").write("\n") +PY + note "admitted $digest" +} + +# The deployment-side gate. Anything not carrying an admitted digest is +# refused, which is exactly what was missing when e1926f3 shipped. +cmd_verify() { + local digest="${1:-}" + [ -n "$digest" ] || die "usage: scripts/admit verify " + python3 - "$RECEIPTS" "$digest" <<'PY' +import json, pathlib, sys +receipts, digest = pathlib.Path(sys.argv[1]), sys.argv[2] +for path in sorted(receipts.glob("*.json")): + data = json.load(open(path)) + if data.get("image", {}).get("registry_digest") != digest: + continue + suites = data.get("suites", {}) + bad = {k: v for k, v in suites.items() if v != "pass"} + if bad: + print(f"REJECT {digest}: {path.name} has non-passing suites: {bad}") + raise SystemExit(1) + print(f"ADMITTED {digest} ({path.name}, stream {data['revisions']['stream'][:12]})") + raise SystemExit(0) +print(f"REJECT {digest}: no receipt admits this digest") +raise SystemExit(1) +PY +} + +case "${1:-}" in + build) shift; cmd_build "$@" ;; + run) shift; cmd_run "$@" ;; + publish) shift; cmd_publish "$@" ;; + verify) shift; cmd_verify "$@" ;; + *) die "usage: scripts/admit {build|run|publish|verify }" ;; +esac diff --git a/tests/admission_contract.py b/tests/admission_contract.py new file mode 100644 index 0000000..70b6f78 --- /dev/null +++ b/tests/admission_contract.py @@ -0,0 +1,88 @@ +"""Offline contract for the deployment-side admission gate. + +The previous experiment deployed an image no receipt covered, and nothing +refused it. These cases pin the refusals, not the happy path: the gate is only +worth anything if it says no to everything it has not seen pass. + + python3 tests/admission_contract.py +""" +import json +import pathlib +import subprocess +import sys +import tempfile + +ADMIT = pathlib.Path(__file__).resolve().parent.parent / "scripts" / "admit" +DIGEST = "sha256:" + "a" * 64 +OTHER = "sha256:" + "b" * 64 + + +def verify(receipts_dir, digest): + return subprocess.run( + [str(ADMIT), "verify", digest], + capture_output=True, + text=True, + env={"PATH": "/usr/bin:/bin:/usr/local/bin", "STREAM_RECEIPTS_DIR": str(receipts_dir)}, + ) + + +def write_receipt(receipts_dir, name, *, digest, suites): + (receipts_dir / name).write_text( + json.dumps( + { + "schema": "stream-admission/1", + "image": {"tag": "atcr.io/zat.dev/stream:test", "registry_digest": digest}, + "revisions": {"stream": "0" * 40}, + "suites": suites, + } + ) + ) + + +ALL_PASS = {"unit-debug": "pass", "archive-contract": "pass"} + + +def case(label, condition): + if not condition: + print(f"FAIL: {label}") + sys.exit(1) + print(f" ok: {label}") + + +with tempfile.TemporaryDirectory() as tmp: + receipts = pathlib.Path(tmp) + + # Nothing admitted yet: the gate must refuse, not default open. + r = verify(receipts, DIGEST) + case("empty receipts dir rejects", r.returncode != 0 and "REJECT" in r.stdout) + + write_receipt(receipts, "abc1234.json", digest=DIGEST, suites=ALL_PASS) + r = verify(receipts, DIGEST) + case("fully passing receipt admits its own digest", r.returncode == 0 and "ADMITTED" in r.stdout) + + # The core of the e1926f3 failure: a *different* artifact must not inherit + # another artifact's receipt. + r = verify(receipts, OTHER) + case("a different digest is rejected", r.returncode != 0 and "REJECT" in r.stdout) + + # A receipt that never reached publish has no digest to stand behind. + write_receipt(receipts, "unpublished.json", digest=None, suites=ALL_PASS) + r = verify(receipts, "null") + case("unpublished receipt admits nothing", r.returncode != 0) + + # A skipped suite is not a passed suite. Silence must not read as success. + skipped = receipts / "skipped" + skipped.mkdir() + write_receipt(skipped, "def5678.json", digest=DIGEST, + suites={**ALL_PASS, "powerloss-oracle": "skipped"}) + r = verify(skipped, DIGEST) + case("a skipped suite blocks admission", r.returncode != 0 and "non-passing" in r.stdout) + + failed = receipts / "failed" + failed.mkdir() + write_receipt(failed, "aaa1111.json", digest=DIGEST, + suites={**ALL_PASS, "differential-oracle": "fail"}) + r = verify(failed, DIGEST) + case("a failed suite blocks admission", r.returncode != 0 and "non-passing" in r.stdout) + +print("ADMISSION GATE CONTRACT PASS")