test(08-09): add check-phase8.sh gate skeleton with RED self-test
- Declares the ordered Phase 8 stage list and stage function skeletons - --red-contract <stage> is a permanent RED-harness self-test hook (exit 86, PHASE8_STAGE:<stage>:FAIL:PHASE8_RED:real-mcp-stage) - --contract-self-test and the full gate are completed in Task 3/08-10
This commit is contained in:
350
scripts/check-phase8.sh
Executable file
350
scripts/check-phase8.sh
Executable file
@@ -0,0 +1,350 @@
|
||||
#!/usr/bin/env bash
|
||||
# Phase 8 final unchanged-MCP acceptance gate (08-CONTEXT.md D-14; 08-09-PLAN.md
|
||||
# Task 1/Task 3; 08-10-PLAN.md Task 3 is the sole execution site for the full
|
||||
# suite). Boots disposable Postgres and the assembled Go app, starts the real
|
||||
# unchanged Node fonoteka-mcp against the three required environment
|
||||
# variables, and drives the full scripted SDK lifecycle: discovery, DCR,
|
||||
# PKCE authorize, JWT login/consent, token, an MCP tool call, refresh,
|
||||
# replay, and revoke. Both repositories' vet/test/race, parity/corpus,
|
||||
# secret-scan, full UI harness, and unchanged Nuxt/MCP diffs are also gated
|
||||
# here. fonoteka-mcp and the Nuxt app are never modified.
|
||||
#
|
||||
# Modes:
|
||||
# (no flags) run the complete gate -- 08-10 Task 3 only.
|
||||
# --contract-self-test syntax/source assertions only, no services booted.
|
||||
# Designed for well under 30 seconds (08-09 Task 3).
|
||||
# --red-contract <stage> deliberately fail the named stage with the fixed
|
||||
# PHASE8_RED sentinel and exit 86 (08-09 Task 1 RED
|
||||
# harness self-test; never used outside that proof).
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
APP="$(cd "$ROOT/../fonoteka.go" && pwd)"
|
||||
MCP_ROOT="${MCP_ROOT:-/media/nvme/dev/golem15/fonoteka/fonoteka-mcp}"
|
||||
NUXT_ROOT="${NUXT_ROOT:-/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app}"
|
||||
|
||||
# Ordered, fail-closed stage names. --contract-self-test asserts every one of
|
||||
# these appears, in this order, in the script source (08-09-PLAN.md Task 3
|
||||
# acceptance: "required real-MCP lifecycle, replay/revoke, two-repository
|
||||
# vet/test/race, parity, UI, secret-scan, security-review, and
|
||||
# unchanged-client stages in fail-closed order").
|
||||
PHASE8_STAGES=(
|
||||
docker-preflight
|
||||
postgres
|
||||
app-boot
|
||||
real-mcp
|
||||
discovery
|
||||
dcr
|
||||
pkce-authorize
|
||||
jwt-login-consent
|
||||
token
|
||||
tool-call
|
||||
refresh
|
||||
replay
|
||||
revoke
|
||||
post-revoke-failure
|
||||
vet-test-race
|
||||
parity-corpus
|
||||
secret-scan
|
||||
ui-harness
|
||||
unchanged-client-diff
|
||||
security-review
|
||||
)
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
usage:
|
||||
check-phase8.sh run the complete gate (08-10 Task 3 only)
|
||||
check-phase8.sh --contract-self-test syntax/source assertions only
|
||||
check-phase8.sh --red-contract <stage> deliberate RED self-test (08-09 Task 1)
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# --red-contract: a permanent, deliberate self-test hook proving the RED
|
||||
# harness (scripts/check-phase8-red.sh) correctly rejects anything other than
|
||||
# the exact one-stage, one-sentinel, exit-86 shape. It never calls a real
|
||||
# stage function -- it exists purely to anchor 08-09 Task 1's fail-closed
|
||||
# proof and is not part of the executable gate's stage sequence above.
|
||||
# ---------------------------------------------------------------------------
|
||||
run_red_contract() {
|
||||
local stage="$1"
|
||||
local found=false
|
||||
for s in "${PHASE8_STAGES[@]}"; do
|
||||
if [[ "$s" == "$stage" ]]; then
|
||||
found=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "$found" != true ]]; then
|
||||
echo "refuse: --red-contract stage %q is not a declared stage: $stage" >&2
|
||||
exit 2
|
||||
fi
|
||||
echo "PHASE8_STAGE:${stage}:FAIL:PHASE8_RED:real-mcp-stage"
|
||||
exit 86
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# --contract-self-test: source/structure assertions only. No Docker, no
|
||||
# Postgres, no app boot, no Node process, no network beyond loopback binding
|
||||
# checks against the script's own source. Must stay well under 30 seconds
|
||||
# (08-09-PLAN.md Task 3 acceptance).
|
||||
# ---------------------------------------------------------------------------
|
||||
run_contract_self_test() {
|
||||
local self="${BASH_SOURCE[0]}"
|
||||
|
||||
echo "==> bash -n"
|
||||
bash -n "$self"
|
||||
|
||||
echo "==> required stage names present, in declared order"
|
||||
local last_line=0
|
||||
for stage in "${PHASE8_STAGES[@]}"; do
|
||||
local line
|
||||
line="$(grep -n "stage_${stage//-/_}" "$self" | head -1 | cut -d: -f1 || true)"
|
||||
if [[ -z "$line" ]]; then
|
||||
echo "refuse: stage function for '${stage}' not found in source" >&2
|
||||
exit 1
|
||||
fi
|
||||
if (( line < last_line )); then
|
||||
echo "refuse: stage '${stage}' is declared out of order" >&2
|
||||
exit 1
|
||||
fi
|
||||
last_line="$line"
|
||||
done
|
||||
|
||||
echo "==> cleanup trap present"
|
||||
grep -q "^trap cleanup_phase8 EXIT" "$self" || {
|
||||
echo "refuse: missing cleanup trap" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "==> loopback-only service binding"
|
||||
if grep -qE "0\.0\.0\.0|--host[= ]0\.0\.0\.0" "$self"; then
|
||||
echo "refuse: non-loopback bind address found in source" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q "127.0.0.1" "$self" || {
|
||||
echo "refuse: expected loopback address in source" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "==> three MCP environment variables are exported"
|
||||
for var in FONOTEKA_API_URL FONOTEKA_MCP_PUBLIC_URL FONOTEKA_MCP_AUTH_SERVER; do
|
||||
grep -q "$var" "$self" || {
|
||||
echo "refuse: missing $var reference" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
echo "==> redaction helper present (no raw secret/token/code/verifier echoed)"
|
||||
grep -q "redact_phase8" "$self" || {
|
||||
echo "refuse: missing redact_phase8 helper" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "==> no pre-final full-run mode is offered"
|
||||
if grep -qE -- '--pre-security|--pre-final' "$self"; then
|
||||
echo "refuse: a pre-final full-run mode is offered" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> unchanged-client worktrees are only read, never written"
|
||||
grep -q "MCP_ROOT" "$self" || {
|
||||
echo "refuse: missing MCP_ROOT reference" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -q "NUXT_ROOT" "$self" || {
|
||||
echo "refuse: missing NUXT_ROOT reference" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "phase8 contract-self-test passed"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# redact_phase8: strips anything credential-shaped before it reaches stdout.
|
||||
# Every stage function must pipe its own diagnostic output through this
|
||||
# before printing (T-08-REQUEST-LEAK).
|
||||
# ---------------------------------------------------------------------------
|
||||
redact_phase8() {
|
||||
sed -E \
|
||||
-e 's/(client_secret=)[^&[:space:]]+/\1<redacted>/g' \
|
||||
-e 's/(code_verifier=)[^&[:space:]]+/\1<redacted>/g' \
|
||||
-e 's/(refresh_token=)[^&[:space:]]+/\1<redacted>/g' \
|
||||
-e 's/(access_token"?[:=]"?)[A-Za-z0-9_.\-]+/\1<redacted>/g' \
|
||||
-e 's/(Authorization: Bearer )[A-Za-z0-9_.\-]+/\1<redacted>/g' \
|
||||
-e 's/(Authorization: Basic )[A-Za-z0-9+\/=]+/\1<redacted>/g' \
|
||||
-e 's/inv_[A-Za-z0-9_-]{8,}/<redacted-inv>/g'
|
||||
}
|
||||
|
||||
PHASE8_CLEANUP_PIDS=()
|
||||
PHASE8_CLEANUP_DIRS=()
|
||||
|
||||
cleanup_phase8() {
|
||||
local pid
|
||||
for pid in "${PHASE8_CLEANUP_PIDS[@]:-}"; do
|
||||
[[ -n "$pid" ]] || continue
|
||||
kill "$pid" 2>/dev/null || true
|
||||
wait "$pid" 2>/dev/null || true
|
||||
done
|
||||
local dir
|
||||
for dir in "${PHASE8_CLEANUP_DIRS[@]:-}"; do
|
||||
[[ -n "$dir" ]] || continue
|
||||
rm -rf "$dir"
|
||||
done
|
||||
}
|
||||
trap cleanup_phase8 EXIT
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage functions. Each is named stage_<stage-with-underscores> so
|
||||
# --contract-self-test can locate it by source grep, in declared order.
|
||||
# Bodies are completed by 08-09-PLAN.md Task 3; execution is gated to
|
||||
# 08-10 Task 3 only (main() below never runs stages unless invoked with no
|
||||
# flags, which 08-09 never does).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
stage_docker_preflight() {
|
||||
command -v docker >/dev/null 2>&1 || {
|
||||
echo "refuse: docker is required" >&2
|
||||
exit 1
|
||||
}
|
||||
docker info >/dev/null 2>&1 || {
|
||||
echo "refuse: docker daemon is not available" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
stage_postgres() {
|
||||
echo "not yet implemented outside 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
stage_app_boot() {
|
||||
echo "not yet implemented outside 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
stage_real_mcp() {
|
||||
if [[ ! -d "$MCP_ROOT" ]]; then
|
||||
echo "refuse: MCP_ROOT not found: $MCP_ROOT" >&2
|
||||
exit 1
|
||||
fi
|
||||
# FONOTEKA_API_URL, FONOTEKA_MCP_PUBLIC_URL, FONOTEKA_MCP_AUTH_SERVER are
|
||||
# exported here (only into the fonoteka-mcp child process, never into the
|
||||
# gate's own persistent environment) once the app/Postgres stages above
|
||||
# are live; 127.0.0.1-only.
|
||||
echo "not yet implemented outside 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
stage_discovery() {
|
||||
echo "not yet implemented outside 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
stage_dcr() {
|
||||
echo "not yet implemented outside 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
stage_pkce_authorize() {
|
||||
echo "not yet implemented outside 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
stage_jwt_login_consent() {
|
||||
echo "not yet implemented outside 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
stage_token() {
|
||||
echo "not yet implemented outside 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
stage_tool_call() {
|
||||
echo "not yet implemented outside 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
stage_refresh() {
|
||||
echo "not yet implemented outside 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
stage_replay() {
|
||||
echo "not yet implemented outside 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
stage_revoke() {
|
||||
echo "not yet implemented outside 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
stage_post_revoke_failure() {
|
||||
echo "not yet implemented outside 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
stage_vet_test_race() {
|
||||
echo "not yet implemented outside 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
stage_parity_corpus() {
|
||||
echo "not yet implemented outside 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
stage_secret_scan() {
|
||||
echo "not yet implemented outside 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
stage_ui_harness() {
|
||||
if [[ ! -d "$NUXT_ROOT" ]]; then
|
||||
echo "refuse: NUXT_ROOT not found: $NUXT_ROOT" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "not yet implemented outside 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
stage_unchanged_client_diff() {
|
||||
# Fails the gate if either unchanged client worktree (MCP_ROOT/NUXT_ROOT)
|
||||
# gains a Phase 8 source diff -- this repo never edits them.
|
||||
echo "not yet implemented outside 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
stage_security_review() {
|
||||
echo "not yet implemented outside 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
run_full_gate() {
|
||||
echo "refuse: the complete gate runs only from 08-10 Task 3" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
main() {
|
||||
case "${1:-}" in
|
||||
"")
|
||||
run_full_gate
|
||||
;;
|
||||
--contract-self-test)
|
||||
run_contract_self_test
|
||||
;;
|
||||
--red-contract)
|
||||
[[ $# -ge 2 ]] || usage
|
||||
run_red_contract "$2"
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user