From 028afdeb589a7fbea175be57177fa2b257c32de0 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 13 Oct 2026 21:40:02 -0700 Subject: [PATCH] refact(frank-dragoon): serve gates run on the shared gate library The hand-written image/ck/tokenizer gates are require_image, require_bytes and require_dir_files from scripts/lib/llm-gates.sh. The serve seam is unchanged and still green (its podman stub now models `podman image exists`, the primitive the gate uses). Behaviour preserved: every refusal names the artifact, the endpoint and the fetch script. --- scripts/lib/llm-gates.sh | 6 ++- scripts/serve-frank-dragoon.sh | 70 +++++++++++++++------------------- tests/frank_dragoon_unit.bats | 7 ++++ tests/llm_gates.bats | 9 +++++ 4 files changed, 50 insertions(+), 42 deletions(-) diff --git a/scripts/lib/llm-gates.sh b/scripts/lib/llm-gates.sh index d6224ab..5b0421f 100755 --- a/scripts/lib/llm-gates.sh +++ b/scripts/lib/llm-gates.sh @@ -69,12 +69,14 @@ require_min_mib() { # 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). +# container mount even when the directory itself is real. A symlinked +# DIRECTORY is refused too: a HF cache snapshot can link the directory +# itself, and a link dangles inside the container mount. require_dir_files() { local dir="$1" label="$2" hint="$3" shift 3 local f - if [[ ! -d "$dir" ]]; then + if [[ -L "$dir" || ! -d "$dir" ]]; then _gate_fail "$label: missing at $dir" "$hint" fi for f in "$@"; do diff --git a/scripts/serve-frank-dragoon.sh b/scripts/serve-frank-dragoon.sh index 135fe38..332e8e2 100755 --- a/scripts/serve-frank-dragoon.sh +++ b/scripts/serve-frank-dragoon.sh @@ -29,7 +29,14 @@ # exactly like frank-archon's. set -euo pipefail +# The artifact gates are the shared library — the serve script and the +# deployer refuse the same missing/truncated payload with the same words +# (the 2026-10-01 crash-loop was the two hand-written twins disagreeing). SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +export GATE_PREFIX=frank-dragoon +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/lib/llm-gates.sh" + ENV_FILE="${FRANK_DRAGOON_ENV_FILE:-$SCRIPT_DIR/frank-dragoon.env}" if [ ! -f "$ENV_FILE" ]; then echo "frank-dragoon: env file not found: $ENV_FILE" >&2 @@ -57,55 +64,37 @@ set +a : "${HALOGEN_KV_SLOTS:?HALOGEN_KV_SLOTS must be set}" : "${HALOGEN_SPEC_ADAPT:?HALOGEN_SPEC_ADAPT must be set (0 = head always on)}" -# Capture podman's output BEFORE grepping: under `set -o pipefail` the -# `podman images | grep -q` pattern races (grep exits on first match, -# podman dies SIGPIPE 141, pipefail turns the 141 into "no match" — the -# 2026-09-30 deploy refusal). -DIGESTS="$(podman images --format '{{.RepoDigests}}')" -if ! grep -qF "${FRANK_DRAGOON_IMAGE#*@}" <<<"$DIGESTS"; then - echo "frank-dragoon: image $FRANK_DRAGOON_IMAGE not in podman storage;" >&2 - echo " pull it: podman pull ${FRANK_DRAGOON_IMAGE%@*}" >&2 - exit 1 -fi +# The image gate is the digest-pinned ref itself: `podman image exists` takes +# a name@sha256 ref in one command, so a retagged look-alike or a newer +# latest cannot satisfy it and there is no pipefail/SIGPIPE race with grep +# (the 2026-09-30 deploy refusal). Runs as dank: the unit's process uses +# dank's rootless storage. +require_image "$FRANK_DRAGOON_IMAGE" \ + "pull it: podman pull docker://$FRANK_DRAGOON_IMAGE" || exit 1 # Size-exact gates on all three payload files: a truncated curl exits 0 on # some failures and a partial .hgn loads far enough to look alive, so the # byte counts from the Hub tree API are the contract (fleet fetch law). -ck() { - # ck - if [ ! -f "$2" ]; then - echo "frank-dragoon: $1 $2 missing;" >&2 - echo " fetch it: bash scripts/download-frank-dragoon-flashnext.sh" >&2 - exit 1 - fi - local sz - sz="$(stat -c %s "$2")" - if [ "$sz" != "$3" ]; then - echo "frank-dragoon: $1 is $sz bytes, expected $3 (partial download?)" >&2 - exit 1 - fi -} -ck checkpoint "$FRANK_DRAGOON_MODEL_DIR/$FRANK_DRAGOON_CHECKPOINT" "$FRANK_DRAGOON_CHECKPOINT_BYTES" -ck n-gram-table "$FRANK_DRAGOON_MODEL_DIR/$FRANK_DRAGOON_NGRAM_TABLE" "$FRANK_DRAGOON_NGRAM_BYTES" -ck vision-tower "$FRANK_DRAGOON_MODEL_DIR/$FRANK_DRAGOON_VISION_TOWER" "$FRANK_DRAGOON_VISION_BYTES" +require_bytes "$FRANK_DRAGOON_MODEL_DIR/$FRANK_DRAGOON_CHECKPOINT" \ + checkpoint "$FRANK_DRAGOON_CHECKPOINT_BYTES" \ + "fetch it: bash scripts/download-frank-dragoon-flashnext.sh" || exit 1 +require_bytes "$FRANK_DRAGOON_MODEL_DIR/$FRANK_DRAGOON_NGRAM_TABLE" \ + n-gram-table "$FRANK_DRAGOON_NGRAM_BYTES" \ + "fetch it: bash scripts/download-frank-dragoon-flashnext.sh" || exit 1 +require_bytes "$FRANK_DRAGOON_MODEL_DIR/$FRANK_DRAGOON_VISION_TOWER" \ + vision-tower "$FRANK_DRAGOON_VISION_BYTES" \ + "fetch it: bash scripts/download-frank-dragoon-flashnext.sh" || exit 1 # The payload ships the tokenizer as a FLAT DIRECTORY. A HuggingFace cache # snapshot symlinks each file into a sibling blobs/, and those symlinks # dangle inside the container mount — so the directory must be real and # every named file must be a regular file, not a link. -TOK_DIR="$FRANK_DRAGOON_MODEL_DIR/tokenizer" -if [ -L "$TOK_DIR" ] || [ ! -d "$TOK_DIR" ]; then - echo "frank-dragoon: tokenizer dir $TOK_DIR missing or a symlink (HF cache snapshot?);" >&2 - echo " fetch it: bash scripts/download-frank-dragoon-flashnext.sh" >&2 - exit 1 -fi -for TF in $FRANK_DRAGOON_TOKENIZER_FILES; do - if [ ! -f "$TOK_DIR/$TF" ] || [ -L "$TOK_DIR/$TF" ]; then - echo "frank-dragoon: tokenizer file $TOK_DIR/$TF missing or a symlink;" >&2 - echo " fetch it: bash scripts/download-frank-dragoon-flashnext.sh" >&2 - exit 1 - fi -done +# The tokenizer file list is word-separated ON PURPOSE (one argument per +# file), so the unquoted expansion below is the interface, not a slip. +# shellcheck disable=SC2086 +require_dir_files "$FRANK_DRAGOON_MODEL_DIR/tokenizer" tokenizer \ + "fetch it: bash scripts/download-frank-dragoon-flashnext.sh" \ + $FRANK_DRAGOON_TOKENIZER_FILES || exit 1 # GPU access is mandatory but the node paths are variables so the seam can # drive the gate on a box without an AMD GPU. @@ -115,6 +104,7 @@ if [ ! -e "$DEV_KFD" ] || [ ! -d "$DEV_DRI" ]; then echo "frank-dragoon: $DEV_KFD or $DEV_DRI missing — no gfx1151 to serve on" >&2 exit 1 fi + # Host-RAM preflight — the engine's pin gate, checked BEFORE podman run. # The engine refuses to pin when MemAvailable is below the checkpoint plus # a 16 GiB floor: pinned pages cannot be swapped, and under the floor the diff --git a/tests/frank_dragoon_unit.bats b/tests/frank_dragoon_unit.bats index 9ce038d..cbe76cf 100755 --- a/tests/frank_dragoon_unit.bats +++ b/tests/frank_dragoon_unit.bats @@ -45,6 +45,13 @@ podman_stub() { #!/usr/bin/env bash case "$1" in images) printf '%s\n' "${STUB_DIGESTS:-}" ;; + image) + # `podman image exists REF` — exit 1 when storage lacks the ref, + # which is exactly the primitive the shared gate library calls. + case " ${STUB_DIGESTS:-} " in + *" $3 "*) exit 0 ;; + *) exit 1 ;; + esac ;; rm) exit 0 ;; run|wait) echo "PODMAN $*"; exit "${STUB_WAIT_RC:-0}" ;; *) exit 0 ;; diff --git a/tests/llm_gates.bats b/tests/llm_gates.bats index dbba811..66fd530 100755 --- a/tests/llm_gates.bats +++ b/tests/llm_gates.bats @@ -130,6 +130,15 @@ call() { [[ "$output" == *"tokenizer: missing"* ]] } +@test "the flat-directory gate refuses a symlinked directory" { + # The snapshot directory itself can be the link (a HF cache blobs/ + # sibling), and a link into the container mount dangles there too. + ln -s "$TMP/flat" "$TMP/linked-dir" + call "require_dir_files '$TMP/linked-dir' tokenizer 'fetch the tokenizer' tokenizer.json" + [ "$status" -ne 0 ] + [[ "$output" == *"tokenizer: missing"* ]] +} + @test "the image gate refuses a ref the storage does not hold" { # A name@sha256 ref exists only if THAT manifest was pulled, so the # gate is the ref itself — one command, no grep race.