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 |
|
|
|
|
|
|
|
|
~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.yamlagainst real isolated PHP with a one-time Go program callingtide.RecordFlowdirectly (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 confidentialclient_secret_basicclient issued viafonoteka:oauth-client's exact issuance path exercising scope-ceiling truncation (read write ai->read write) and theinvalid_scoperedirect (aialone, fully outside the ceiling). TestOAuthFlowsreplays that fixture byte-for-byte against the real assembled Go app on testcontainers Postgres, and re-asserts named projections of the existingmcp-oauth/mcp-toolsfixtures 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-storePHP sets actually arrives asno-store, private(Laravel's session-cookie default merge), unheadered JSON error responses default tono-cache, privaterather than nothing, and every PHP redirect (authorize success and error) carries Symfony's exact HTML redirect body withContent-Type: text/html; charset=utf-8that Go's bare 302 never sent. All three are now fixed (wristband/redirect_html.gois new) and every affectedwristband/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), andOAuthConsentController::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-ValidationExceptionbehavior on this specific route. - All nine OAuth
parity/manifest.yamlentries (4 raw + 5 JWT-group) arestatus: ported, each gainingseed_hook: genres;TestParityCorpusreportsrecorded 169/169 passing 31 failing 0 unrecorded 0 pending 138with zero regressions across both fullgo test ./...(root + all workspace modules) and the touched-racepackages. scripts/check-phase8.shis the complete fail-closed final gate: every stage fromdocker-preflightthroughsecurity-reviewhas a real implementation (disposable Postgres, the built-and-served Go app, the real unchangedfonoteka-mcpprocess, the full scripted SDK lifecycle delegated to the newscripts/check-phase8-mcp-client.mjs, both repositories'vet/test/-race, the parity/corpus/secret-scan gate, the existingcheck-phase8-ui.mjs --final-gateUI harness, an unchanged-client git-diff check, and a08-SECURITY-REVIEW.md status: verifiedgate).--contract-self-testvalidates 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-contractself-test from Task 1 still passes unchanged.run_full_gateis never invoked by this plan.
Task Commits
- Task 1: RED parity-gate anchor (both repos)
d9b168f(test, fonoteka.go):TestPhase8RedParityGatefails closed withPHASE8_RED:parity-gatewhilemcp-lifecycle.yamldoesn't exist yet; verified viascripts/check-phase8-red.shgo mode.246a488(test, summercms.go):scripts/check-phase8.shskeleton with the ordered stage list and the permanent--red-contractself-test; verified viascripts/check-phase8-red.shshell mode (exit 86, exactPHASE8_STAGE:real-mcp:FAIL:PHASE8_RED:real-mcp-stageline).
- 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 thetide.isIDKey_idsmasking 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, theOAuthConsentController/routes.gofixes, and theparity_test.go/parity_contract_test.gocount updates.
- Task 3: complete the final gate (summercms.go)
e87346f(feat): allscripts/check-phase8.shstage bodies plusscripts/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 entriesstatus: ported+seed_hook: genres../fonoteka.go/parity/migrate_test.go--testConfignow setsapp.urlto match isolated PHP's fixed origin../fonoteka.go/parity/parity_test.go/parity_contract_test.go--expectedPortedRoutes22 -> 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+ newwinter_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 expectationswristband/redirect_html.go-- Symfony-exact HTML redirect body + PHPhtmlspecialchars(ENT_QUOTES)portwristband/authorize.go/register.go/token.go+ their_test.gofiles -- corrected Cache-Control byte contracttide/normalize.go--isIDKeymasks_idsplural arraysscripts/check-phase8.sh-- the complete final gate scriptscripts/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
TestOAuthFlowsreplay attempt - Issue:
wristbandset bareCache-Control: no-storeand 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) sendsno-store, privateandno-cache, privaterespectively. - Fix:
authorize.go,register.go,token.goupdated; every affectedwristbandand 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.goports the exact byte template and PHP'shtmlspecialchars(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:
TestOAuthFlowsbyte-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,
TestOAuthFlowsfull-package run (collection_ids[0]: expected 1 actual 8) - Issue:
isIDKeymatched_idandidbut not the plural_idssuffix, so a literalcollection_idsarray 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:
isIDKeynow 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,
TestParityCorpusfull run - Issue: The 08-UI-SPEC.md-derived
[A-Za-z0-9_-]{16,128}constraint rejected the Phase-2-recorded 14-characterparity-missingtest value at the router (bare text/plain 404) beforeOAuthConsentController::showcould 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,
TestParityCorpusfull 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 withAPP_DEBUG=false, matching the isolated-PHP convention) crashes to Winter's generic production 500 HTML page. Go returned a cleanwriteValidation422. - Fix:
ConsentStore's basic-validation-failure branch now writes the same byte-identical Winter error page the user plugin already embeds (newly duplicated intocontrollers/api/winter_error_page.htmlto 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
TestParityCorpusrun, after fixes 4/5 above - Issue:
POST .../oauth/consent's case fixture had been recorded withAPP_DEBUG=true(a full debug stack trace with incidental scrub collisions in the stack-frame numbers);GET .../connected-apps's case fixture hadmanual_tokens_countover-scrubbed to a coincidental{{id:alice}}placeholder. - Fix: Both re-recorded/hand-corrected against live isolated PHP with the current
APP_DEBUG=falseconvention and thegenresseed 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)leftapp.urlempty (breaking every issuer/absolute-URL field) and thegenresseed hook's hardcoded "Parity Collection" name didn't match the recorded PHP flow's actual onboarding-auto-created default collection name ("Moja kolekcja"). - Fix:
testConfignow setsapp.urlto isolated PHP's fixed origin;TestOAuthFlowsseeds its own "Moja kolekcja" default collection with noactive_collection_contextrow, lettingActiveCollectionResolver'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 justTestOAuthFlowsin isolation) - Issue:
TestOAuthFlows's confidential-client token was left live (unrevoked) at test end, inflatingconnected_apps_countfor a siblingTestParityCorpuscase that shares the samealice@parity.testidentity across the whole package's shared Postgres pool. - Fix:
TestOAuthFlowsnow revokes everyoauth_client_id-linked token for its alice int.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
TestParityCorpusrun after flipping the nine routes toported - 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: genresto 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:
TestParityContracthardcoded22 ported/147 pendingand an explicit route-id allow-list that didn't include the nine new routes. - Fix: Updated to reference
expectedPortedRoutes/expectedPHPRoutesand 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-10can now runscripts/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 (exactcompassconfig keys, timing) once actually exercised.08-SECURITY-REVIEW.mddoes not exist yet;stage_security_reviewwill fail closed until 08-10 creates it withstatus: 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