From f13f76ed4bb128462d4e7558ed2e7c7c3455a15e Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Wed, 23 Sep 2026 22:23:40 +0200 Subject: [PATCH] docs(08-08): complete mcp-me-prerequisite plan --- .planning/ROADMAP.md | 4 +- .planning/STATE.md | 18 ++- .../08-08-SUMMARY.md | 130 ++++++++++++++++++ 3 files changed, 143 insertions(+), 9 deletions(-) create mode 100644 .planning/phases/08-oauth2-1-authorization-server/08-08-SUMMARY.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 67418af..e4a519a 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -350,7 +350,7 @@ Plans: **Wave 7** *(parallel; blocked on 08-06)* - [x] 08-07-PLAN.md — Provision confidential clients through the exact operator command -- [ ] 08-08-PLAN.md — Serve the MCP personal-token bootstrap on the existing token surface +- [x] 08-08-PLAN.md — Serve the MCP personal-token bootstrap on the existing token surface **Wave 8** *(blocked on 08-07 and 08-08)* @@ -496,7 +496,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → | 5. Data layer full fidelity | 6/6 | Complete | 2026-09-18 | | 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 | 7/10 | In Progress| | +| 8. OAuth2.1 authorization server | 8/10 | In Progress| | | 9. Backend admin authentication and schema pipeline | 0/TBD | Not started | - | | 10. Admin Vue SPA | 0/TBD | Not started | - | | 11. Jobs, realtime and search infrastructure | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 28c57c4..a2a79d4 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,14 +3,14 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone status: executing -stopped_at: Completed 08-07-PLAN.md -last_updated: "2026-09-23T20:05:28.781Z" +stopped_at: Completed 08-08-PLAN.md +last_updated: "2026-09-23T20:23:27.498Z" last_activity: 2026-09-23 progress: total_phases: 15 completed_phases: 7 total_plans: 55 - completed_plans: 52 + completed_plans: 53 percent: 47 --- @@ -26,11 +26,11 @@ See: .planning/PROJECT.md (updated 2026-09-16) ## Current Position Phase: 08 (oauth2-1-authorization-server) — EXECUTING -Plan: 8 of 10 +Plan: 9 of 10 Status: Ready to execute Last activity: 2026-09-23 -Progress: [██████████] 95% +Progress: [██████████] 96% ## Performance Metrics @@ -98,6 +98,7 @@ Progress: [██████████] 95% | Phase 08 P05 | 55min | 3 tasks | 12 files | | Phase 08 P06 | 50min | 3 tasks | 10 files | | Phase 08 P07 | 35min | 2 tasks | 9 files | +| Phase 08 P08 | 20min | 2 tasks | 4 files | ## Accumulated Context @@ -244,6 +245,9 @@ Recent decisions affecting current work: - [Phase ?]: [Phase 08 P07]: list/update operate directly on models.OAuthClient via *gorm.DB rather than extending wristband.ClientStore -- only client issuance needs the shared wristband hash/validation path (T-08-SECRET-TIMING) - [Phase ?]: [Phase 08 P07]: Fixed clientRecordToModel to persist ScopeCeiling, a mapping gap silent since 08-02 because DCR never sets a ceiling - [Phase ?]: [Phase 08 P07]: AUTH-05/AUTH-07 remain Pending in REQUIREMENTS.md -- both requirements' full text still needs 08-08/08-09's unchanged fonoteka-mcp install/auth flow proof +- [Phase ?]: [Phase 08 P08]: me_token_controller.go was created in the Task 1 RED commit as a compiling 501 stub (Rule 3), following the 08-04/08-06/08-07 precedent -- controllers/api must compile with its new test file while the route stays unmounted +- [Phase ?]: [Phase 08 P08]: scopes/collection_ids on GET /api/v1/fonoteka/me always serialize via wire.Slice (nil -> []), a deliberate divergence from PHP's collection_ids null-means-unrestricted semantics, accepted because fonoteka-mcp's TS MeResponse type never reads collection_ids +- [Phase ?]: [Phase 08 P08]: AUTH-07 stays Pending in REQUIREMENTS.md -- this plan ships the /me prerequisite but the requirement's 'fonoteka-mcp completes its install and auth flow unchanged' clause needs 08-09's real MCP gate ### Pending Todos @@ -265,6 +269,6 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-09-23T20:05:28.751Z -Stopped at: Completed 08-07-PLAN.md +Last session: 2026-09-23T20:23:27.474Z +Stopped at: Completed 08-08-PLAN.md Resume file: None diff --git a/.planning/phases/08-oauth2-1-authorization-server/08-08-SUMMARY.md b/.planning/phases/08-oauth2-1-authorization-server/08-08-SUMMARY.md new file mode 100644 index 0000000..704461e --- /dev/null +++ b/.planning/phases/08-oauth2-1-authorization-server/08-08-SUMMARY.md @@ -0,0 +1,130 @@ +--- +phase: 08-oauth2-1-authorization-server +plan: 08 +subsystem: auth +tags: [mcp, personal-token, inv_token, bouncer, fonoteka-mcp-prerequisite] + +# Dependency graph +requires: + - phase: 08-oauth2-1-authorization-server + plan: 06 + provides: "the D-17 sweep/lifecycle wave this plan builds on top of; more directly, Phase 7's inv_token TokenGuard and inv.scope:read middleware this plan's route reuses unchanged" +provides: + - "GET /api/v1/fonoteka/me on the existing personal-token group: the minimal exact bootstrap payload (scopes, collection_ids, user_id, name) fonoteka-mcp's client.me() call needs before constructing its MCP server (D-20)" + - "controllers/api.MeToken: reads bouncer.Credential/bouncer.User directly, no bearer reparse, no second token query" +affects: [08-09-parity-and-real-mcp-gate, 08-10-unit-tests-and-security-review] + +# Tech tracking +tech-stack: + added: [] + patterns: + - "MeToken is the first handler in this codebase that reads bouncer.Credential purely for response serialization (not authorization) -- it trusts the inv_token guard's already-matched *models.ApiToken and never touches gorm.DB, proven by a test that mutates the persisted row after the credential was matched and asserts the response still reflects the pre-mutation in-memory value" + - "scopes/collection_ids serialize through wire.Slice so a nil Jsonable[[]T] (unset scopes or an unrestricted/no-collection token) always emits [] on this endpoint, not null -- an explicit, plan-directed simplification of PHP's collectionIds() null-means-unrestricted semantics, chosen because the unchanged TS MeResponse type does not even consume collection_ids" + +key-files: + created: + - ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller.go + - ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller_test.go + - ../fonoteka.go/plugins/golem15/fonoteka/oauth_tools_test.go + modified: + - ../fonoteka.go/plugins/golem15/fonoteka/routes.go + +key-decisions: + - "controllers/api/me_token_controller.go was created in the Task 1 RED commit (not literally in Task 1's files_modified list) as a compiling 501 stub, following the 08-04/08-06/08-07 precedent of shipping a minimal compiling seam in the RED commit so the package and its new test file build while the route stays unmounted; Task 2 replaced the stub body and left the file's identity/location unchanged (Rule 3 -- blocking compile prerequisite)" + - "user_id is emitted as *uint (nil-able), not a bare uint defaulting to 0, to match PHP's $user?->id nullable semantics even though the inv_token guard always populates both Credential and User together in production, so this path is defensive-only" + - "scopes/collection_ids always serialize via wire.Slice (nil -> []), per Task 1's explicit behavior/acceptance criteria (\"arrays are never null\") -- a deliberate divergence from PHP's collection_ids null-means-unrestricted contract, accepted because fonoteka-mcp's TS MeResponse type does not read collection_ids at all" + - "AUTH-07 stays Pending in REQUIREMENTS.md: its full text also requires 'fonoteka-mcp completes its install and auth flow unchanged', which only 08-09's real MCP gate (D-14) can prove; this plan ships the /me prerequisite that gate needs, not the gate itself" + +patterns-established: + - "Route-isolation assertion (assertMeRouteIsolation in oauth_tools_test.go) is reused by both the RED anchor and a standalone named test, following the shared-helper convention 08-06's oauthLifecycleGrant established for reusable multi-test setup" + +requirements-completed: [] + +# Metrics +duration: ~20min +completed: 2026-09-23 +--- + +# Phase 08 Plan 08: MCP /me Prerequisite Summary + +**GET /api/v1/fonoteka/me on the personal-token group returns exactly scopes/collection_ids/user_id/name straight off the inv_token guard's already-matched credential, with no bearer reparse and no second database query, unblocking 08-09's real fonoteka-mcp gate.** + +## Performance + +- **Duration:** ~20 min +- **Started:** ~2026-09-23T20:00:00Z (approx.) +- **Completed:** 2026-09-23T20:19:37Z +- **Tasks:** 2 completed (2 commits: RED then GREEN, both in fonoteka.go; no summercms.go changes this plan) +- **Files modified:** 4 (3 created, 1 modified, all in fonoteka.go) + +## Accomplishments + +- `controllers/api.MeToken` serves the exact PHP `MeTokenController::me` contract as a Go handler: `{"data":{"scopes":[...],"collection_ids":[...],"user_id":...,"name":...}}`, sourced entirely from `bouncer.Credential`/`bouncer.User` -- the request context values the real `inv_token` guard already populated during its own bearer-parse/hash/DB-lookup pass, so the handler performs zero additional queries +- `wire.Slice` on both `scopes` and `collection_ids` guarantees neither array ever serializes as JSON `null`, even for a token minted with no explicit scopes/collections (a `TestMeTokenHandlerNilScopesAndCollectionIDsSerializeAsEmptyArraysAndNullableName` regression proves this directly against the raw response bytes) +- `TestMeTokenHandlerReusesMatchedCredentialWithoutReQuerying` proves the "no second lookup" contract mechanically: it mutates the persisted token row's `name` column *after* the credential was matched and asserts the response still reflects the original in-memory value -- if the handler ever re-queried, this test would catch it +- `GET /me` is mounted as the third route on the existing `/api/v1/fonoteka` personal-token group, inheriting `inv_token`, `throttle:fonoteka-api-token`, `inv.scope:read` in that exact order (proven by route-table inspection, not just manual reading of `routes.go`); missing/unknown-token 401 and wrong-scope 403 bytes are byte-identical to the pre-existing token-surface contract, with no new header +- Both the Phase 8 RED sentinel (`PHASE8_RED:mcp-me` in `fonoteka`, verified fail-closed via `scripts/check-phase8-red.sh` against the genuine pre-implementation state -- route unmounted, 404 instead of the exact positive body) and the full GREEN suite (`go vet`/`go test`/`go test -race` across every subpackage of `plugins/golem15/fonoteka` plus `plugins/golem15/user`) are green + +## Task Commits + +Both tasks carry `tdd="true"`; RED then GREEN landed as separate commits, both in `fonoteka.go` (this plan makes no `summercms.go` changes): + +1. **Task 1: RED anchor** -- `686d622` (test): `TestPhase8RedMCPMe` (positive payload, missing/wrong-scope negative bytes, route-table isolation) fails with `PHASE8_RED:mcp-me` against the unmounted route; `controllers/api.MeToken` ships as a compiling 501 stub so the package and its new test files build. Verified fail-closed via `scripts/check-phase8-red.sh`. +2. **Task 2: implement and mount** -- `19ac5fd` (feat): the real `MeToken` handler and `GET /me` mounted on the personal-token group in `routes.go`. + +**Plan metadata:** committed as part of this summary/state-update commit. + +## Files Created/Modified + +- `../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller.go` -- `MeToken` (real implementation, Task 2; compiling 501 stub in Task 1) +- `../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller_test.go` -- `TestMeTokenHandlerExactFourFieldsWithScopesAndCollectionIDs`, `TestMeTokenHandlerNilScopesAndCollectionIDsSerializeAsEmptyArraysAndNullableName`, `TestMeTokenHandlerReusesMatchedCredentialWithoutReQuerying` (package-local unit tests injecting an already-matched credential/principal into context, matching the `connected_app_controller_test.go` precedent) +- `../fonoteka.go/plugins/golem15/fonoteka/oauth_tools_test.go` -- `TestPhase8RedMCPMe` (RED anchor), `TestOAuthToolsMePositiveAndNegativeContract`, `TestOAuthToolsMeRouteIsolation`, plus shared helpers `meToolsInsertToken`/`meToolsBearerRequest`/`assertMeRouteIsolation` +- `../fonoteka.go/plugins/golem15/fonoteka/routes.go` -- mounts `GET /me` on the existing `/api/v1/fonoteka` personal-token group, after `/genres` + +## Decisions Made + +See frontmatter `key-decisions`. The most load-bearing: shipping `me_token_controller.go` as a compiling 501 stub in the RED commit (rather than leaving the file entirely absent until Task 2) follows the exact precedent 08-04's `Server.Token` stub, 08-06's store-interface extensions, and 08-07's `bonfire` compiling seam all established -- a RED commit is allowed to add non-literally-listed compile prerequisites when the alternative is an uncompilable package, and this is tracked here as a Rule 3 (blocking issue) auto-fix rather than a silent scope change. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] `me_token_controller.go` added in the Task 1 RED commit, not literally in Task 1's `files_modified`** +- **Found during:** Task 1, writing `me_token_controller_test.go` and `oauth_tools_test.go` +- **Issue:** Task 1's plan-listed files are only the two test files. Without `me_token_controller.go` existing at all, `controllers/api` would not compile once its test file referenced `MeToken`, and `go test ./plugins/golem15/fonoteka` (the RED verify command's target) depends on `controllers/api` compiling as a regular dependency. +- **Fix:** Added a minimal `MeToken` stub returning an opaque 500, in the same file Task 2's `files_modified` list already names, so the RED commit compiles cleanly and the RED failure comes from the intended source (unmounted route, 404) rather than a build error. +- **Files modified:** `../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller.go` +- **Verification:** `go vet ./plugins/golem15/fonoteka/...` clean at the RED commit; `scripts/check-phase8-red.sh` confirms the RED failure is exactly `PHASE8_RED:mcp-me` with no build/setup/panic noise. +- **Committed in:** `686d622` (Task 1 RED commit) + +--- + +**Total deviations:** 1 auto-fixed (compile prerequisite, no scope change) +**Impact on plan:** None beyond making the RED commit itself buildable; Task 2 implemented the handler in the same file, at the same location, matching the plan's own file list. + +## Issues Encountered + +None beyond the auto-fixed item above. Full `go vet`/`go test ./...` are green across every subpackage of `fonoteka.go`'s `plugins/golem15/fonoteka` module (`classes`, `classes/auth`, `console`, `controllers/api`, `middleware`, `models`, `updates`) and `plugins/golem15/user`; `go test -race` on the touched packages (`controllers/api`, top-level `fonoteka`) is also green. `go build ./...` at the `fonoteka.go` workspace root is clean. This plan makes no `summercms.go` changes, so `summercms.go`'s own `go vet`/`go test ./...` are unaffected (not re-run). + +## User Setup Required + +None -- no external service configuration required. + +## Next Phase Readiness + +- `GET /api/v1/fonoteka/me` is live on the assembled app and ready for 08-09's real `fonoteka-mcp` process (started via `scripts/check-phase8.sh`, D-14) to call as its first authenticated request before constructing the MCP server. +- The response's `scopes` field is exactly what `fonoteka-mcp`'s `scopeGatedTools`-style factory needs to gate tool visibility (a read-only token never sees write/ai tools) -- no further backend change is anticipated for that consumption path. +- AUTH-07 remains Pending in REQUIREMENTS.md: this plan ships the `/me` prerequisite its text implicitly depends on, but the requirement's own "fonoteka-mcp completes its install and auth flow unchanged" clause is only provable once 08-09's real Node MCP gate runs end to end against the Go backend. +- No blockers. + +## Self-Check: PASSED + +- FOUND: ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller.go +- FOUND: ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller_test.go +- FOUND: ../fonoteka.go/plugins/golem15/fonoteka/oauth_tools_test.go +- FOUND: ../fonoteka.go/plugins/golem15/fonoteka/routes.go (modified) +- FOUND commits (fonoteka.go): 686d622, 19ac5fd + +--- +*Phase: 08-oauth2-1-authorization-server* +*Completed: 2026-09-23*