add validate.sh pipeline gate and audit convention notes
.agents/validate.sh is the commit gate: full E2E suite, container security-header check, and repo-hygiene checks must all pass before a phase is committed. AGENTS.md now documents the audit-driven conventions (M-1 innerHTML/terminal input, M-2 simulated curl/wget, M-4 security-header snippet, L-2 server_tokens) and the test-locked featured-projects grid. LLM-Generated: true
This commit is contained in:
Executable
+90
@@ -0,0 +1,90 @@
|
|||||||
|
#!/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 <file>
|
||||||
|
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 <file>'):" >&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 <file>' 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"
|
||||||
@@ -17,8 +17,14 @@ Rules for any agent working in this repository.
|
|||||||
deployed artifact — never test `src/` directly).
|
deployed artifact — never test `src/` directly).
|
||||||
- `npm test` — the Playwright E2E suite (pinned `@playwright/test`
|
- `npm test` — the Playwright E2E suite (pinned `@playwright/test`
|
||||||
1.63.0; do not bump without re-checking the cached browser revision).
|
1.63.0; do not bump without re-checking the cached browser revision).
|
||||||
- For any nginx/Dockerfile change: `./scripts/check-headers.sh`
|
- `.agents/validate.sh` — the pipeline gate: full E2E suite + the container
|
||||||
(Phase 03+).
|
security-header check + repo-hygiene checks; must exit 0 before a phase is
|
||||||
|
committed.
|
||||||
|
- For any `nginx.conf` / `nginx-security-headers.inc` / Dockerfile change:
|
||||||
|
`./scripts/check-headers.sh` (added by Phase 03, run automatically by
|
||||||
|
`.agents/validate.sh` when a container runtime exists). It builds the real
|
||||||
|
image, serves it on `127.0.0.1:18099` and must print
|
||||||
|
`HEADER CHECK PASSED`; don't substitute a hand-rolled `curl` for it.
|
||||||
|
|
||||||
## Never
|
## Never
|
||||||
- Modify `.agents/PLAN.md` except via an explicit user-approved change to the
|
- Modify `.agents/PLAN.md` except via an explicit user-approved change to the
|
||||||
@@ -37,6 +43,36 @@ Rules for any agent working in this repository.
|
|||||||
- Static site: edits in `src/` → `./build.sh` → `dist/` (hash-cached names).
|
- Static site: edits in `src/` → `./build.sh` → `dist/` (hash-cached names).
|
||||||
New root-level assets are picked up automatically by `build.sh`; nested
|
New root-level assets are picked up automatically by `build.sh`; nested
|
||||||
directories are **not** (keep JS files flat in `src/`).
|
directories are **not** (keep JS files flat in `src/`).
|
||||||
|
- Terminal input is untrusted: never render typed input or `commandHistory`
|
||||||
|
with `innerHTML` — route it through `updateDisplay()`/`textContent`
|
||||||
|
(audit M-1, guarded by the markup-payload test in
|
||||||
|
`tests/e2e/02-terminal.spec.js`). The only `innerHTML` uses allowed in
|
||||||
|
`src/terminal.js` are static prompt literals, the `clear`/prank element
|
||||||
|
wipes, and the canned `neofetch` output (the `cmdText === 'neofetch'` branch —
|
||||||
|
every other command output goes through `textContent`). In
|
||||||
|
`terminal-vim.js`/`terminal-achievements.js` `innerHTML` is acceptable only
|
||||||
|
with static markup or `escapeHtml()`-escaped text.
|
||||||
|
- The fake terminal does zero network I/O: `curl`/`wget` render canned output
|
||||||
|
from `simulatedCurlOutput()` / `simulatedWgetOutput()` in
|
||||||
|
`src/terminal-commands.js` (pure string builders, no DOM, no network). Keep
|
||||||
|
any browser-side fetch call out of `src/` entirely (audit M-2, Phase 02).
|
||||||
|
Guarded by the curl/wget regression test in `tests/e2e/02-terminal.spec.js`,
|
||||||
|
which records every request whose URL falls outside `baseURL` from
|
||||||
|
`playwright.config.js` and fails the suite if the list is not empty.
|
||||||
|
- Security headers live in `nginx-security-headers.inc` (the Dockerfile copies
|
||||||
|
it to `/etc/nginx/snippets/security-headers.inc`) and **every** `location` in
|
||||||
|
`nginx.conf` `include`s it. nginx does not merge `add_header` across
|
||||||
|
configuration levels, so never rely on server-level inheritance and never add
|
||||||
|
inline security `add_header` lines to `nginx.conf` — put them in the snippet
|
||||||
|
(audit M-4, Phase 03). `server_tokens off;` stays in the `server` block
|
||||||
|
(audit L-2). `scripts/check-headers.sh` guards this rule: drop one `include`
|
||||||
|
from a location and the check fails for that path.
|
||||||
|
- Featured-projects content is test-locked: the `#projects` grid in
|
||||||
|
`src/index.html` (six cards in a fixed order — Brain of Reese first with its
|
||||||
|
Gitea source link, ServWaf last with no link because it is closed source) is
|
||||||
|
asserted by the `featured projects show the six updated cards in order` test
|
||||||
|
in `tests/e2e/01-page.spec.js`. Edit the cards in `src/index.html` and update
|
||||||
|
that test in the same change (Phase 05).
|
||||||
- Terminal modules: `terminal-commands.js` (canned data),
|
- Terminal modules: `terminal-commands.js` (canned data),
|
||||||
`terminal-achievements.js`, `terminal-vim.js`, `terminal.js` (core) — load
|
`terminal-achievements.js`, `terminal-vim.js`, `terminal.js` (core) — load
|
||||||
order is fixed in `index.html` (all `defer`); keep dependencies pointing
|
order is fixed in `index.html` (all `defer`); keep dependencies pointing
|
||||||
|
|||||||
Reference in New Issue
Block a user