Files
summercms/.planning/phases/08-oauth2-1-authorization-server/08-09-SUMMARY.md
2026-09-23 23:23:12 +02:00

27 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
08-oauth2-1-authorization-server 09 auth
oauth2
parity
tide
wristband
mcp
playwright-free-recording
gate
phase plan provides
08-oauth2-1-authorization-server 08 GET /api/v1/fonoteka/me prerequisite the real MCP gate needs, and the complete authorize/token/consent/register/revoke/refresh-rotation surface from 08-01..08-07
fixtures/mcp/mcp-lifecycle.yaml: a 17-step, Go-recorded (no Playwright) lifecycle fixture covering DCR, authorize, consent, token, refresh, replay-kill, connected-apps list/revoke, post-revoke failure, deny, and a confidential client_secret_basic client exercising scope-ceiling truncation and invalid_scope
TestOAuthFlows (parity/oauth_flow_test.go): replays that fixture byte-for-byte against the assembled Go app on real Postgres, plus named projections of mcp-oauth/mcp-tools that fail closed if a required step disappears
Nine OAuth parity/manifest.yaml route entries (4 raw + 5 JWT-group) flipped pending -> ported, each replaying their own single-case fixture cleanly
wristband byte-contract fixes: Cache-Control no-store/no-cache both gain the Laravel-session ', private' suffix live PHP actually sends; every wristband redirect now emits Symfony's exact HTML redirect body (wristband/redirect_html.go)
tide.isIDKey now also masks '_ids' plural array fields (e.g. collection_ids), closing a parity-corpus normalization gap
scripts/check-phase8.sh: the complete fail-closed Phase 8 final gate (disposable Postgres, assembled app, real fonoteka-mcp, full scripted SDK lifecycle via the new check-phase8-mcp-client.mjs driver, both repos' vet/test/race, parity/corpus/secret-scan, UI harness, unchanged-client diff, security-review gate) plus --contract-self-test and the permanent --red-contract self-test
08-10-unit-tests-and-security-review
added patterns
Lifecycle fixtures for multi-step stateful flows (PKCE, rotation, revoke) are recorded directly via tide.RecordFlow against isolated PHP from a small one-time Go program, not via Playwright/capture_clients.mjs -- login and consent are plain JWT-authenticated JSON calls with no browser dependency, and PKCE verifier/challenge pairs are generated and pre-seeded into the vars store before recording.
scripts/check-phase8-mcp-client.mjs: a single stateful Node driver, invoked once per named stage with a shared JSON state file, resolves the MCP SDK's auth helpers from fonoteka-mcp's own node_modules (same resolution pattern as capture_clients.mjs) so the framework gate never adds a Node dependency of its own.
Winter's generic production 500 HTML page (already embedded once in the user plugin) is now also embedded directly in fonoteka's controllers/api package for the one route (OAuthConsentController::store) whose bare Laravel validate() call crashes to that page instead of a clean JSON 422 -- duplicated per plugin rather than shared cross-plugin, preserving plugin independence.
created modified
../fonoteka.go/parity/fixtures/mcp/mcp-lifecycle.yaml
../fonoteka.go/parity/oauth_flow_test.go
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/winter_error_page.html
wristband/redirect_html.go
scripts/check-phase8.sh
scripts/check-phase8-mcp-client.mjs
../fonoteka.go/parity/manifest.yaml
../fonoteka.go/parity/migrate_test.go
../fonoteka.go/parity/parity_contract_test.go
../fonoteka.go/parity/parity_test.go
../fonoteka.go/parity/fixtures/routes/GET___fonoteka_api_v1_oauth_connected-apps_jwt.yaml
../fonoteka.go/parity/fixtures/routes/POST___fonoteka_api_v1_oauth_consent_jwt.yaml
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go
../fonoteka.go/plugins/golem15/fonoteka/routes.go
../fonoteka.go/plugins/golem15/fonoteka/oauth_authorize_test.go
../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go
wristband/authorize.go
wristband/register.go
wristband/token.go
wristband/authorize_test.go
wristband/registration_test.go
wristband/token_test.go
tide/normalize.go
Recorded the lifecycle fixture with a one-time Go program calling tide.RecordFlow directly (deleted after use, never committed) rather than extending capture_clients.mjs's Playwright-driven flow -- D-16's own scope only requires the Phase 2 tide tooling, and login/consent are plain JWT API calls with no UI dependency, so a Go recorder is simpler and more robust than browser automation. Documented here as a deviation from the plan's literal capture_clients.mjs file-list entry.
list runs before refresh/replay in the recorded lifecycle (not after, as D-16's prose ordering suggests): PHP's RevokeLineage walks forward from a replayed spent refresh row and revokes every descendant's access token too, including the then-current terminal row, so connected-apps would already be empty if list ran after replay. Revoke's own DELETE still succeeds afterward regardless (PHP's destroy() query has no revoked_at filter), so revoke and refresh-after-revoke stay after replay.
wristband's 'no-store' Cache-Control values become 'no-store, private', and its unheadered JSON error responses (invalid_client, invalid_grant, etc.) gain 'no-cache, private' -- both confirmed by live-recording against real isolated PHP, not assumed from source reading. The earlier 08-CONTEXT.md <specifics> wording ('Cache-Control: no-store') is superseded by this live-recorded byte contract.
{request_id}'s router-level [A-Za-z0-9_-]{16,128} constraint (08-UI-SPEC.md) is now upper-bound only ({1,128}): real PHP applies no router-level shape constraint at all and returns the controller's clean 404 JSON for any non-matching string, including a short/malformed one; the 16-char lower bound was silently rejecting that case at the router with a bare text/plain 404.
OAuthConsentController's basic-validation failure (missing/invalid request_id or scopes) now writes Winter's generic production 500 HTML page instead of a clean 422: PHP's bare $request->validate() on this specific route is never caught by a JSON exception renderer, confirmed live against isolated PHP with APP_DEBUG=false. Domain-level checks (Request not found, No grantable scopes) are unaffected and keep their existing clean JSON responses.
Two pre-existing Phase 2 single-case OAuth fixtures were stale, not stale-fixture-but-correct: the consent 500 page had been recorded with APP_DEBUG=true (a full debug stack trace) and the connected-apps manual_tokens_count had been over-scrubbed to a coincidental {{id:alice}} placeholder. Both were re-recorded against live PHP rather than reverse-engineered from the old bytes.
All nine newly-ported OAuth manifest routes gained seed_hook: genres (matching the corpus's existing convention) because none of them can fall through to the manifest's global onboarding-bootstrap seed, which no Go route currently implements.
tide.isIDKey now masks '_ids' plural array fields the same way it already masks singular '_id'/'id' fields -- no prior fixture in the corpus had exercised a literal, non-empty, non-placeholder array-of-ids value until this plan's connected-apps collection_ids field.
scripts/check-phase8.sh's stage bodies are fully implemented (not stubs) as required by Task 3's 'finish the executable final gate', but this plan never executes run_full_gate end to end -- only --contract-self-test and --red-contract, exactly as the plan's own verification section specifies. 08-10 Task 3 is the first and sole real execution.
AUTH-05/AUTH-06/AUTH-07 remain Pending in REQUIREMENTS.md: this plan builds and structurally self-validates the real-MCP gate machinery (D-14) but does not execute it against the real unchanged fonoteka-mcp process, which is the only thing that can actually prove those requirements' 'unchanged fonoteka-mcp completes its install/auth flow' clause. That execution is 08-10 Task 3's job.
Go-recorded (Playwright-free) multi-step lifecycle fixtures for JWT-authenticated flows: generate PKCE pairs and any CLI-issued credentials up front, pre-seed a private tide.Store, define each step's Capture rules explicitly to avoid capture-rules.yaml route-match surprises, and call tide.RecordFlow directly against isolated PHP.
scripts/check-phase8-mcp-client.mjs's --stage/shared-state-file shape is the template for any future gate needing a stateful multi-step real-client script driven from a bash stage sequence.
~55min 2026-09-23

Phase 08 Plan 09: Parity and Real-MCP Gate Summary

A Go-recorded (no Playwright) 17-step mcp-lifecycle fixture replays byte-for-byte against the assembled Go app, flipping all nine OAuth manifest routes to ported after fixing three genuine wristband/routing byte-contract bugs the live recording uncovered, and the complete scripted-SDK scripts/check-phase8.sh final gate is built and self-validated (never executed) for 08-10.

Performance

  • Duration: ~55 min
  • Started: ~2026-09-23T20:23:00Z (approx., following 08-08's completion)
  • Completed: 2026-09-23T21:18:44Z
  • Tasks: 3 completed (4 commits: RED x2 in Task 1, GREEN x1 in Task 2, GREEN x1 in Task 3)
  • Files modified: 25 (7 created, 18 modified, across both repos)

Accomplishments

  • Recorded fixtures/mcp/mcp-lifecycle.yaml against real isolated PHP with a one-time Go program calling tide.RecordFlow directly (deleted after use): DCR -> authorize -> consent -> token -> connected-apps list (showing the live app) -> refresh -> replay of the now-spent refresh token (invalid_grant, lineage dead) -> revoke -> refresh-after-revoke failure -> a deny path, plus a confidential client_secret_basic client issued via fonoteka:oauth-client's exact issuance path exercising scope-ceiling truncation (read write ai -> read write) and the invalid_scope redirect (ai alone, fully outside the ceiling).
  • TestOAuthFlows replays that fixture byte-for-byte against the real assembled Go app on testcontainers Postgres, and re-asserts named projections of the existing mcp-oauth/mcp-tools fixtures fail closed if a required step disappears.
  • The live recording surfaced three genuine, previously-undetected byte-contract gaps between wristband's assumed behavior and real PHP: every explicit Cache-Control: no-store PHP sets actually arrives as no-store, private (Laravel's session-cookie default merge), unheadered JSON error responses default to no-cache, private rather than nothing, and every PHP redirect (authorize success and error) carries Symfony's exact HTML redirect body with Content-Type: text/html; charset=utf-8 that Go's bare 302 never sent. All three are now fixed (wristband/redirect_html.go is new) and every affected wristband/app-level unit test assertion was updated to the corrected expected bytes.
  • Two more real bugs surfaced once the nine OAuth routes were exercised for the first time: a router-level {request_id} length constraint rejected a valid PHP 404 test case before it reached the controller (now upper-bound only), and OAuthConsentController::store's basic validation failure needed to crash to Winter's generic production 500 HTML page (reusing the exact byte-identical page the user plugin already embeds) rather than return a clean 422, matching real PHP's uncaught-ValidationException behavior on this specific route.
  • All nine OAuth parity/manifest.yaml entries (4 raw + 5 JWT-group) are status: ported, each gaining seed_hook: genres; TestParityCorpus reports recorded 169/169 passing 31 failing 0 unrecorded 0 pending 138 with zero regressions across both full go test ./... (root + all workspace modules) and the touched -race packages.
  • scripts/check-phase8.sh is the complete fail-closed final gate: every stage from docker-preflight through security-review has a real implementation (disposable Postgres, the built-and-served Go app, the real unchanged fonoteka-mcp process, the full scripted SDK lifecycle delegated to the new scripts/check-phase8-mcp-client.mjs, both repositories' vet/test/-race, the parity/corpus/secret-scan gate, the existing check-phase8-ui.mjs --final-gate UI harness, an unchanged-client git-diff check, and a 08-SECURITY-REVIEW.md status: verified gate). --contract-self-test validates structure only (stage names/order, cleanup trap, loopback-only binding, the three MCP env vars, the redaction helper, no pre-final full-run flag) in ~85ms with no services booted; the permanent --red-contract self-test from Task 1 still passes unchanged. run_full_gate is never invoked by this plan.

Task Commits

  1. Task 1: RED parity-gate anchor (both repos)
    • d9b168f (test, fonoteka.go): TestPhase8RedParityGate fails closed with PHASE8_RED:parity-gate while mcp-lifecycle.yaml doesn't exist yet; verified via scripts/check-phase8-red.sh go mode.
    • 246a488 (test, summercms.go): scripts/check-phase8.sh skeleton with the ordered stage list and the permanent --red-contract self-test; verified via scripts/check-phase8-red.sh shell mode (exit 86, exact PHASE8_STAGE:real-mcp:FAIL:PHASE8_RED:real-mcp-stage line).
  2. Task 2: record and replay the full lifecycle (both repos)
    • 6cc07a4 (fix, summercms.go): the three wristband byte-contract fixes (redirect_html.go, Cache-Control corrections) plus the tide.isIDKey _ids masking fix, all confirmed by the live recording.
    • 23e7885 (feat, fonoteka.go): the recorded fixture, TestOAuthFlows, the nine manifest flips, the two re-recorded stale fixtures, the OAuthConsentController/routes.go fixes, and the parity_test.go/parity_contract_test.go count updates.
  3. Task 3: complete the final gate (summercms.go)
    • e87346f (feat): all scripts/check-phase8.sh stage bodies plus scripts/check-phase8-mcp-client.mjs.

Plan metadata: committed as part of this summary/state-update commit.

Note: Task 1 and Task 2 both carry tdd="true"; RED/GREEN pairs land as separate commits, split per repo. Task 3 (type="auto", no tdd) is a single commit.

Files Created/Modified

  • ../fonoteka.go/parity/fixtures/mcp/mcp-lifecycle.yaml -- the 17-step recorded lifecycle fixture
  • ../fonoteka.go/parity/oauth_flow_test.go -- TestPhase8RedParityGate, TestOAuthFlows, projection helpers, issueConfidentialClient, pkcePair, upsertParityDefaultCollection
  • ../fonoteka.go/parity/manifest.yaml -- nine OAuth route entries status: ported + seed_hook: genres
  • ../fonoteka.go/parity/migrate_test.go -- testConfig now sets app.url to match isolated PHP's fixed origin
  • ../fonoteka.go/parity/parity_test.go / parity_contract_test.go -- expectedPortedRoutes 22 -> 31 and the ported-route allow-list
  • ../fonoteka.go/parity/fixtures/routes/GET___fonoteka_api_v1_oauth_connected-apps_jwt.yaml / POST___fonoteka_api_v1_oauth_consent_jwt.yaml -- re-recorded against live PHP
  • ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go + new winter_error_page.html -- basic-validation-failure now matches PHP's crash-to-500 behavior
  • ../fonoteka.go/plugins/golem15/fonoteka/routes.go -- {request_id} constraint is upper-bound only
  • ../fonoteka.go/plugins/golem15/fonoteka/oauth_authorize_test.go / oauth_registration_test.go -- updated Cache-Control expectations
  • wristband/redirect_html.go -- Symfony-exact HTML redirect body + PHP htmlspecialchars(ENT_QUOTES) port
  • wristband/authorize.go / register.go / token.go + their _test.go files -- corrected Cache-Control byte contract
  • tide/normalize.go -- isIDKey masks _ids plural arrays
  • scripts/check-phase8.sh -- the complete final gate script
  • scripts/check-phase8-mcp-client.mjs -- the stateful scripted-SDK MCP client driver

Decisions Made

See frontmatter key-decisions. Most load-bearing: the lifecycle fixture was recorded via a one-time Go program (not Playwright/capture_clients.mjs), and every one of the five byte-contract/routing bugs this plan fixed was confirmed against real live PHP output before being fixed in Go -- none were guessed from source reading alone.

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] wristband Cache-Control values didn't match live PHP

  • Found during: Task 2, first TestOAuthFlows replay attempt
  • Issue: wristband set bare Cache-Control: no-store and left unheadered JSON errors with no Cache-Control at all; real PHP (confirmed via the live recording and cross-checked against Phase-2-recorded single-case fixtures already in git) sends no-store, private and no-cache, private respectively.
  • Fix: authorize.go, register.go, token.go updated; every affected wristband and app-level unit test assertion updated to match.
  • Files modified: wristband/authorize.go, wristband/register.go, wristband/token.go, wristband/authorize_test.go, wristband/registration_test.go, wristband/token_test.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_authorize_test.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go
  • Verification: go test ./wristband/... -count=1, go test ./plugins/golem15/fonoteka -count=1, TestOAuthFlows
  • Committed in: 6cc07a4

2. [Rule 1 - Bug] wristband redirects never carried PHP's HTML body

  • Found during: Task 2, same replay
  • Issue: PHP's redirect()->away(...) renders Symfony's default HTML redirect body (Content-Type: text/html; charset=utf-8, a fixed template with the target URL HTML-escaped four times); Go's bare 302 had no body and no Content-Type.
  • Fix: New wristband/redirect_html.go ports the exact byte template and PHP's htmlspecialchars(ENT_QUOTES) escaping; authorize.go's success and error-redirect paths now call it.
  • Files modified: wristband/redirect_html.go (new), wristband/authorize.go
  • Verification: TestOAuthFlows byte-for-byte body/header comparison
  • Committed in: 6cc07a4

3. [Rule 1 - Bug] tide's id-masking missed plural _ids array fields

  • Found during: Task 2, TestOAuthFlows full-package run (collection_ids[0]: expected 1 actual 8)
  • Issue: isIDKey matched _id and id but not the plural _ids suffix, so a literal collection_ids array value was compared byte-for-byte instead of being structurally masked -- no prior fixture in the corpus had exercised this with a non-empty, non-placeholder value.
  • Fix: isIDKey now also matches _ids; each array element still reaches the existing per-element masking path.
  • Files modified: tide/normalize.go
  • Verification: go test ./tide/... -count=1, TestOAuthFlows
  • Committed in: 6cc07a4

4. [Rule 1 - Bug] {request_id}'s router constraint rejected a valid PHP 404 test case

  • Found during: Task 2, TestParityCorpus full run
  • Issue: The 08-UI-SPEC.md-derived [A-Za-z0-9_-]{16,128} constraint rejected the Phase-2-recorded 14-character parity-missing test value at the router (bare text/plain 404) before OAuthConsentController::show could return its real {"error":"Request not found"} 404 JSON, which is what live PHP (no router-level constraint at all) actually returns.
  • Fix: Constraint changed to [A-Za-z0-9_-]{1,128} (upper bound only).
  • Files modified: ../fonoteka.go/plugins/golem15/fonoteka/routes.go
  • Verification: TestParityCorpus/GET___fonoteka_api_v1_oauth_request_{request_id}_jwt
  • Committed in: 23e7885

5. [Rule 1 - Bug] Consent's basic-validation failure returned a clean 422, not PHP's 500

  • Found during: Task 2, TestParityCorpus full run
  • Issue: OAuthConsentController::store's bare $request->validate([...]) is never caught by a JSON exception renderer on this specific route; live PHP (confirmed via curl with APP_DEBUG=false, matching the isolated-PHP convention) crashes to Winter's generic production 500 HTML page. Go returned a clean writeValidation 422.
  • Fix: ConsentStore's basic-validation-failure branch now writes the same byte-identical Winter error page the user plugin already embeds (newly duplicated into controllers/api/winter_error_page.html to preserve plugin independence). Domain-level checks (Request not found, No grantable scopes) are unaffected.
  • Files modified: ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go, winter_error_page.html (new)
  • Verification: TestParityCorpus/POST___fonoteka_api_v1_oauth_consent_jwt
  • Committed in: 23e7885

6. [Rule 1 - Bug] Two pre-existing Phase 2 fixtures were stale

  • Found during: Task 2, same TestParityCorpus run, after fixes 4/5 above
  • Issue: POST .../oauth/consent's case fixture had been recorded with APP_DEBUG=true (a full debug stack trace with incidental scrub collisions in the stack-frame numbers); GET .../connected-apps's case fixture had manual_tokens_count over-scrubbed to a coincidental {{id:alice}} placeholder.
  • Fix: Both re-recorded/hand-corrected against live isolated PHP with the current APP_DEBUG=false convention and the genres seed hook's actual manual-token count (1, not the fresh-user 0 a throwaway curl user showed).
  • Files modified: ../fonoteka.go/parity/fixtures/routes/POST___fonoteka_api_v1_oauth_consent_jwt.yaml, GET___fonoteka_api_v1_oauth_connected-apps_jwt.yaml
  • Verification: TestParityCorpus
  • Committed in: 23e7885

7. [Rule 3 - Blocking] TestOAuthFlows needed app.url and a matching default collection

  • Found during: Task 2, iterative replay debugging
  • Issue: testConfig(t) left app.url empty (breaking every issuer/absolute-URL field) and the genres seed hook's hardcoded "Parity Collection" name didn't match the recorded PHP flow's actual onboarding-auto-created default collection name ("Moja kolekcja").
  • Fix: testConfig now sets app.url to isolated PHP's fixed origin; TestOAuthFlows seeds its own "Moja kolekcja" default collection with no active_collection_context row, letting ActiveCollectionResolver's own fallback resolve it exactly as the recording did.
  • Files modified: ../fonoteka.go/parity/migrate_test.go, ../fonoteka.go/parity/oauth_flow_test.go
  • Verification: TestOAuthFlows
  • Committed in: 23e7885

8. [Rule 3 - Blocking] Shared-pool cross-test pollution between TestOAuthFlows and pre-existing corpus tests

  • Found during: Task 2, full go test ./... (not just TestOAuthFlows in isolation)
  • Issue: TestOAuthFlows's confidential-client token was left live (unrevoked) at test end, inflating connected_apps_count for a sibling TestParityCorpus case that shares the same alice@parity.test identity across the whole package's shared Postgres pool.
  • Fix: TestOAuthFlows now revokes every oauth_client_id-linked token for its alice in t.Cleanup, regardless of pass/fail.
  • Files modified: ../fonoteka.go/parity/oauth_flow_test.go
  • Verification: go test ./parity/... -count=1 (full package, not just the single test)
  • Committed in: 23e7885

9. [Rule 2 - Missing Critical] Nine new manifest routes needed seed_hook: genres

  • Found during: Task 2, first TestParityCorpus run after flipping the nine routes to ported
  • Issue: None of the nine routes has its own seed_hook, so they fell through to the manifest's global onboarding-bootstrap seed, which no Go route currently implements -- every one failed with a 404 on /_fonoteka/api/v1/onboarding/status.
  • Fix: Added seed_hook: genres to all nine, matching the corpus's existing convention for every other ported route.
  • Files modified: ../fonoteka.go/parity/manifest.yaml, ../fonoteka.go/parity/parity_contract_test.go (allow-list)
  • Verification: TestParityCorpus
  • Committed in: 23e7885

10. [Rule 3 - Blocking] parity_contract_test.go's hardcoded counts/allow-list were stale

  • Found during: Task 2, full go test ./...
  • Issue: TestParityContract hardcoded 22 ported/147 pending and an explicit route-id allow-list that didn't include the nine new routes.
  • Fix: Updated to reference expectedPortedRoutes/expectedPHPRoutes and added the nine route ids to the allow-list.
  • Files modified: ../fonoteka.go/parity/parity_contract_test.go
  • Verification: go test ./... -count=1 (fonoteka.go root)
  • Committed in: 23e7885

Total deviations: 10 auto-fixed (6 Rule 1 bug fixes, 1 Rule 2 missing-critical addition, 3 Rule 3 blocking-issue fixes) Impact on plan: All ten were necessary for the plan's own stated goal -- proving exact recorded byte parity -- to actually hold. None were scope creep; each was discovered specifically because this plan is the first to exercise these nine routes and this full lifecycle against the real Go implementation.

Issues Encountered

None beyond the auto-fixed items above. Full go vet/go test ./... (including -race on touched packages) are green in summercms.go and across every fonoteka.go workspace module (root fonoteka/parity, plugins/golem15/fonoteka and all its subpackages, plugins/golem15/user and its subpackages).

User Setup Required

None -- no external service configuration required. scripts/check-phase8.sh's full gate needs Docker and the already-installed Node dependencies in fonoteka-mcp/vue-fonoteka-app, but this plan never invokes it; that's 08-10 Task 3.

Next Phase Readiness

  • 08-10 can now run scripts/check-phase8.sh (no flags) for the first time. The stage bodies are real, complete implementations, but none has been execution-verified end to end in this plan -- 08-10 Task 3 is the first real run and may need to iterate on the app-boot/real-mcp stage details (exact compass config keys, timing) once actually exercised.
  • 08-SECURITY-REVIEW.md does not exist yet; stage_security_review will fail closed until 08-10 creates it with status: verified.
  • AUTH-05/AUTH-06/AUTH-07 remain Pending in REQUIREMENTS.md: this plan proves exact recorded-PHP byte parity for all nine OAuth routes and builds the complete real-MCP gate machinery, but only 08-10's actual execution of that gate can prove the "unchanged fonoteka-mcp completes its install/auth flow" clause those requirements need.
  • No blockers.

Self-Check: PASSED

  • FOUND: ../fonoteka.go/parity/fixtures/mcp/mcp-lifecycle.yaml
  • FOUND: ../fonoteka.go/parity/oauth_flow_test.go
  • FOUND: ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/winter_error_page.html
  • FOUND: wristband/redirect_html.go
  • FOUND: scripts/check-phase8.sh
  • FOUND: scripts/check-phase8-mcp-client.mjs
  • FOUND commits (summercms.go): 246a488, 6cc07a4, e87346f
  • FOUND commits (fonoteka.go): d9b168f, 23e7885

Phase: 08-oauth2-1-authorization-server Completed: 2026-09-23