diff --git a/.agents/validate.sh b/.agents/validate.sh new file mode 100755 index 0000000..b897ea4 --- /dev/null +++ b/.agents/validate.sh @@ -0,0 +1,163 @@ +#!/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. +# +# Adapted to THIS project's real gate (AGENTS.md "Definition of done", PLAN.md +# "Validation / Verification Workflow"): +# +# uv run pytest --cov hermetic full suite, coverage floor 94% +# uv run ruff check src tests agent +# uv run pyright +# +# Two rules this script had to learn the hard way: +# +# * Run the PROJECT toolchain, not whatever is on PATH. `python3 -m pytest` here +# resolves to /usr/bin/python3 + ~/.local site-packages (tests/conftest.py puts +# src/ on sys.path, so it passes while testing an interpreter nobody deploys), +# and a bare `ruff` on PATH is older than the `ruff>=0.17.1` floor in +# pyproject.toml. `uv run` is the only invocation that matches the docs. +# * Never skip a check silently. `if command -v ruff; then ruff check .; fi` +# exits 0 when the linter is missing — a gate that can pass without linting is +# not a gate. A missing tool is a FAILURE here; the checks are listed by name. +set -uo pipefail +rc=0 + +# Run from the repo root no matter where the harness invoked us, so `src`, +# `tests`, `agent` and the pytest/ruff/pyright configs all mean the same tree. +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" || { echo "validation FAILED: cannot cd to $ROOT"; exit 1; } + +# --- the Python gate ---------------------------------------------------------- +# uv is the documented entry point (`uv sync --python 3.14`); if it is gone the +# venv is still usable directly, and only a total absence of both is fatal. +if command -v uv >/dev/null 2>&1; then + RUN=(uv run) +elif [[ -x .venv/bin/pytest && -x .venv/bin/ruff && -x .venv/bin/pyright ]]; then + RUN=(.venv/bin/) + echo "note: uv not on PATH — using .venv/bin directly" >&2 +else + echo "validation FAILED: no uv and no complete .venv (pytest+ruff+pyright)." >&2 + echo "Run 'uv sync --python 3.14'. Refusing to guess a system interpreter:" >&2 + echo "that path skips the pinned toolchain and can pass on the wrong stack." >&2 + exit 1 +fi + +echo "== uv run pytest --cov (hermetic: no network, live tests skip) ==" +# --cov-fail-under is the floor from PLAN.md (overall must not drop below 94%); +# the project floor in AGENTS.md is 80%, and we are at 95%. +"${RUN[@]}" pytest --cov --cov-fail-under=94 || rc=1 + +echo "== uv run ruff check src tests agent ==" +"${RUN[@]}" ruff check src tests agent || rc=1 + +echo "== uv run pyright ==" +# pyright's `include` in pyproject.toml is src+tests on purpose: agent/host_agent.py +# is stdlib-only and must stay runnable on the fleet's Python 3.10, so it is linted +# (ruff, above) but not type-checked against 3.14 stubs. +"${RUN[@]}" pyright || rc=1 + +# --- static assets: syntax only, no toolchain added --------------------------- +# There is no JS build step or JS linter (a locked anchor), so a broken page is +# only caught in a browser. `node --check` is a zero-dependency syntax gate that +# catches the class of error a stray brace makes; a missing node is a visible +# skip, never a silent pass of a check that ran nothing. +if command -v node >/dev/null 2>&1; then + echo "== node --check src/dashboards/web/static/*.js ==" + for js in src/dashboards/web/static/*.js; do + node --check "$js" || rc=1 + done +else + echo "note: node not on PATH — skipping the JS syntax check (browser verification only)" >&2 +fi + +# --- dependency currency (advisory, not a gate) ------------------------------- +# AGENTS.md rule 8 checks this after touching dependencies; making it a hard gate +# would block an unrelated phase on an upstream release that landed five minutes +# ago, so it is loud in the log instead of silent. +if command -v uv >/dev/null 2>&1 && [[ -f uv.lock ]]; then + outdated="$(uv pip list --outdated 2>/dev/null | grep -vE '^(Package|---|$)' || true)" + if [[ -n "$outdated" ]]; then + echo "advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):" >&2 + printf ' %s\n' "$outdated" >&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$' + # Project allow-list (this repo's deliberate, documented choice — see .gitignore): + # the env *templates* `.env.example` / `.env.dev.example` are tracked on purpose, + # because `cp .env.example .env` is the setup documented in README.md and every + # real `.env` / `.env.dev` stays git-ignored. A template is documentation, not key + # material — and 2b below keeps that claim honest, so this allow-list cannot smuggle + # a live credential past the gate. Nothing else is allow-listed: logs, key material, + # and real dot-env files still fail here. + SAFE_TEMPLATES='(^|/)\.env[^/]*\.(example|template|sample)$' + bad="$(git ls-files | grep -E "$DANGEROUS" | grep -vE "$SAFE_TEMPLATES" || 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 + + # 2b. A tracked env template may only carry placeholders: every + # *_PASSWORD|*_TOKEN|*_SECRET|*_KEY assignment must be empty or a placeholder + # word (AGENTS.md rule 6 — "never leak a credential", not even into a sample). + PLACEHOLDER='^(|changeme|change-me|replace-me[-a-z0-9]*|secret|placeholder|dummy|example|example\.net|x+)$' + tmpl_hits="" + while IFS= read -r f; do + [[ -z "$f" ]] && continue + while IFS= read -r line; do + val="${line#*=}" + val="${val%%#*}" + val=${val//[[:space:]]/} + val=${val//\"/} + val=${val//\'/} + [[ "${val,,}" =~ $PLACEHOLDER ]] && continue + tmpl_hits+=" $f: $line"$'\n' + done < <(grep -inE '^[[:space:]]*(export[[:space:]]+)?[A-Za-z0-9_]*(PASSWORD|TOKEN|SECRET|KEY)[A-Za-z0-9_]*=' "$f" 2>/dev/null || true) + done < <(git ls-files | grep -E "$SAFE_TEMPLATES" || true) + if [[ -n "$tmpl_hits" ]]; then + echo "repo hygiene: tracked env TEMPLATE carries a non-placeholder secret (move the real value to the git-ignored .env):" >&2 + printf '%s' "$tmpl_hits" >&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"