avoigt1121
Claude Opus 5.5
feat: governed ToolUniverse core for the knowledge specialist (s1)
377417d
Raw History Blame Contribute Delete
9.27 kB
#!/usr/bin/env bash
# Two jobs, in this order (the genomics-agent pattern):
# 1. ADR-0014 security gate β€” runs on EVERY push. Blocks on findings.
# 2. GitHub mirror sync β€” only on a PROD deploy (`main` to `origin`). Warns, never blocks.
# The gate runs first, so a blocked deploy can never have already updated the mirror.
#
# --- 1. ADR-0014 pre-push security gate ----------------------------------------------------
#
# `origin` and `hf-dev` ARE HuggingFace Spaces β€” a push builds a live Space β€” so there is
# no CI merge gate in front of them. This hook is the enforced check: it runs the shared
# scan (scripts/security_scan.sh) before anything leaves the machine.
#
# It scans the commit being PUSHED, not the working tree it happens to run from. The
# sibling hooks scan the working tree, and on 2026-09-21 that went wrong in the
# orchestrator: pushed from a checkout parked on another lane's branch, the scan audited
# THAT branch's requirements.txt instead of the commit being shipped. Here each pushed sha
# is checked out into a throwaway worktree and scanned there; reports (and the cached
# audit venv) are written to this checkout's security/ so they outlive it.
#
# Install with: make install-hooks
# Bypass (e.g. a docs-only push, knowingly): git push --no-verify
# Set SECURITY_SCAN_STRICT=1 to fail on skipped stages.
set -euo pipefail
# The mirror push below sets this, so that nested push is neither rescanned nor re-mirrored.
[[ -n "${SECURITY_SCAN_DONE:-}" ]] && exit 0
remote_name="${1:-}"
remote_url="${2:-}"
mirror_remote="${MIRROR_REMOTE:-github}"
dev_remote="${DEV_REMOTE:-hf-dev}"
repo_root="$(git rev-parse --show-toplevel)"
# stdin: "<local ref> <local sha> <remote ref> <remote sha>" per ref being pushed. Git sends it
# once and both jobs need it, so it is read here and replayed from the variable.
push_lines="$(cat)"
shas=()
while read -r _lref lsha _rref _rsha; do
[[ -n "${lsha:-}" ]] || continue
case "$lsha" in *[!0]*) ;; *) continue ;; esac # all-zero = a delete; nothing to scan
[[ " ${shas[*]-} " == *" $lsha "* ]] || shas+=("$lsha")
done <<< "$push_lines"
[[ ${#shas[@]} -gt 0 ]] || exit 0
# Sweep leaks from earlier interrupted pushes. Named (not `mktemp -d`) so a leak is
# identifiable, and keyed on LIVENESS rather than age: each scan dir records its hook's PID
# and its scan child's, so a dir whose hook is gone is swept at once while a CONCURRENT
# push's dir is never touched. A dir with no .hookpid predates this and is by definition a
# leak. (2026-09-23: a leaked worktree was found with `git worktree add`'s inner
# `git reset --hard` still sleeping after 2 days 7 hours, in a directory macOS had reaped.)
for stale in "${TMPDIR:-/tmp}"/pre-push-scan.*; do
[[ -d "$stale" ]] || continue
stale_pid="$(cat "$stale/.hookpid" 2>/dev/null || true)"
if [[ -z "$stale_pid" ]] || ! kill -0 "$stale_pid" 2>/dev/null; then
stale_scan="$(cat "$stale/.scanpid" 2>/dev/null || true)"
if [[ -n "$stale_scan" ]] && kill -0 "$stale_scan" 2>/dev/null; then
kill -TERM "-$stale_scan" 2>/dev/null || kill -TERM "$stale_scan" 2>/dev/null || true
sleep 1
if kill -0 "$stale_scan" 2>/dev/null; then
kill -KILL "-$stale_scan" 2>/dev/null || kill -KILL "$stale_scan" 2>/dev/null || true
fi
fi
for wt in "$stale"/wt-*; do
[[ -d "$wt" ]] && git worktree remove --force "$wt" >/dev/null 2>&1
done
rm -rf "$stale"
echo "[pre-push] swept a leaked scan directory from an interrupted push: $(basename "$stale")"
fi
done
git worktree prune >/dev/null 2>&1 || true
tmp="$(mktemp -d "${TMPDIR:-/tmp}/pre-push-scan.XXXXXX")"
echo "$$" > "$tmp/.hookpid"
scan_pid=""
cleanup() {
# Kill the scan BEFORE its directory disappears: deleting the tree under a live child is
# how a gitleaks or `git reset --hard` ends up running in a path that no longer exists.
if [[ -n "$scan_pid" ]] && kill -0 "$scan_pid" 2>/dev/null; then
# Negative pid = the whole process group. run_scan starts the job with `set -m` so it
# gets its own group; signalling the group takes gitleaks / pip-audit / bandit down with
# the shell that launched them, instead of reparenting them to init.
kill -TERM "-$scan_pid" 2>/dev/null || kill -TERM "$scan_pid" 2>/dev/null || true
wait "$scan_pid" 2>/dev/null || true
fi
for wt in "$tmp"/wt-*; do [[ -d "$wt" ]] && git worktree remove --force "$wt" >/dev/null 2>&1; done
git worktree prune >/dev/null 2>&1 || true
rm -rf "$tmp"
}
# EXIT alone is not enough: a non-interactive bash killed by an UNTRAPPED signal dies
# without running its EXIT trap, so Ctrl-C, a closed terminal or a terminated agent session
# left the worktree registered and its directory behind. `trap - EXIT` stops a double run.
# Nothing in-process survives SIGKILL β€” that case is what the sweep above is for.
on_signal() {
cleanup
trap - EXIT
echo "[pre-push] interrupted β€” scan worktree cleaned up; push aborted." >&2
exit 130
}
trap cleanup EXIT
trap on_signal INT TERM HUP
# Run the scan as a background job and `wait`: bash defers a trap handler until the running
# FOREGROUND command returns, so a Ctrl-C during a multi-minute scan would not be acted on
# until the scan finished. `wait` IS interruptible, so cleanup runs at once.
run_scan() {
set -m # give the job its own process group, so cleanup can signal the tree
"$@" &
scan_pid=$!
set +m
echo "$scan_pid" > "$tmp/.scanpid"
local rc=0
wait "$scan_pid" || rc=$?
scan_pid=""
rm -f "$tmp/.scanpid"
return "$rc"
}
for sha in "${shas[@]}"; do
short="${sha:0:7}"
# Branches that predate the scan have no script β€” skip them cleanly rather than block.
if ! git cat-file -e "$sha:scripts/security_scan.sh" 2>/dev/null; then
echo "[pre-push] $short has no scripts/security_scan.sh β€” skipping scan for it."
continue
fi
echo "[pre-push] running ADR-0014 security scan on the pushed commit $short (git push --no-verify to skip)…"
wt="$tmp/wt-$short"
git worktree add --detach --quiet "$wt" "$sha"
# Git exports GIT_DIR & co. into hooks; left set, the scan's own git calls (bandit's
# target discovery, gitleaks) would read the wrong checkout.
scan_job() {
env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE -u GIT_PREFIX \
SECURITY_SCAN_OUT="$repo_root/security" bash "$wt/scripts/security_scan.sh"
}
if run_scan scan_job; then
echo "[pre-push] $short: security scan clean."
else
echo "[pre-push] $short: security scan reported findings β€” push blocked." >&2
echo "[pre-push] triage the reports in security/, or 'git push --no-verify' to override." >&2
exit 1
fi
done
echo "[pre-push] security scan clean β€” proceeding."
# --- 2. GitHub mirror sync -----------------------------------------------------------------
# `origin` IS the prod Space and `github` is a source mirror that only runs CI; nothing links
# them, so the mirror fell behind prod by hand-sync on 2026-10-01 more than once. A stale mirror
# means CI runs a SUBSET of production, which reads as a pass. This syncs the mirror's `main`
# at the only moment git offers: when a prod deploy is about to go out.
#
# Two honest limitations (as in pdac-genomics-agent):
# 1. The mirror is pushed FIRST: git has no post-push hook. If the `origin` push then fails,
# `github` is briefly AHEAD of prod. That self-corrects on the next deploy, and CI running a
# superset of production is the milder error.
# 2. A mirror failure WARNS; it does not block. The mirror is CI; `origin` is production. It is
# pushed without force, so a mirror that has moved on (another lane synced it) is left alone.
#
# Only `main` on `origin` is mirrored. `git push hf-dev dev:main` also targets refs/heads/main on
# a huggingface.co URL, so the dev Space is excluded by name AND by URL first β€” mirroring it
# would put dev code on the mirror's `main`, where CI reads it as production.
case "$remote_name$remote_url" in
"$dev_remote"*|*-dev|*-dev.git|*-dev/) exit 0 ;;
esac
case "$remote_name$remote_url" in
origin*|*huggingface.co/spaces/anne-voigt/pdac-knowledge-agent) ;;
*) exit 0 ;;
esac
main_sha=""
while read -r _lref lsha rref _rsha; do
[[ "${rref:-}" == "refs/heads/main" ]] || continue
case "${lsha:-}" in *[!0]*) main_sha="$lsha" ;; esac # all-zero = a DELETE; never mirror it
done <<< "$push_lines"
[[ -n "$main_sha" ]] || exit 0 # nothing headed for main: not a deploy
if ! git remote get-url "$mirror_remote" >/dev/null 2>&1; then
echo "[pre-push] no '$mirror_remote' remote β€” the GitHub mirror will fall behind this deploy." >&2
echo "[pre-push] git remote add $mirror_remote https://github.com/Anne-Voigt/pdac-knowledge-agent.git" >&2
exit 0
fi
echo "[pre-push] mirroring ${main_sha:0:7} to '$mirror_remote' main so the mirror matches prod…"
if SECURITY_SCAN_DONE=1 git push --quiet "$mirror_remote" "$main_sha:refs/heads/main"; then
echo "[pre-push] mirror in sync β€” proceeding with the deploy."
else
echo "[pre-push] WARNING: mirror push failed (often: it already moved on). The deploy proceeds." >&2
echo "[pre-push] Check it after the deploy: git fetch $mirror_remote && git log --oneline -1 $mirror_remote/main" >&2
fi
exit 0