feat(scripts): one artifact-gate library for the LLM endpoints

The serve scripts and the deployer each hand-wrote the same missing/truncated
artifact gates; the 2026-10-01 crash-loop was those twins disagreeing
mid-cutover. scripts/lib/llm-gates.sh is the single place the refusal lives:
require_file / require_bytes / require_min_mib / require_dir_files /
require_image, each returning 1 and naming the endpoint, the artifact and the
remedy. New seam tests/llm_gates.bats pins the contract at the arguments-in /
status-and-stderr-out boundary.
This commit is contained in:
Frank
2026-10-13 21:38:50 -07:00
parent c52ba781e3
commit acf2a5cee5
3 changed files with 264 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# llm-gates.sh — the artifact gates every LLM endpoint runs before it starts.
#
# Why this file exists: the fleet law from the 2026-10-01 crash-loop is that
# a serve script and the deployer must refuse the SAME missing/truncated
# artifact, the SAME way. When the two gates were hand-written twins they
# drifted mid-cutover — green deploy, red unit, every time the payload moved.
# Both sides source this library, so the refusal is one text and one rule.
#
# Contract (public interface):
# require_file PATH LABEL HINT
# require_bytes PATH LABEL WANT_BYTES HINT
# require_min_mib PATH LABEL MIN_MIB HINT
# require_dir_files DIR LABEL HINT FILE...
# require_image REF HINT
#
# Each returns 0 and prints nothing on success. On failure it prints two
# lines to stderr — the first naming the endpoint ($GATE_PREFIX) and the
# artifact, the second the remedy — and returns 1. It NEVER exits: the serve
# scripts run under `set -e` (so a refusal is terminal), while the deployer
# wants `|| return 1` so a failed gate reports a red deploy for that one
# service and the dispatcher's state marker stays pending.
#
# The size law is equality, not a floor, for `require_bytes`: a hub fetch
# that truncates is the fleet's standing hazard (curl exits 0 on 401/403 and
# writes the error body as the artifact), and the declared byte count is a
# contract in both directions — a bigger file is a DIFFERENT artifact.
# GATE_PREFIX names the endpoint in every refusal (e.g. frank-dragoon).
: "${GATE_PREFIX:=gate}"
_gate_fail() {
printf '%s: %s\n' "$GATE_PREFIX" "$1" >&2
printf ' hint: %s\n' "$2" >&2
return 1
}
# require_file — PATH must be a regular file. A directory (an unpacked
# staging dir wearing the payload's name) and a symlink (a HuggingFace cache
# snapshot's entry, which dangles inside the container mount) both refuse.
require_file() {
local path="$1" label="$2" hint="$3"
if [[ -L "$path" || ! -f "$path" ]]; then
_gate_fail "$label: missing at $path" "$hint"
fi
}
# require_bytes — PATH must exist AND be exactly WANT_BYTES bytes.
require_bytes() {
local path="$1" label="$2" want="$3" hint="$4" got
require_file "$path" "$label" "$hint" || return 1
got="$(stat -c %s "$path")"
if [[ "$got" != "$want" ]]; then
_gate_fail "$label: is $got bytes, expected $want" "$hint"
fi
}
# require_min_mib — PATH must be at least MIN_MIB mebibytes. The floor form
# is for build products (a .rad container is a range of legal sizes; a
# truncated convert is not).
require_min_mib() {
local path="$1" label="$2" min="$3" hint="$4" mib
require_file "$path" "$label" "$hint" || return 1
mib=$(( $(stat -c %s "$path") / 1048576 ))
if (( mib < min )); then
_gate_fail "$label: is $mib MiB, expected >= $min MiB" "$hint"
fi
}
# require_dir_files — DIR must be a directory and every named member a
# regular file (the flat-tokenizer law: snapshot symlinks dangle in the
# container mount even when the directory itself is real).
require_dir_files() {
local dir="$1" label="$2" hint="$3"
shift 3
local f
if [[ ! -d "$dir" ]]; then
_gate_fail "$label: missing at $dir" "$hint"
fi
for f in "$@"; do
if [[ -L "$dir/$f" || ! -f "$dir/$f" ]]; then
_gate_fail "$label: $dir/$f is missing or a symlink" "$hint"
fi
done
}
# require_image — the image ref must be in podman storage. A name@sha256 ref
# exists only if THAT manifest was pulled, so the gate is the ref itself, in
# one command: no pipefail/grep race (the 2026-09-30 lesson).
require_image() {
local ref="$1" hint="$2"
if ! podman image exists "$ref"; then
_gate_fail "image: $ref is not in podman storage" "$hint"
fi
}