Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04f61c1238 |
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