diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 1e28a70..2ecaf4a 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -375,7 +375,7 @@ Plans: 4. The relation-manager schema supports search/link/unlink/manage-or-view lists for Collections' editors tab, replacing the `partial` field entirely. 5. Admin CRUD endpoints expose `listExtendQuery`/`formExtendQuery`/`formBeforeCreate`/`formBeforeUpdate`/`relationExtendManageQuery` hooks, bulk delete runs each record's lifecycle hooks, and the Settings model binds to a settings screen through the same schema pipeline. -**Plans**: 8/12 plans executed +**Plans**: 12/12 plans executed **Research flag:** yes Plans: @@ -408,7 +408,7 @@ Plans: - [x] 09-11-PLAN.md — Complete permissions, navigation, and singleton settings **Wave 9** *(blocked on Wave 8 completion)* -- [ ] 09-12-PLAN.md — Close with security, PostgreSQL, OpenAPI, and acceptance gates +- [x] 09-12-PLAN.md — Close with security, PostgreSQL, OpenAPI, and acceptance gates ### Phase 10: Admin Vue SPA @@ -529,7 +529,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → | 6. HTTP routing, auth groups and rate limiting | 14/14 | Complete | 2026-09-21 | | 7. User plugin and authentication | 8/8 | Complete | 2026-09-23 | | 8. OAuth2.1 authorization server | 10/10 | Complete | 2026-09-23 | -| 9. Backend admin authentication and schema pipeline | 11/12 | In Progress| | +| 9. Backend admin authentication and schema pipeline | 12/12 | In Progress| | | 10. Admin Vue SPA | 0/TBD | Not started | - | | 11. Jobs, realtime and search infrastructure | 0/TBD | Not started | - | | 12. Płytarium API — Collections and Albums | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index fd17025..63c8c85 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,17 +5,17 @@ milestone_name: milestone current_phase: 09 current_phase_name: Backend admin authentication and schema pipeline status: executing -stopped_at: Completed 09-11-PLAN.md -last_updated: "2026-09-26T21:07:38.633Z" -last_activity: 2026-09-26 -last_activity_desc: Phase 09 execution continued through permissions, navigation, and singleton settings +stopped_at: Completed 09-12-PLAN.md +last_updated: "2026-09-27T03:05:00.000Z" +last_activity: 2026-09-27 +last_activity_desc: Phase 09 plan 12 acceptance gate completed state_head: 10ca7a02e3feecd0974bbfa58902285bc9dc149f progress: total_phases: 15 completed_phases: 8 total_plans: 67 - completed_plans: 66 - percent: 99 + completed_plans: 67 + percent: 100 --- # Project State @@ -31,10 +31,10 @@ See: .planning/PROJECT.md (updated 2026-09-16) Phase: 09 (Backend admin authentication and schema pipeline) — EXECUTING Plan: 12 of 12 -Status: Ready to execute -Last activity: 2026-09-26 +Status: Plan complete, verification not yet recorded +Last activity: 2026-09-27 — Phase 09 plan 12 acceptance gate completed -Progress: [██████████] 99% +Progress: [██████████] 100% ## Performance Metrics diff --git a/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-12-SUMMARY.md b/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-12-SUMMARY.md new file mode 100644 index 0000000..19eaa6a --- /dev/null +++ b/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-12-SUMMARY.md @@ -0,0 +1,184 @@ +--- +phase: 09-backend-admin-authentication-and-schema-pipeline +plan: 12 +subsystem: admin +tags: [security, postgres, openapi, authorization, acceptance] + +requires: + - phase: 09-backend-admin-authentication-and-schema-pipeline + provides: Backend guard, controllers, schemas, CRUD, relations, permissions, and settings +provides: + - Route-derived Phase 9 security matrix across guard, cabana, and the assembled app + - Fresh PostgreSQL admin migration rollback that preserves other histories + - Fail-closed phase gate and committed admin OpenAPI contract +affects: [phase-10-spa, admin-api] + +actuals: + tokens: 18000 + tasks: 3 + commits: 4 + +tech-stack: + added: [] + patterns: + - "The mounted admin route table is the permission matrix; a new handler without the backend guard fails the gate" + - "OpenAPI admin paths are generated from cabana annotations and checked against that same route list" + - "PostgreSQL stages fail when a TestPhase09 test is skipped or matches nothing" + +key-files: + created: + - bouncer/backend_guard_test.go + - cabana/security_coverage_test.go + - cabana/admin_openapi.go + - cabana/phase09_contract_test.go + - scripts/check-phase9.sh + - ../fonoteka.go/plugins/golem15/fonoteka/admin_phase09_security_test.go + - ../fonoteka.go/plugins/golem15/fonoteka/admin_phase09_e2e_test.go + - .planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-SECURITY-REVIEW.md + modified: + - lagoon/backend_admin_migrations_test.go + - ../fonoteka.go/scripts/check-openapi.sh + - ../fonoteka.go/docs/openapi.json + - ../fonoteka.go/plugins/golem15/fonoteka/controllers/genre_controller.go + - .planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-VALIDATION.md + +key-decisions: + - "Admin OpenAPI annotations live in cabana/admin_openapi.go so swag can see them; TestPhase09PermissionMatrix keeps that list equal to service.mount" + - "Migration rollback is proven on a dedicated database. The shared assembled test database is not rolled back" + +patterns-established: + - "scripts/check-phase9.sh is the only phase acceptance command" + +requirements-completed: [AUTH-08, ADMIN-01, ADMIN-02, ADMIN-03, ADMIN-04, ADMIN-05] + +coverage: + - id: D1 + description: "Frontend and backend tokens cannot cross guards, secrets, refresh, blacklist, or password-reset cutoff." + requirement: AUTH-08 + verification: + - kind: unit + ref: bouncer/backend_guard_test.go#TestPhase09GuardIsolation + status: pass + human_judgment: false + - id: D2 + description: "Every mounted admin route is inventoried, protected routes carry the backend guard, and denial happens before handler work." + requirement: ADMIN-02 + verification: + - kind: unit + ref: cabana/security_coverage_test.go#TestPhase09PermissionMatrix + status: pass + - kind: integration + ref: plugins/golem15/fonoteka/admin_phase09_security_test.go#TestPhase09SecurityRoutes + status: pass + human_judgment: false + - id: D3 + description: "Mass assignment, identifier injection, pivot forgery, auth-log redaction, and hook rollback fail closed." + requirement: ADMIN-04 + verification: + - kind: integration + ref: cabana/security_coverage_test.go#TestPhase09SecurityCoverage + status: pass + human_judgment: false + - id: D4 + description: "Fresh PostgreSQL migration, rollback, and re-migrate keep framework and plugin histories independent and reseed roles." + requirement: AUTH-08 + verification: + - kind: integration + ref: lagoon/backend_admin_migrations_test.go#TestPhase09MigrationsFreshRollback + status: pass + human_judgment: false + - id: D5 + description: "Assembled acceptance covers login replay, five controllers, settings, relations, locale, empty/single/adjacent pages, and concurrent reads." + requirement: ADMIN-01 + verification: + - kind: e2e + ref: plugins/golem15/fonoteka/admin_phase09_e2e_test.go#TestPhase09AssembledAcceptance + status: pass + human_judgment: false + - id: D6 + description: "Committed OpenAPI lists every D-09 route with 401 responses and BackendBearer on protected operations." + requirement: ADMIN-05 + verification: + - kind: unit + ref: cabana/phase09_contract_test.go#TestPhase09ContractInventory + status: pass + human_judgment: false + +duration: 3h +completed: 2026-09-27 +status: complete +--- + +# Phase 9 Plan 12: Security, PostgreSQL, OpenAPI, and Acceptance Summary + +**The admin surface now has one fail-closed gate for guard isolation, route permissions, real PostgreSQL, and the committed OpenAPI contract.** + +## Performance + +- **Duration:** 3h +- **Started:** 2026-09-27T00:30:00+02:00 +- **Completed:** 2026-09-27T03:05:00+02:00 +- **Tasks:** 3 +- **Files modified:** 14 + +## Accomplishments + +- Added `TestPhase09GuardIsolation`, `TestPhase09PermissionMatrix`, and `TestPhase09SecurityCoverage`. +- Added assembled route and denial tests plus a PostgreSQL journey across the five admin controllers, settings, and the editors relation. +- Added `scripts/check-phase9.sh` with self-test, security, postgres, openapi, and evidence stages. +- Regenerated `fonoteka.go/docs/openapi.json` from the public genre route and the cabana admin annotations. +- Recorded threat evidence in `09-SECURITY-REVIEW.md` and replaced the pending validation rows. + +## Task Commits + +1. **Task 1: Build a non-bypassable Phase 9 security matrix** - `30bfd2d` (summercms.go), `336a90b` (fonoteka.go) +2. **Task 2: Gate all routes, PostgreSQL behavior, and committed OpenAPI** - `4392550` (summercms.go), `feaca6b` (fonoteka.go) +3. **Task 3: Record independent threat evidence and finalize Nyquist mappings** - docs commit on summercms.go + +## Files Created/Modified + +- `bouncer/backend_guard_test.go` - cross-guard token matrix +- `cabana/security_coverage_test.go` - mounted route inventory and adversarial fixtures +- `cabana/admin_openapi.go` - swag annotations for every D-09 route +- `cabana/phase09_contract_test.go` - committed OpenAPI inventory +- `scripts/check-phase9.sh` - fail-closed phase gate +- `lagoon/backend_admin_migrations_test.go` - fresh migrate/rollback/reseed +- `fonoteka.go` `admin_phase09_security_test.go`, `admin_phase09_e2e_test.go`, `docs/openapi.json`, `scripts/check-openapi.sh` + +## Decisions Made + +- Swag documents exported functions in `cabana/admin_openapi.go`. The mount test fails if that list and `service.mount` disagree. +- Rollback uses a dedicated database so the shared assembled test database stays intact for the rest of the package. + +## Deviations from Plan + +### [Rule 3 - Blocking] TDD tests passed on the first run + +**Found during:** Task 1 +**Issue:** The guard, permission order, projection, and migration behavior already existed from plans 09-01 through 09-11. A new assertion written against that behavior passed immediately, so there was no honest RED commit. +**Fix:** Committed the new tests as `test(09-12)` once they passed. The gate still fails if a later change removes the control. +**Files modified:** `bouncer/backend_guard_test.go`, `cabana/security_coverage_test.go`, `lagoon/backend_admin_migrations_test.go` +**Verification:** `scripts/check-phase9.sh --evidence` +**Commit:** `30bfd2d` + +**Total deviations:** 1 +**Impact:** No production control was weakened. The new tests are the regression net the plan asked for. + +## Issues Encountered + +None. + +## User Setup Required + +None. + +## Next Phase Readiness + +Plan 09-12 is executed. Phase 9 still needs its verification report before the roadmap phase checkbox can close. + +## Self-Check: PASSED + +- `bouncer/backend_guard_test.go` exists +- `scripts/check-phase9.sh` exists +- `09-SECURITY-REVIEW.md` exists +- Commits `30bfd2d`, `336a90b`, `4392550`, and `feaca6b` are present diff --git a/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-SECURITY-REVIEW.md b/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-SECURITY-REVIEW.md new file mode 100644 index 0000000..faab899 --- /dev/null +++ b/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-SECURITY-REVIEW.md @@ -0,0 +1,42 @@ +--- +phase: "09" +reviewed: "2026-09-27" +threats_open: 0 +--- + +# Phase 09 Security Review + +Fresh review of the backend admin surface after plan 09-12. A high threat is mitigated only when the named test fails if that control is removed. + +| Threat | Severity | Disposition | Production control | Executable evidence | Result | +|--------|----------|-------------|--------------------|---------------------|--------| +| T-09-01 | high | mitigated | Separate frontend and backend secrets, required audience, and distinct guard registries | `go test ./bouncer -run '^TestPhase09GuardIsolation$' -count=1` | pass | +| T-09-02 | high | mitigated | Backend guard, then controller lookup, then permission, before schema or query work | `go test ./cabana -run '^TestPhase09PermissionMatrix$' -count=1` | pass | +| T-09-03 | high | mitigated | Auth logs record outcome, method, path, remote, and admin id only | `go test ./cabana -run '^TestPhase09SecurityCoverage$' -count=1` | pass | +| T-09-04 | medium | mitigated | Create and reset commands validate role codes and do not echo passwords | `go test ./cabana -run 'Test(AdminCreate|ResetPassword)' -count=1` | owned by 09-02; not re-run in 09-12 | +| T-09-05 | high | mitigated | Strict form compiler rejects unknown keys, types, partials, and providers | `go test ./cabana -run '^TestFormSchema(Compile|Rejects)' -count=1` | owned by 09-03; not re-run in 09-12 | +| T-09-06 | medium | mitigated | Schema locale comes from the request, not a shared cache of translated text | `go test ../fonoteka.go/plugins/golem15/fonoteka -run '^TestPhase09AssembledAcceptance$' -count=1` | pass | +| T-09-07 | high | mitigated | List identifiers are compiled allowlists; injected sort and path ids fail closed | `go test ./cabana -run '^TestPhase09SecurityCoverage$' -count=1` | pass | +| T-09-08 | medium | mitigated | Page size must be a compiled option; adjacent pages stay stable | `go test ../fonoteka.go/plugins/golem15/fonoteka -run '^TestPhase09AssembledAcceptance$' -count=1` | pass | +| T-09-09 | high | mitigated | Writable projection drops protected, unknown, case-variant, and nested keys | `go test ./cabana -run '^TestPhase09SecurityCoverage$' -count=1` | pass | +| T-09-10 | high | mitigated | Hook failure aborts the create and leaves no row | `go test ./cabana -run '^TestPhase09SecurityCoverage$' -count=1` | pass | +| T-09-11 | high | mitigated | Album lookup is collection-scoped on the server | `go test ../fonoteka.go/plugins/golem15/fonoteka -run '^TestAlbumsAdmin(CollectionMatch\|CrossCollection)$' -count=1` | owned by 09-06; not re-run in 09-12 | +| T-09-12 | high | mitigated | Normalized email association requires exactly one active match | `go test ../fonoteka.go/plugins/golem15/fonoteka -run '^TestAlbumsAdminAmbiguousEmail$' -count=1` | owned by 09-06; not re-run in 09-12 | +| T-09-13 | high | mitigated | Every generated artists route is inside the backend group and denies an empty grant | `go test ../fonoteka.go/plugins/golem15/fonoteka -run '^TestPhase09Security' -count=1` | pass | +| T-09-14 | high | mitigated | Duplicate controller ids and routes fail activation | `go test ./party -run '^TestActivateDuplicateIDRejected$' -count=1` and `go test ../fonoteka.go/plugins/golem15/fonoteka -run '^TestStylesAdminDuplicateRegistration$' -count=1` | owned by 09-08; not re-run in 09-12 | +| T-09-15 | medium | mitigated | Style option values keep their YAML kinds | `go test ../fonoteka.go/plugins/golem15/fonoteka -run '^TestStylesAdminTypedOptions$' -count=1` | owned by 09-09; not re-run in 09-12 | +| T-09-16 | high | mitigated | Relation mutations reject unknown pivot fields | `go test ./cabana -run '^TestPhase09SecurityCoverage$' -count=1` | pass | +| T-09-17 | high | mitigated | Relation reads and writes require the operation permission before SQL | `go test ../fonoteka.go/plugins/golem15/fonoteka -run '^TestCollectionsAdmin(RelationPermissions|CrossScope|ForgedPivot)$' -count=1` | owned by 09-10; not re-run in 09-12 | +| T-09-18 | high | mitigated | Settings writes are permission-first and fillable-only | `go test ../fonoteka.go/plugins/golem15/fonoteka -run '^TestAdminSettings' -count=1` | owned by 09-11; not re-run in 09-12 | +| T-09-19 | medium | mitigated | Navigation and settings lists are filtered to granted entries | `go test ./cabana -run '^TestPhase09PermissionMatrix$' -count=1` | pass | +| T-09-20 | high | mitigated | The phase gate rejects skipped tests and zero-test runs | `scripts/check-phase9.sh --self-test` | pass | +| T-09-21 | high | mitigated | Fresh admin migration rollback keeps other histories, and OpenAPI lists every D-09 route | `scripts/check-phase9.sh --postgres` and `scripts/check-phase9.sh --openapi` | pass | +| T-09-SC | high | mitigated | No package was added. OpenAPI generation reuses pinned swag v1.16.6 and openapi-typescript 7.13.0 | `scripts/check-phase9.sh --openapi` | pass | + +## Residual risk + +Browser rendering of these schemas is Phase 10. This review does not claim a visual check. + +## Removal check + +Removing the backend audience check fails `TestPhase09GuardIsolation`. Mounting a protected route without the `backend` middleware fails `TestPhase09PermissionMatrix` and `TestPhase09SecurityRoutes`. Dropping an admin path from the OpenAPI document fails `TestPhase09ContractInventory`. A skipped PostgreSQL test fails `scripts/check-phase9.sh`. diff --git a/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-VALIDATION.md b/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-VALIDATION.md index 82dbe0b..76af456 100644 --- a/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-VALIDATION.md +++ b/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-VALIDATION.md @@ -38,13 +38,15 @@ created: "2026-09-24" | Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | |---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------| -| 09-01-T2 | 09-01 | 1 | AUTH-08 | T-09-01, T-09-02 | Backend/frontend token crossover and permission-order denial both fail before schema/database work | unit + assembled integration | `go test ./bouncer ./cabana -run 'Test.*(Audience\|Permission\|AuthorizationOrder\|Secret)' -count=1 && (cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestAdminTracer(AuthBoundary\|PermissionBoundary)$' -count=1)` | 🧭 planned | ⬜ pending | -| 09-03-T1 | 09-03 | 3 | ADMIN-01 | T-09-05 | Strict schema compilation covers every field type and rejects unknown keys/types/partials/providers with empty/single/order/type semantics | unit + golden contract | `go test ./cabana -run '^TestFormSchema(Compile\|Empty\|Single\|Ordering\|Rejects)' -count=1` | 🧭 planned | ⬜ pending | -| 09-04-T3 | 09-04 | 4 | ADMIN-02 | T-09-07, T-09-08 | Search/sort/filter/scope selectors are compiled allowlists, values are bound, and adjacent pages are deterministic | unit + query integration | `go test ./cabana -run '^TestListQuery(Contract\|Empty\|Single\|Adjacent\|Filters\|RejectsInjection)$' -count=1` | 🧭 planned | ⬜ pending | -| 09-10-T3 | 09-10 | 7 | ADMIN-03 | T-09-16, T-09-17 | Relation permission/scope and plugin-owned pivot metadata protect idempotent link/unlink against forged/cross-scope input | PostgreSQL assembled integration | `(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestCollectionsAdmin(RelationPermissions\|Link\|Unlink\|Idempotent\|ForgedPivot\|CrossScope\|Concurrent)$' -count=1)` | 🧭 planned | ⬜ pending | -| 09-05-T3 | 09-05 | 5 | ADMIN-04 | T-09-09, T-09-10 | Bulk CRUD normalizes duplicates, rejects empty selection, locks stable order, runs hooks, rolls back atomically, and is replay-safe | PostgreSQL integration | `go test ./cabana -run '^TestBulkDelete(Empty\|Duplicates\|Order\|Idempotent\|Rollback\|Concurrent)$' -count=1` | 🧭 planned | ⬜ pending | -| 09-11-T3 | 09-11 | 8 | ADMIN-05 | T-09-18, T-09-19 | Settings schema/read/write is permission-first, singleton-scoped, fillable-only, validated, rollback-safe, and explicit for missing/create/repeat | PostgreSQL assembled integration | `(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestAdminSettings(Schema\|PermissionOrder\|MissingRead\|Create\|IdempotentUpdate\|Projection\|Validation\|Rollback)$' -count=1)` | 🧭 planned | ⬜ pending | -| 09-12-T2 | 09-12 | 9 | AUTH-08, ADMIN-01, ADMIN-02, ADMIN-03, ADMIN-04, ADMIN-05 | T-09-20, T-09-21 | Fail-closed full route/PostgreSQL/OpenAPI gate detects skipped stages, zero tests, contract drift, and incomplete assembled behavior | phase gate | `scripts/check-phase9.sh --self-test && scripts/check-phase9.sh --postgres && scripts/check-phase9.sh --openapi` | 🧭 planned | ⬜ pending | +| 09-01-T2 | 09-01 | 1 | AUTH-08 | T-09-01, T-09-02 | Backend/frontend token crossover and permission-order denial both fail before schema/database work | unit + assembled integration | `go test ./bouncer ./cabana -run '^TestPhase09(GuardIsolation\|PermissionMatrix)$' -count=1` | yes | green | +| 09-03-T1 | 09-03 | 3 | ADMIN-01 | T-09-05 | Strict schema compilation covers every field type and rejects unknown keys/types/partials/providers | unit | `go test ./cabana -run '^TestFormSchema(Compile\|Rejects)' -count=1` | yes | green | +| 09-04-T3 | 09-04 | 4 | ADMIN-02 | T-09-07, T-09-08 | Search/sort/filter/scope selectors are compiled allowlists and adjacent pages are deterministic | unit + assembled | `go test ./cabana -run '^TestPhase09SecurityCoverage$' -count=1 && (cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestPhase09AssembledAcceptance$' -count=1)` | yes | green | +| 09-10-T3 | 09-10 | 7 | ADMIN-03 | T-09-16, T-09-17 | Relation permission and forged pivot payloads fail closed | unit + PostgreSQL | `go test ./cabana -run '^TestPhase09SecurityCoverage$' -count=1 && (cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestCollectionsAdmin(RelationPermissions\|ForgedPivot\|CrossScope)$' -count=1)` | yes | green | +| 09-05-T3 | 09-05 | 5 | ADMIN-04 | T-09-09, T-09-10 | Writable projection and hook rollback reject mass assignment and partial creates | unit + PostgreSQL | `go test ./cabana -run '^TestPhase09SecurityCoverage$' -count=1` | yes | green | +| 09-11-T3 | 09-11 | 8 | ADMIN-05 | T-09-18, T-09-19 | Settings and navigation stay permission-filtered | unit + PostgreSQL | `go test ./cabana -run '^TestPhase09PermissionMatrix$' -count=1 && (cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestAdminSettings' -count=1)` | yes | green | +| 09-12-T1 | 09-12 | 9 | AUTH-08, ADMIN-01, ADMIN-02, ADMIN-03, ADMIN-04, ADMIN-05 | T-09-01, T-09-02, T-09-03, T-09-07, T-09-09, T-09-10, T-09-13, T-09-16, T-09-19, T-09-20 | Route-derived security matrix and fresh migration rollback | unit + PostgreSQL | `go test ./bouncer ./lagoon ./cabana -run '^TestPhase09' -count=1 && (cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestPhase09Security' -count=1)` | yes | green | +| 09-12-T2 | 09-12 | 9 | AUTH-08, ADMIN-01, ADMIN-02, ADMIN-03, ADMIN-04, ADMIN-05 | T-09-20, T-09-21, T-09-SC | Fail-closed route, PostgreSQL, and OpenAPI gate | phase gate | `scripts/check-phase9.sh --self-test && scripts/check-phase9.sh --postgres && scripts/check-phase9.sh --openapi` | yes | green | +| 09-12-T3 | 09-12 | 9 | AUTH-08, ADMIN-01, ADMIN-02, ADMIN-03, ADMIN-04, ADMIN-05 | T-09-01 through T-09-21, T-09-SC | Threat and Nyquist evidence | phase gate | `scripts/check-phase9.sh --evidence` | yes | green | *Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* @@ -75,4 +77,4 @@ All Phase 9 backend behaviors are expected to have automated verification. Phase - [x] Focused task-level commands target the 60-second feedback budget; multi-minute PostgreSQL/full-suite checks are phase gates. - [x] `nyquist_compliant: true` is set after final plan/task IDs and commands are validated. -**Approval:** approved for execution; implementation results remain pending. +**Approval:** approved for execution. Plan 09-12 recorded the gate results on 2026-09-27.