fix(08-10): close real defects found by check-phase8.sh's first end-to-end run
08-10 Task 3 is the first time this gate has actually been executed against real Docker/Postgres/the real fonoteka CLI/the real fonoteka-mcp process. Four independent, previously-undetected defects surfaced: - stage_postgres never set POSTGRES_INITDB_ARGS for the ICU pl-PL locale lagoon.Use requires (every other Postgres testcontainer in this project already does); the app failed to boot at all. - stage_app_boot's seed step POSTed to /_fonoteka/api/v1/onboarding/bootstrap, a route routes.go never mounts (its own comment marks that group deliberately empty, pending a later phase). The gate's test user/collection are now seeded directly with SQL, matching every app-level OAuth test's own real-Postgres seeding. - phase8_workdir() assigned PHASE8_WORKDIR from inside a function body that is always invoked via command substitution (a subshell): the assignment never escaped back to the calling shell, so every separate caller (stage_postgres, stage_app_boot, each phase8_mcp_stage call, ...) minted its own fresh mktemp directory. This silently fragmented one run's state (app.log, the MCP client's gate-state.json) across dozens of directories that never saw each other's writes -- the MCP client's dcr stage could never see discovery's saved metadata. PHASE8_WORKDIR is now set once, directly, in run_full_gate before any stage runs. - gate-state.json (the MCP client's shared cross-invocation state) holds raw live secrets by design and is never redacted; stage_secret_scan correctly flagged it. It is now deleted once the MCP lifecycle stages are done with it, before the scan runs -- the scan itself stays exactly as strict as it already was. stage_security_review also now refuses a nonzero threats_open count or a missing required T-08-* row, not just a missing/unverified file, and gains --security-review-only, a focused mode for Task 2's own verify command.
This commit is contained in:
@@ -231,6 +231,10 @@ PHASE8_MCP_PORT="18100"
|
|||||||
PHASE8_MCP_URL="http://127.0.0.1:${PHASE8_MCP_PORT}"
|
PHASE8_MCP_URL="http://127.0.0.1:${PHASE8_MCP_PORT}"
|
||||||
PHASE8_GATE_EMAIL="phase8-gate@parity.test"
|
PHASE8_GATE_EMAIL="phase8-gate@parity.test"
|
||||||
PHASE8_GATE_PASSWORD="phase8-gate-pass"
|
PHASE8_GATE_PASSWORD="phase8-gate-pass"
|
||||||
|
# bcrypt(cost=10) of PHASE8_GATE_PASSWORD, precomputed via
|
||||||
|
# golang.org/x/crypto/bcrypt (bouncer.HashPassword's own algorithm) so the
|
||||||
|
# app-boot seed step below needs no Go toolchain of its own.
|
||||||
|
PHASE8_GATE_PASSWORD_HASH='$2a$10$cwulxI1xSv3pH0AzpT/AquBokra0uv73b41bkpy6jG.le2JOmJHJG'
|
||||||
PHASE8_MCP_CLIENT="$ROOT/scripts/check-phase8-mcp-client.mjs"
|
PHASE8_MCP_CLIENT="$ROOT/scripts/check-phase8-mcp-client.mjs"
|
||||||
|
|
||||||
phase8_workdir() {
|
phase8_workdir() {
|
||||||
@@ -245,8 +249,13 @@ stage_postgres() {
|
|||||||
local dir
|
local dir
|
||||||
dir="$(phase8_workdir)"
|
dir="$(phase8_workdir)"
|
||||||
PHASE8_PG_CONTAINER="phase8-pg-$$"
|
PHASE8_PG_CONTAINER="phase8-pg-$$"
|
||||||
|
# ICU pl-PL locale matches every other Go test's testcontainers Postgres
|
||||||
|
# in this project (e.g. plugins/golem15/fonoteka/updates's own
|
||||||
|
# startOAuthUpdatesPostgres): lagoon.Use refuses any other locale
|
||||||
|
# provider/locale at boot.
|
||||||
docker run -d --rm --name "$PHASE8_PG_CONTAINER" \
|
docker run -d --rm --name "$PHASE8_PG_CONTAINER" \
|
||||||
-e POSTGRES_PASSWORD=phase8 -e POSTGRES_DB=fonoteka_phase8 \
|
-e POSTGRES_PASSWORD=phase8 -e POSTGRES_DB=fonoteka_phase8 \
|
||||||
|
-e POSTGRES_INITDB_ARGS="--locale-provider=icu --icu-locale=pl-PL --encoding=UTF8" \
|
||||||
-p 127.0.0.1::5432 postgres:16-alpine >/dev/null
|
-p 127.0.0.1::5432 postgres:16-alpine >/dev/null
|
||||||
PHASE8_CLEANUP_CONTAINERS+=("$PHASE8_PG_CONTAINER")
|
PHASE8_CLEANUP_CONTAINERS+=("$PHASE8_PG_CONTAINER")
|
||||||
PHASE8_PG_PORT="$(docker port "$PHASE8_PG_CONTAINER" 5432/tcp | tail -1 | cut -d: -f2)"
|
PHASE8_PG_PORT="$(docker port "$PHASE8_PG_CONTAINER" 5432/tcp | tail -1 | cut -d: -f2)"
|
||||||
@@ -303,13 +312,28 @@ stage_app_boot() {
|
|||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
|
||||||
# Seed the gate's own throwaway account via the real onboarding endpoint
|
# Seed the gate's own throwaway account and its default collection
|
||||||
# (matching TestOAuthFlows' seeding, but through HTTP since this stage
|
# directly in the disposable Postgres database, matching TestOAuthFlows'
|
||||||
# drives the real listening app, not an in-process handler).
|
# real-Postgres seeding (../fonoteka.go/parity/oauth_flow_test.go). No
|
||||||
curl -s -X POST "$PHASE8_APP_URL/_fonoteka/api/v1/onboarding/bootstrap" \
|
# /_fonoteka/api/v1/onboarding/bootstrap route is mounted anywhere in
|
||||||
-H "Content-Type: application/json" -H "Accept: application/json" \
|
# routes.go (routes.go's own comment marks that group's builder
|
||||||
-d "{\"org_name\":\"Phase 8 Gate\",\"email\":\"${PHASE8_GATE_EMAIL}\",\"password\":\"${PHASE8_GATE_PASSWORD}\"}" \
|
# deliberately empty, "pending its real handler in a later phase") --
|
||||||
>/dev/null
|
# the account is seeded with SQL instead, exactly like every other
|
||||||
|
# app-level OAuth test in this phase seeds its user/collection.
|
||||||
|
# PHASE8_GATE_PASSWORD_HASH is the fixed bcrypt (cost 10) hash of
|
||||||
|
# PHASE8_GATE_PASSWORD, matching bouncer.HashPassword's own algorithm
|
||||||
|
# (golang.org/x/crypto/bcrypt), precomputed once so this stage needs no
|
||||||
|
# Go toolchain of its own.
|
||||||
|
docker exec -i "$PHASE8_PG_CONTAINER" psql -U postgres -d fonoteka_phase8 -v ON_ERROR_STOP=1 \
|
||||||
|
-v email="$PHASE8_GATE_EMAIL" -v hash="$PHASE8_GATE_PASSWORD_HASH" <<'SQL' >/dev/null
|
||||||
|
WITH new_user AS (
|
||||||
|
INSERT INTO users (email, password, is_activated, activated_at)
|
||||||
|
VALUES (:'email', :'hash', true, NOW())
|
||||||
|
RETURNING id
|
||||||
|
)
|
||||||
|
INSERT INTO golem15_fonoteka_collections (owner_id, name)
|
||||||
|
SELECT id, 'Phase 8 Gate Collection' FROM new_user;
|
||||||
|
SQL
|
||||||
}
|
}
|
||||||
|
|
||||||
stage_real_mcp() {
|
stage_real_mcp() {
|
||||||
@@ -460,6 +484,20 @@ stage_security_review() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
run_full_gate() {
|
run_full_gate() {
|
||||||
|
# Every stage/helper that needs the shared workdir calls
|
||||||
|
# dir="$(phase8_workdir)" -- command substitution, which always forks a
|
||||||
|
# subshell. If PHASE8_WORKDIR were first assigned *inside* that
|
||||||
|
# subshelled function body, the assignment would never escape back to
|
||||||
|
# this (the real, top-level) shell: every separate caller would then
|
||||||
|
# see PHASE8_WORKDIR still empty and mint its own fresh mktemp
|
||||||
|
# directory, silently fragmenting one run's state (app.log, the MCP
|
||||||
|
# client's gate-state.json, etc.) across many directories that never
|
||||||
|
# see each other's writes. Setting it here, directly (not through a
|
||||||
|
# command substitution), makes every later `phase8_workdir()` call see
|
||||||
|
# it already populated and simply echo the same value back.
|
||||||
|
PHASE8_WORKDIR="$(mktemp -d /tmp/summercms-phase8-XXXXXX)"
|
||||||
|
PHASE8_CLEANUP_DIRS+=("$PHASE8_WORKDIR")
|
||||||
|
|
||||||
stage_docker_preflight
|
stage_docker_preflight
|
||||||
stage_postgres
|
stage_postgres
|
||||||
stage_app_boot
|
stage_app_boot
|
||||||
@@ -474,6 +512,16 @@ run_full_gate() {
|
|||||||
stage_replay
|
stage_replay
|
||||||
stage_revoke
|
stage_revoke
|
||||||
stage_post_revoke_failure
|
stage_post_revoke_failure
|
||||||
|
|
||||||
|
# gate-state.json (the MCP client's shared cross-invocation state file)
|
||||||
|
# has done its job: no later stage reads it, and it necessarily holds
|
||||||
|
# raw live secrets (access/refresh tokens, the JWT) the MCP SDK's own
|
||||||
|
# helpers returned during the stages above -- unlike every stage's own
|
||||||
|
# stdout/stderr, it is never piped through redact_phase8. Removing it
|
||||||
|
# now, before stage_secret_scan's directory-wide scan, is the fix (not
|
||||||
|
# loosening that scan): nothing legitimate needs this file anymore.
|
||||||
|
rm -f "$(phase8_workdir)/gate-state.json"
|
||||||
|
|
||||||
stage_vet_test_race
|
stage_vet_test_race
|
||||||
stage_parity_corpus
|
stage_parity_corpus
|
||||||
stage_secret_scan
|
stage_secret_scan
|
||||||
|
|||||||
Reference in New Issue
Block a user