feat: land the revision-preflight guard, fixed and wired into contract execution
PR Pipeline — Authorize → Validate → Review → Merge / auth (pull_request) Successful in 6s
PR Pipeline — Authorize → Validate → Review → Merge / validate (pull_request) Successful in 4s
PR Pipeline — Authorize → Validate → Review → Merge / lint (pull_request) Failing after 6s
PR Pipeline — Authorize → Validate → Review → Merge / ai-review (pull_request) Successful in 2s
PR Pipeline — Authorize → Validate → Review → Merge / gate (pull_request) Skipped
PR Pipeline — Authorize → Validate → Review → Merge / auth (pull_request) Successful in 6s
PR Pipeline — Authorize → Validate → Review → Merge / validate (pull_request) Successful in 4s
PR Pipeline — Authorize → Validate → Review → Merge / lint (pull_request) Failing after 6s
PR Pipeline — Authorize → Validate → Review → Merge / ai-review (pull_request) Successful in 2s
PR Pipeline — Authorize → Validate → Review → Merge / gate (pull_request) Skipped
A contract verdict is only meaningful if it came from the merged copy. The fleet has been bitten three times on 2026-09-25 (a clone parked on a merged feature branch while executing from another clone; a script copied into the runner clone by hand; a stale local origin/master making an ancestry check report unlanded work). The control for this existed as an untracked draft and protected nobody, because it was entirely fail-open. Defect in the draft, preserved verbatim as tests/fixtures/revision-preflight.prefix.sh: git -C "$CLONE" show "origin/master:$(basename "$SCRIPT")" basename drops the scripts/ prefix, so for any script under scripts/ it queried the repo root, failed, took the "warn but don't block" branch and exited 0 - passing a script that exists in no revision at all. Reproduced: pre-fix + scripts/demo.sh under scripts/ -> 'could not resolve', EXIT=0 pre-fix + a script in no revision -> EXIT=0 Fixed guard (scripts/revision-preflight.sh): * resolves the repo-relative path inside the clone, so scripts/ paths resolve; * FAILS CLOSED - a path absent from the ref, an unresolvable ref, or a failed fetch is a failure, never a warning; * fetches the remote by default, because a stale local ref would otherwise pass a stale script as current; --no-fetch states the assumption instead of hiding it. Wiring (scripts/contract-run.sh): before executing, the wrapper runs the guard against the clone it lives in. Default CONTRACT_REVISION_PREFLIGHT=enforce withholds the verdict, alerts and exits 2 on mismatch; =warn logs and continues; =off skips. Verified live: match -> contract proceeds and PASSes; mismatch -> 'VERDICT WITHHELD', exit 2; =warn -> continues. Pinning (docs/contract-execution-pinning.md): every contract pins the clone contract-run.sh lives in - the deployed runner being /opt/contract-runner on CT 100. Documented that daily-health-digest has no contract file at all, which is why its execution copy was silently operator-chosen. Tests: tests/test_revision_preflight.sh, 15 assertions over a throwaway clone with a real bare remote. It runs the pre-fix draft against the same cases and shows it passing a ghost script, so the tests provably bite. shellcheck: scripts/revision-preflight.sh and the new test are clean. The three findings remaining in contract-run.sh (SC2086 x2, SC2034) are pre-existing and byte-identical on master.
This commit is contained in:
@@ -18,6 +18,14 @@
|
||||
# litellm-health -> scripts/litellm-health-check.py
|
||||
# disk-gc-threat-response -> scripts/disk-gc-scan.py
|
||||
# pm2-self-heal -> scripts/pm2-self-heal.sh
|
||||
# search-stack-visibility -> scripts/search-stack-check.py
|
||||
#
|
||||
# Execution copy: every contract pins the clone this script lives in (see
|
||||
# docs/contract-execution-pinning.md). Before a contract runs, this wrapper
|
||||
# proves the script it is about to execute byte-matches origin/master:
|
||||
# CONTRACT_REVISION_PREFLIGHT=enforce (default) refuse to report on mismatch
|
||||
# CONTRACT_REVISION_PREFLIGHT=warn log the mismatch and continue
|
||||
# CONTRACT_REVISION_PREFLIGHT=off skip the check entirely
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = contract passed
|
||||
@@ -111,6 +119,35 @@ echo "Started: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" | tee -a "$LOG_FILE"
|
||||
echo "Script: $SCRIPT_PATH" | tee -a "$LOG_FILE"
|
||||
echo "" | tee -a "$LOG_FILE"
|
||||
|
||||
# ── Revision preflight ───────────────────────────────────────────────────────
|
||||
# A verdict is only meaningful if it came from the merged copy. Refuse to report
|
||||
# one from a mismatched or unverifiable copy; that is a probe failure (exit 2),
|
||||
# not a contract verdict, because the result would be untrustworthy.
|
||||
# See docs/contract-execution-pinning.md.
|
||||
REVISION_PREFLIGHT_MODE="${CONTRACT_REVISION_PREFLIGHT:-enforce}"
|
||||
REPO_ROOT="$(cd "${SCRIPTS_DIR}/.." && pwd)"
|
||||
if [ "$REVISION_PREFLIGHT_MODE" != "off" ] && [ -x "${SCRIPTS_DIR}/revision-preflight.sh" ]; then
|
||||
if "${SCRIPTS_DIR}/revision-preflight.sh" "$SCRIPT_PATH" "$REPO_ROOT" 2>&1 | tee -a "$LOG_FILE"; then
|
||||
:
|
||||
elif [ "$REVISION_PREFLIGHT_MODE" = "warn" ]; then
|
||||
echo "⚠️ revision preflight failed — continuing because CONTRACT_REVISION_PREFLIGHT=warn" | tee -a "$LOG_FILE"
|
||||
else
|
||||
echo "🚫 VERDICT WITHHELD: executing copy does not match the merged revision" | tee -a "$LOG_FILE"
|
||||
ALERT_MSG="🔴 Contract $CONTRACT_NAME: revision mismatch — verdict withheld. Log: $LOG_FILE"
|
||||
ZULIP_API_URL="${ZULIP_API_URL:-https://chat.sysloggh.net/api/v1}"
|
||||
ZULIP_API_KEY="${ZULIP_API_KEY:-}"
|
||||
ZULIP_USER="${ZULIP_USER:-abiba-bot@chat.sysloggh.net}"
|
||||
if [ -n "$ZULIP_API_KEY" ] && command -v curl &> /dev/null; then
|
||||
curl -sf -X POST "${ZULIP_API_URL}/messages" \
|
||||
-u "${ZULIP_USER}:${ZULIP_API_KEY}" \
|
||||
-d "type=private" \
|
||||
-d "to=9" \
|
||||
-d "content=${ALERT_MSG}" > /dev/null 2>&1 || true
|
||||
fi
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
# Use timeout to prevent hangs (10 minutes default)
|
||||
TIMEOUT=600
|
||||
timeout "$TIMEOUT" $INTERPRETER "$SCRIPT_PATH" 2>&1 | tee -a "$LOG_FILE"
|
||||
|
||||
Executable
+128
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env bash
|
||||
# revision-preflight.sh — prove the copy a contract is about to execute is the
|
||||
# copy that is merged.
|
||||
#
|
||||
# Usage:
|
||||
# revision-preflight.sh [options] <script-path> <clone-path>
|
||||
#
|
||||
# Options:
|
||||
# --ref <ref> Ref to compare against (default: origin/master)
|
||||
# --no-fetch Do not refresh the ref first (see FRESHNESS below)
|
||||
# --quiet Print nothing on success
|
||||
# -h, --help Show this help
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 the executing script byte-matches <ref>:<repo-relative-path>
|
||||
# 1 MISMATCH, or the revision could not be resolved (see FAIL CLOSED)
|
||||
#
|
||||
# FRESHNESS
|
||||
# A guard is only as good as the ref it compares against. On 2026-09-25 a
|
||||
# stale local origin/master made an ancestry check on this fleet report
|
||||
# "unlanded work" for a branch that had in fact merged, and it would equally
|
||||
# have passed a stale script as current. So by default this guard FETCHES the
|
||||
# remote before comparing. With --no-fetch it compares against whatever the
|
||||
# local ref points at and says so out loud; it never silently assumes
|
||||
# freshness.
|
||||
#
|
||||
# FAIL CLOSED
|
||||
# An unresolvable path or ref is a FAILURE, never a warning. "Cannot verify"
|
||||
# is precisely the state a stale or hand-edited copy produces, so treating it
|
||||
# as success would defeat the guard. The original draft of this script did
|
||||
# exactly that: it resolved the master revision with
|
||||
# `git show origin/master:$(basename "$SCRIPT")`, which drops the scripts/
|
||||
# prefix, queries the repo root, fails, and exited 0 — passing a script that
|
||||
# exists in no revision at all.
|
||||
#
|
||||
# WHICH CLONE
|
||||
# Pass the clone the contract is actually executing from. See
|
||||
# docs/contract-execution-pinning.md for which clone each contract pins.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REF="origin/master"
|
||||
FETCH=1
|
||||
QUIET=0
|
||||
|
||||
usage() {
|
||||
sed -n '2,45p' "$0" | sed 's/^# \{0,1\}//'
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--ref)
|
||||
[[ $# -ge 2 ]] || { echo "revision-preflight: --ref needs a value" >&2; exit 1; }
|
||||
REF="$2"; shift 2 ;;
|
||||
--no-fetch) FETCH=0; shift ;;
|
||||
--quiet) QUIET=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--) shift; break ;;
|
||||
-*) echo "revision-preflight: unknown option: $1" >&2; exit 1 ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ $# -lt 2 ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT="$1"
|
||||
CLONE="$2"
|
||||
|
||||
say() { [[ $QUIET -eq 1 ]] || echo "$@" >&2; }
|
||||
fail() { echo "❌ revision-preflight: $*" >&2; exit 1; }
|
||||
|
||||
# ── 1. inputs must exist ──────────────────────────────────────────────────────
|
||||
[[ -f "$SCRIPT" ]] || fail "executing script not found: $SCRIPT"
|
||||
[[ -d "$CLONE" ]] || fail "clone path is not a directory: $CLONE"
|
||||
git -C "$CLONE" rev-parse --git-dir >/dev/null 2>&1 \
|
||||
|| fail "not a git clone: $CLONE"
|
||||
|
||||
# ── 2. resolve the repo-relative path (the original defect) ───────────────────
|
||||
CLONE_ABS=$(cd "$CLONE" && pwd)
|
||||
SCRIPT_ABS=$(cd "$(dirname "$SCRIPT")" && pwd)/$(basename "$SCRIPT")
|
||||
case "$SCRIPT_ABS" in
|
||||
"$CLONE_ABS"/*) REL="${SCRIPT_ABS#"$CLONE_ABS"/}" ;;
|
||||
*) fail "script is outside the clone: $SCRIPT_ABS is not under $CLONE_ABS" ;;
|
||||
esac
|
||||
|
||||
# ── 3. refresh the ref so staleness cannot mask a stale script ────────────────
|
||||
if [[ $FETCH -eq 1 ]]; then
|
||||
REMOTE="${REF%%/*}"
|
||||
[[ "$REMOTE" == "$REF" ]] && REMOTE="origin"
|
||||
if ! git -C "$CLONE" fetch --quiet "$REMOTE" 2>/dev/null; then
|
||||
fail "cannot fetch '$REMOTE' in $CLONE — refusing to verify against a possibly stale '$REF'. Re-run with network access, or pass --no-fetch to compare against the local ref deliberately."
|
||||
fi
|
||||
else
|
||||
say "⚠️ revision-preflight: --no-fetch — comparing against the LOCAL '$REF'; freshness is assumed, not verified"
|
||||
fi
|
||||
|
||||
# ── 4. resolve the merged revision; unresolvable is a failure ────────────────
|
||||
git -C "$CLONE" rev-parse --verify --quiet "$REF" >/dev/null \
|
||||
|| fail "ref '$REF' does not resolve in $CLONE"
|
||||
REF_COMMIT=$(git -C "$CLONE" rev-parse --short "$REF")
|
||||
|
||||
TMPFILE=$(mktemp)
|
||||
trap 'rm -f "$TMPFILE"' EXIT
|
||||
|
||||
if ! git -C "$CLONE" show "$REF:$REL" > "$TMPFILE" 2>/dev/null; then
|
||||
fail "'$REL' does not exist in $REF ($REF_COMMIT) — cannot verify $SCRIPT. A path that is absent from $REF can never be a merged copy."
|
||||
fi
|
||||
|
||||
# ── 5. compare ───────────────────────────────────────────────────────────────
|
||||
EXEC_SHA=$(sha256sum "$SCRIPT" | cut -d' ' -f1)
|
||||
MERGED_SHA=$(sha256sum "$TMPFILE" | cut -d' ' -f1)
|
||||
|
||||
if [[ "$EXEC_SHA" != "$MERGED_SHA" ]]; then
|
||||
{
|
||||
echo "❌ revision-preflight: MISMATCH — refusing to report from this copy"
|
||||
echo " script: $SCRIPT_ABS"
|
||||
echo " clone: $CLONE_ABS"
|
||||
echo " executed: $EXEC_SHA"
|
||||
echo " merged: $MERGED_SHA ($REF:$REL @ $REF_COMMIT)"
|
||||
} >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
say "✅ revision-preflight: $REL matches $REF @ $REF_COMMIT ($EXEC_SHA)"
|
||||
exit 0
|
||||
Reference in New Issue
Block a user