build: the Doorpost read before the image is built
Tablet 54 (2026-10-15): two spec edits in @spec_files shipped without moving @expected_hash; the pod logged DOORPOST_DRIFT_AT_STARTUP, readiness answered 503, and the gate — with every Claude Code session behind it — went dark. The Doorpost test catches this only when the suite runs on the exact commit that gets built, and the build script never ran it. scripts/doorpost_check.py computes the Doorpost identity the way the module does (sha256 per spec file in listed order, "path\thash" joined by newline, sha256 of that) with no Elixir, against the working tree the image is copied from (default) or a git revision (--rev). On a mismatch it exits 1 and names each file changed since --base (origin/main). scripts/build_and_push.sh runs it before anything else and refuses to build; DOORPOST_SKIP=1 is the written-down exception. Proven: working tree andcb18f719match their pins;347f83cf(the tablet 54 commit) reports DRIFT naming specs/bikoret_tamid.md and specs/bishearecha.md; a blank line appended to specs/lo_tosif.md makes both the check and the build script exit 1. KOHEN.md "Rolling out" carries the procedure. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
07463c9c37
commit
04f61c1238
3 files changed
+136
No files matched your search
@@ -566,6 +566,26 @@ is (Day 4). `scripts/upstream-key-set` stores a caller's model key for
|
|||||||
the gate scenario without echoing it (`~/.config/bikoret/upstream_keys`,
|
the gate scenario without echoing it (`~/.config/bikoret/upstream_keys`,
|
||||||
0600) — run it from a real terminal, not through a non-interactive runner.
|
0600) — run it from a real terminal, not through a non-interactive runner.
|
||||||
|
|
||||||
|
**Before the image is built — the Doorpost read on the workbench.**
|
||||||
|
`VavPipeline.Doorpost` bakes a hash over the category-grade spec files
|
||||||
|
(`@spec_files` in `vav_pipeline/lib/vav_pipeline/doorpost.ex`); a pod
|
||||||
|
whose files do not match logs `DOORPOST_DRIFT_AT_STARTUP`, answers 503
|
||||||
|
on readiness, and takes the gate dark (tablet 54: two spec edits shipped
|
||||||
|
without the pin). `scripts/build_and_push.sh` now runs
|
||||||
|
`scripts/doorpost_check.py` first and refuses to build on drift, naming
|
||||||
|
the files that changed since `origin/main`. Hand builds should do the
|
||||||
|
same:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -I scripts/doorpost_check.py # the working tree, as the image is built from
|
||||||
|
python3 -I scripts/doorpost_check.py --rev HEAD # a commit
|
||||||
|
python3 -I scripts/doorpost_check.py --print # the hash the pin should be
|
||||||
|
```
|
||||||
|
|
||||||
|
A spec change and its pin move belong in ONE commit, with
|
||||||
|
`mix test test/doorpost_test.exs` run on it. `DOORPOST_SKIP=1` exists
|
||||||
|
for a deliberate, written-down exception; it is never the way to ship.
|
||||||
|
|
||||||
### Standing at the gate — *Bishe'arecha*
|
### Standing at the gate — *Bishe'arecha*
|
||||||
|
|
||||||
A caller can put Bikoret between themselves and a model instead of
|
A caller can put Bikoret between themselves and a model instead of
|
||||||
|
|||||||
@@ -53,6 +53,16 @@ if [[ ! -d "deploy/docker" || ! -d "l1/torah" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Sanity: docker buildx
|
# Sanity: docker buildx
|
||||||
|
# The Doorpost, read before the build: the spec files the image will carry
|
||||||
|
# must hash to the pin baked into VavPipeline.Doorpost, or the pod drifts
|
||||||
|
# at startup, readiness answers 503, and the gate goes dark (tablet 54,
|
||||||
|
# 2026-10-15). scripts/doorpost_check.py computes the same hash the same
|
||||||
|
# way and names the drifted files. DOORPOST_SKIP=1 only for a deliberate,
|
||||||
|
# documented exception — never to ship a drift.
|
||||||
|
if [[ "${DOORPOST_SKIP:-0}" != "1" ]]; then
|
||||||
|
python3 -I scripts/doorpost_check.py || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if ! docker buildx version >/dev/null 2>&1; then
|
if ! docker buildx version >/dev/null 2>&1; then
|
||||||
echo "ERROR: docker buildx not available — install Docker Desktop or buildx plugin" >&2
|
echo "ERROR: docker buildx not available — install Docker Desktop or buildx plugin" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
Executable
+106
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""doorpost_check.py — the Doorpost read BEFORE the image is built.
|
||||||
|
|
||||||
|
VavPipeline.Doorpost bakes @expected_hash over the category-grade spec
|
||||||
|
files (@spec_files in vav_pipeline/lib/vav_pipeline/doorpost.ex). An
|
||||||
|
image whose spec files do not hash to its pin logs
|
||||||
|
DOORPOST_DRIFT_AT_STARTUP, answers 503 on readiness, and takes the gate
|
||||||
|
dark with it — tablet 54, 2026-10-15: two spec edits shipped without a
|
||||||
|
pin move and every Claude Code session behind the gate was cut off.
|
||||||
|
|
||||||
|
This script computes the same hash the same way, with no Elixir:
|
||||||
|
for each path in @spec_files, in listed order: sha256(contents)
|
||||||
|
join "path\\thash" with "\\n"; sha256 of that.
|
||||||
|
and compares it with the pin. Exit 0 on a match, 1 on drift (the
|
||||||
|
message names each drifted file against a base revision), 2 on a
|
||||||
|
missing file or an unreadable module.
|
||||||
|
|
||||||
|
scripts/doorpost_check.py # the working tree, as the image is built from
|
||||||
|
scripts/doorpost_check.py --rev <rev> # a git revision instead of the working tree
|
||||||
|
scripts/doorpost_check.py --base <rev> # on drift, diff the manifest against <rev> (default origin/main)
|
||||||
|
scripts/doorpost_check.py --print # print the hash only (what the pin should be)
|
||||||
|
|
||||||
|
Devarim 4:2 — nothing added, nothing taken: the pin moves by a commit
|
||||||
|
that moves it, never by a build that ignores it.
|
||||||
|
"""
|
||||||
|
import argparse, hashlib, os, re, subprocess, sys
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
MODULE = "vav_pipeline/lib/vav_pipeline/doorpost.ex"
|
||||||
|
|
||||||
|
|
||||||
|
def read(rev, path):
|
||||||
|
"""Bytes of `path` at `rev`, or from the working tree when rev is None. None if missing."""
|
||||||
|
if rev is None:
|
||||||
|
full = os.path.join(ROOT, path)
|
||||||
|
try:
|
||||||
|
with open(full, "rb") as f:
|
||||||
|
return f.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
|
r = subprocess.run(["git", "-C", ROOT, "show", f"{rev}:{path}"], capture_output=True)
|
||||||
|
return r.stdout if r.returncode == 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def spec_files_and_pin(rev):
|
||||||
|
src = read(rev, MODULE)
|
||||||
|
if src is None:
|
||||||
|
sys.exit(f"doorpost_check: cannot read {MODULE}" + (f" at {rev}" if rev else ""))
|
||||||
|
src = src.decode()
|
||||||
|
block = src.split("@spec_files [", 1)[1].split("]", 1)[0]
|
||||||
|
files = re.findall(r'"([^"]+)"', block)
|
||||||
|
pin = re.search(r'@expected_hash\s+"([0-9a-f]{64})"', src)
|
||||||
|
if not files or not pin:
|
||||||
|
sys.exit(f"doorpost_check: could not parse @spec_files/@expected_hash from {MODULE}")
|
||||||
|
return files, pin.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
def manifest(rev, files):
|
||||||
|
out = {}
|
||||||
|
for p in files:
|
||||||
|
data = read(rev, p)
|
||||||
|
out[p] = None if data is None else hashlib.sha256(data).hexdigest()
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def identity(man):
|
||||||
|
return hashlib.sha256("\n".join(f"{p}\t{h}" for p, h in man.items()).encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--rev", help="git revision to check instead of the working tree")
|
||||||
|
ap.add_argument("--base", default="origin/main", help="revision to name drifted files against")
|
||||||
|
ap.add_argument("--print", action="store_true", help="print the computed hash only")
|
||||||
|
a = ap.parse_args()
|
||||||
|
|
||||||
|
files, pin = spec_files_and_pin(a.rev)
|
||||||
|
man = manifest(a.rev, files)
|
||||||
|
missing = [p for p, h in man.items() if h is None]
|
||||||
|
if missing:
|
||||||
|
print("doorpost_check: MISSING spec file(s): " + ", ".join(missing), file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
computed = identity(man)
|
||||||
|
if a.print:
|
||||||
|
print(computed)
|
||||||
|
return
|
||||||
|
where = a.rev or "working tree"
|
||||||
|
if computed == pin:
|
||||||
|
print(f"doorpost: {where} hashes to its pin {pin[:8]}… ({len(files)} files) — the Doorpost will verify")
|
||||||
|
return
|
||||||
|
print(f"doorpost: DRIFT in {where} — computed {computed}, pinned {pin}", file=sys.stderr)
|
||||||
|
base_files, _ = spec_files_and_pin(a.base)
|
||||||
|
base_man = manifest(a.base, base_files)
|
||||||
|
for p in files:
|
||||||
|
if base_man.get(p) != man[p]:
|
||||||
|
print(f" changed since {a.base}: {p}", file=sys.stderr)
|
||||||
|
for p in files:
|
||||||
|
if p not in base_man:
|
||||||
|
print(f" added to @spec_files since {a.base}: {p}", file=sys.stderr)
|
||||||
|
print(f" Move @expected_hash in {MODULE} to {computed} in the same commit as the spec change,", file=sys.stderr)
|
||||||
|
print(f" then run: mix test test/doorpost_test.exs. Refusing to build an image that would drift at startup.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in new issue
Block a user