#!/usr/bin/env bash # .agents/validate.sh — validation gate for the phased-execution pipeline. # # A unit is only moved to .agents/phases/complete/ if this script exits 0, # and a phase is only COMMITTED if its final pass exits 0 — so this script # is the last line of defense for "no phase commits unless 100% passing". # It MUST run the project's FULL test suite (every test, not a subset) plus # its linter, so a failure anywhere in the project blocks the commit. # # Adapt the checks below to this project's real test suite, linter, and # coverage floor, then commit the result. set -uo pipefail rc=0 if [[ -f pyproject.toml || -f pytest.ini || -f setup.py ]]; then python3 -m pytest -q || rc=1 if command -v ruff >/dev/null 2>&1; then ruff check . || rc=1 fi fi if [[ -f package.json ]]; then npm test --silent || rc=1 fi # --- project-specific gate: container security headers (Phase 03, audit M-4) -- # Builds the real nginx image and asserts all four hardening headers on every # response class (/, /index.html, a hashed asset). Needs a container runtime; # if the host has none the check is skipped loudly instead of failing silently. if [[ -x scripts/check-headers.sh ]]; then if command -v podman >/dev/null 2>&1; then RT=podman elif command -v docker >/dev/null 2>&1; then RT=docker else RT="" fi if [[ -n "$RT" ]]; then ./scripts/check-headers.sh || rc=1 # check-headers.sh removes its container but not its scratch image tag; # drop it here so repeated gates do not fill container storage. leftovers="$("$RT" images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep 'homepage-headercheck-' || true)" if [[ -n "$leftovers" ]]; then printf '%s\n' "$leftovers" | xargs -r "$RT" rmi -f >/dev/null 2>&1 || true fi else echo "WARN: no podman/docker available — SKIPPING scripts/check-headers.sh (security-header gate not exercised)" >&2 fi fi # --- repo hygiene (language-agnostic; runs in every project) ------------------ # Scoped staging commits everything the phase touched, so a file that is not # ignored gets committed — including raw log drops and key material. These # gates make the phase fail instead. Fix by untracking, keeping the working # copy: git rm --cached if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then # 1. A tracked file that matches a .gitignore rule should never be tracked. junk="$(git ls-files -i -c --exclude-standard 2>/dev/null || true)" if [[ -n "$junk" ]]; then echo "repo hygiene: TRACKED files match .gitignore rules (untrack with 'git rm --cached '):" >&2 printf ' %s\n' "$junk" >&2 rc=1 fi # 2. Personal-data / key-material file SHAPES that must never be tracked, # even in a project with no .gitignore rule for them. DANGEROUS='(^|/)[^/]*\.log$|(^|/)[^/]*\.jsonl$|(^|/)\.env($|\.)|\.(pem|key|crt|cer|p12|pfx|keystore|jks)$|(^|/)(id_rsa|id_ecdsa|id_ed25519)(\.pub)?$|(^|/)(credentials|service-account|client_secret)([-a-z0-9]*)\.json$' bad="$(git ls-files | grep -E "$DANGEROUS" || true)" if [[ -n "$bad" ]]; then echo "repo hygiene: TRACKED files look like log drops / key material (untrack with 'git rm --cached ' and add a .gitignore rule):" >&2 printf ' %s\n' "$bad" >&2 rc=1 fi # 3. Project-specific content scan for personal data inside tracked files # (client IPs, account ids, ...). Empty by default — fill it in per # project, and allow-list documentation ranges (RFC 5737 / 4193 / 3849, # 127.0.0.0/8, RFC1918) so fixtures do not trip it. Format: one ERE. PII_PATTERNS="${PII_PATTERNS:-}" if [[ -n "$PII_PATTERNS" ]]; then hits="$(git ls-files -z | xargs -0 grep -IHnE "$PII_PATTERNS" 2>/dev/null || true)" if [[ -n "$hits" ]]; then echo "personal-data gate: TRACKED files match PII_PATTERNS — redact them:" >&2 printf ' %s\n' "$hits" >&2 rc=1 fi fi fi if [[ $rc -ne 0 ]]; then echo "validation FAILED (see output above)" fi exit "$rc"