docs(32.1-03): complete MCP consent and backfill plan

This commit is contained in:
2026-10-28 15:44:26 +01:00
parent 3f52b1ab49
commit 23238ccd47
3 changed files with 184 additions and 2 deletions

View File

@@ -1621,7 +1621,7 @@ Plans:
**Goal:** As an application whose plugins declare their own member token scopes, I want an agent connected to `/mcp` over OAuth to get the same tools, attribution and revocation as one using a personal token, so that OAuth sign-in can be switched on in production. A consuming app's agent-access work found three gaps (its D-36, D-37 and D-38); all three are additive and ship as patch releases of the framework (0.5.x) and `sm-oauthserver-plugin`.
**Requirements**: TBD
**Depends on:** Phase 19.4 (OIDC provider on wristband), Phase 19.7 (generic MCP module)
**Plans:** 2/5 plans executed
**Plans:** 3/5 plans executed
**Success Criteria** (what must be TRUE):
1. Scopes a plugin declares through `pact.HasFrontendTokenScopes` can be requested, consented to and granted over MCP OAuth: the wristband metadata and authorize allow-list and pier's protected-resource scopes include them, so a tool route behind `user.scope:<declared>` lists and runs for an OAuth agent granted that scope. Today the list is fixed to `read`, `write`, `ai`, `offline_access` (and `backend`).
@@ -1637,7 +1637,7 @@ Plans:
- [x] 32.1-02-PLAN.md — sm-user-plugin from v0.1.14: publish the catalog and Implies for OAuth subjects
**Wave 3** *(blocked on Wave 2 completion)*
- [ ] 32.1-03-PLAN.md — sm-oauthserver-plugin: consent replace, revoke, backfill
- [x] 32.1-03-PLAN.md — sm-oauthserver-plugin: consent replace, revoke, backfill
**Wave 4** *(blocked on Wave 3 completion)*
- [ ] 32.1-04-PLAN.md — vue-oauth-layer /connect checkboxes

View File

@@ -0,0 +1,181 @@
---
phase: 32.1-mcp-oauth-for-plugin-apps-declared-scopes-mcp-subject-on-con
plan: 03
subsystem: auth
tags: [mcp, oauth, consent, gormigrate, sm-oauthserver-plugin]
requires:
- phase: 32.1-01
provides: pact.MCPAdvertisedScopes and offline access on the pending consent view
- phase: 32.1-02
provides: pact.MemberTokenScopeCatalog from sm-user-plugin
provides:
- "One MCP scope slice from golem15.oauthserver.mcp_scopes drives metadata and MCPTokenVerifier.MCPScopesSupported"
- "MCP consent replaces stored scopes, revoke deletes MCP codes under lockUserGrants, and authorizations carry kind"
- "Migration 202610280001 backfills one consent per live MCP grant and its Rollback does not delete rows"
affects: [32.1-05]
actuals:
tokens: 21059
tasks: 4
commits: 0
plan_head_before: 3f52b1ab495b487b03b8e534bdfc6b6676104b00
plan_head_after: 3f52b1ab495b487b03b8e534bdfc6b6676104b00
tech-stack:
added: []
patterns:
- "MCP scope slice is built once in NewServers and stored on the verifier; OIDC scopes stay untouched"
- "MCP consent replace writes golem15_oauthserver_consents directly and does not call SaveConsent"
- "The D-14 backfill is one-way: Rollback returns nil and does not delete consents"
key-files:
created:
- ../sm-oauthserver-plugin/updates/202610280001_backfill_golem15_oauthserver_mcp_consents.go
- .planning/phases/32.1-mcp-oauth-for-plugin-apps-declared-scopes-mcp-subject-on-con/32.1-03-backfill-decision.txt
modified:
- ../sm-oauthserver-plugin/classes/mcp_scopes.go
- ../sm-oauthserver-plugin/controllers/mcp_consent_api.go
- ../sm-oauthserver-plugin/controllers/authorizations.go
- ../sm-oauthserver-plugin/README.md
key-decisions:
- "Checkpoint write-noop-rollback: backfill live MCP grants into golem15_oauthserver_consents, and Rollback returns nil so a down migration cannot delete consents created later"
- "Scope union walks live tokens by created_at (nulls last), then access before refresh, then id; offline_access is appended when any live refresh token has that column set"
patterns-established:
- "Pattern: an unknown golem15.oauthserver.mcp_scopes name fails boot and names the key"
- "Pattern: MCP revoke takes lockUserGrants, deletes the consent and MCP codes, and emits MCPSubjectRevoked with a zero TokenID"
requirements-completed: []
coverage:
- id: D1
description: "Unset mcp_scopes advertises the five fixed names plus every declared member scope; a set list keeps only those declared names; an empty list is the five fixed names; an unknown name fails boot. OIDC discovery scopes stay openid, profile, email."
verification:
- kind: unit
ref: mcp_scopes_test.go#TestMCPAdvertisedScopes
status: pass
human_judgment: false
- id: D2
description: "MCP consent show adds scope_details and client.logo_url, an allow replaces stored scopes instead of calling SaveConsent, revoke deletes MCP codes under lockUserGrants, and GET /oauth/authorizations ends with kind."
verification:
- kind: integration
ref: mcp_consent_smoke_test.go#TestMCPConsentSmoke
status: pass
- kind: integration
ref: authorizations_smoke_test.go#TestAuthorizationsSmoke
status: pass
human_judgment: false
- id: D3
description: "The accepted checkpoint is write-noop-rollback. Migration 202610280001_backfill_golem15_oauthserver_mcp_consents is registered, names golem15_oauthserver_consents, and its Rollback returns nil and does not delete."
verification:
- kind: other
ref: python3 checkpoint record and migration text gate in 32.1-03-PLAN.md task 4
status: pass
human_judgment: true
rationale: "The plan gate checks the decision file and that Rollback returns nil without a delete. It does not insert a live token and migrate twice. Plan 05 TestMCPConsentBackfillIdempotent is the row-level proof."
duration: 24min
completed: 2026-10-28
status: complete
---
# Phase 32.1 Plan 03: MCP scope slice, consent replace, and one-way backfill Summary
**One MCP scope slice drives metadata and the verifier, consent replace and revoke delete MCP codes, and a no-op-rollback migration backfills consents for live grants**
## Performance
- **Duration:** 24 min
- **Started:** 2026-10-28T14:18:42Z
- **Completed:** 2026-10-28T14:43:00Z
- **Tasks:** 4
- **Files modified:** 17
The clock starts at the task 1 commit. This session resumed after the checkpoint.
## Accomplishments
- `golem15.oauthserver.mcp_scopes` filters declared member scopes. The fixed names `read`, `write`, `ai`, `offline_access`, and `backend` are always advertised, in that order, then declared names. An unknown name fails boot and names the key. `MCPTokenVerifier.MCPScopesSupported` returns that same slice. OIDC scopes are not rewritten.
- An MCP allow replaces `golem15_oauthserver_consents` for the user and client and does not call `SaveConsent`. `offline_access` stays a column, copied into the consent scope list when it was granted. Revoke takes `lockUserGrants`, deletes the consent, OIDC codes, MCP codes, access tokens, and refresh tokens, then emits `MCPSubjectRevoked` with `ClientID` set and `TokenID` left zero. `GET /oauth/authorizations` always ends with `kind` `oidc` or `mcp`.
- Checkpoint option `write-noop-rollback` was accepted. The decision file was written before the migration existed. `202610280001_backfill_golem15_oauthserver_mcp_consents` inserts one consent per MCP user and client that has a live access or refresh token and no consent yet. Live means `revoked_at` is null and `expires_at` is null or still in the future. A second run inserts nothing. `Rollback` returns nil and does not delete consents.
## Task Commits
Plugin repo `sm-oauthserver-plugin` on `master`. summercms.go had no commits in the measured range `3f52b1ab495b487b03b8e534bdfc6b6676104b00..HEAD` at summary time (`commits: 0`). The decision record is committed with this summary.
1. **Task 1: Advertise one MCP scope slice** - `644641a` (feat)
2. **Task 2: MCP consent replace, revoke, and kind** - `52529c6` (feat)
3. **Task 3: Confirm the one-way MCP consent backfill** - option `write-noop-rollback` recorded in `32.1-03-backfill-decision.txt` before the migration file was created. No plugin commit.
4. **Task 4: Backfill MCP consents for live grants** - `a409074` (feat)
**Plan metadata:** this summary, the decision record, and the roadmap progress update.
## Files Created/Modified
- `../sm-oauthserver-plugin/config/config.yaml` - `mcp_scopes` and the `SUMMER_GOLEM15__OAUTHSERVER__MCP_SCOPES` spelling
- `../sm-oauthserver-plugin/classes/mcp_scopes.go` - the one advertised slice and the unknown-name boot error
- `../sm-oauthserver-plugin/classes/mcp_verifier.go` - `MCPScopesSupported`
- `../sm-oauthserver-plugin/servers.go` - assigns that slice only to the MCP server
- `../sm-oauthserver-plugin/mcp_scopes_test.go` - unset, set, empty, and unknown name
- `../sm-oauthserver-plugin/controllers/mcp_consent_api.go` - `scope_details`, `logo_url`, `offline_access`, consent replace
- `../sm-oauthserver-plugin/controllers/authorizations.go` - `kind` and revoke of MCP codes under `lockUserGrants`
- `../sm-oauthserver-plugin/classes/mcp_store.go` - `MarkIssued` reads the offline-access override
- `../sm-oauthserver-plugin/classes/mcp_backend_binding.go` - context value for that override
- `../sm-oauthserver-plugin/lang/en/lang.yaml` - backend and offline_access consent phrases
- `../sm-oauthserver-plugin/lang/pl/lang.yaml` - the same phrases in Polish
- `../sm-oauthserver-plugin/README.md` - scope filter, replace versus the OIDC union, kind, revoke, and old DCR ceilings
- `../sm-oauthserver-plugin/authorizations_smoke_test.go` - seven keys ending in `kind`
- `../sm-oauthserver-plugin/mcp_consent_smoke_test.go` - smoke expectations kept in step with the new JSON
- `../sm-oauthserver-plugin/updates/202610280001_backfill_golem15_oauthserver_mcp_consents.go` - idempotent one-way consent backfill
- `32.1-03-backfill-decision.txt` - the entire text is `write-noop-rollback`
## Decisions Made
- The checkpoint accepted `write-noop-rollback`. Rollback must not delete every MCP consent, because those rows cannot be told from consents the new code creates after the migration. Undo is a hand-written delete, not the migration Rollback.
- The union walks live tokens from earliest `created_at` (nulls last), access before refresh on a tie, then id. Empty scope names are dropped. `offline_access` is added at the end when any live refresh token has the column set and the word is not already present. Access tokens have no such column. `created_at` and `updated_at` are that earliest token time, or null when every live token has none. `last_used_at` stays null. A pair with any existing consent row is skipped, and the insert also uses `WHERE NOT EXISTS`.
## Deviations from Plan
None - plan executed exactly as written.
## Issues Encountered
`state.advance-plan` was not run. It has no phase argument and advances whatever Current Position names. That position is phase 33, from another session. Advancing it would move the wrong plan counter. Roadmap progress for 32.1 was updated with `roadmap.update-plan-progress`. A phase-scoped decision and metric may be recorded in STATE.md. STATE.md and state.json are not committed.
`go.work` in sm-oauthserver-plugin was already dirty (one replace line). It was not staged, reverted, or committed.
The plugin commit is on `master`, which is where tasks 1 and 2 of this plan already landed. This resume required that. Nothing was pushed or tagged.
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
Plan 05 can add `TestMCPConsentBackfillIdempotent` against this migration. A live MCP access token with no consent should gain one row. A refresh token with `offline_access` true should add that word. A pair that already has a consent must be left unchanged. A second migrate inserts nothing. `Rollback` returns nil.
Plan 04 (vue-oauth-layer) can read `scope_details`, `logo_url`, `consent_required`, and `kind`. It does not depend on the backfill having run.
## Self-Check: PASSED
- FOUND: ../sm-oauthserver-plugin/updates/202610280001_backfill_golem15_oauthserver_mcp_consents.go
- FOUND: ../sm-oauthserver-plugin/config/config.yaml
- FOUND: ../sm-oauthserver-plugin/classes/mcp_scopes.go
- FOUND: ../sm-oauthserver-plugin/classes/mcp_verifier.go
- FOUND: ../sm-oauthserver-plugin/controllers/mcp_consent_api.go
- FOUND: ../sm-oauthserver-plugin/controllers/authorizations.go
- FOUND: ../sm-oauthserver-plugin/README.md
- FOUND: 32.1-03-backfill-decision.txt (`write-noop-rollback`)
- FOUND: 644641a
- FOUND: 52529c6
- FOUND: a409074
- Python task-4 gate: exit 0
- `go test -count=1 -timeout 240s -run 'TestAuthorizationsSmoke|TestMCPAdvertisedScopes'` exit 0
- `go test -count=1 -timeout 240s -run 'TestMCPConsentSmoke|TestMCPAdvertisedScopes'` exit 0
- `TestAuthorizationsSmoke` PASS against Postgres, so the new migration applies on an empty database
---
*Phase: 32.1-mcp-oauth-for-plugin-apps-declared-scopes-mcp-subject-on-con*
*Completed: 2026-10-28*