diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 20d026a2..05d16ced 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -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:** 5 plans +**Plans:** 1/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:` lists and runs for an OAuth agent granted that scope. Today the list is fixed to `read`, `write`, `ai`, `offline_access` (and `backend`). @@ -1631,7 +1631,7 @@ Plans: Plans: **Wave 1** -- [ ] 32.1-01-PLAN.md — Framework seam: catalog, MCPCall, one advertised scope list +- [x] 32.1-01-PLAN.md — Framework seam: catalog, MCPCall, one advertised scope list **Wave 2** *(blocked on Wave 1 completion)* - [ ] 32.1-02-PLAN.md — sm-user-plugin from v0.1.14: publish the catalog and Implies for OAuth subjects diff --git a/.planning/phases/32.1-mcp-oauth-for-plugin-apps-declared-scopes-mcp-subject-on-con/32.1-01-SUMMARY.md b/.planning/phases/32.1-mcp-oauth-for-plugin-apps-declared-scopes-mcp-subject-on-con/32.1-01-SUMMARY.md new file mode 100644 index 00000000..8b3a1e8f --- /dev/null +++ b/.planning/phases/32.1-mcp-oauth-for-plugin-apps-declared-scopes-mcp-subject-on-con/32.1-01-SUMMARY.md @@ -0,0 +1,191 @@ +--- +phase: 32.1-mcp-oauth-for-plugin-apps-declared-scopes-mcp-subject-on-con +plan: 01 +subsystem: auth +tags: [mcp, oauth, pact, pier, wristband, scopes] + +requires: + - phase: 19.7-generic-mcp-module + provides: pier in-process dispatch, PRMScopes and the read/write ceiling + - phase: 19.4-oidc-provider-on-wristband + provides: OIDC discovery scope list left unchanged +provides: + - "pact.MemberTokenScopeCatalog, pact.MCPAdvertisedScopes, pact.MCPCall, pact.WithMCPCall, pact.MCPCallFrom" + - "pact.FrontendTokenScope.Implies" + - "wristband resolvedScopes shared by MCP metadata and authorize" + - "wristband.PendingRequestView.OfflineAccess" + - "pier PRM copies MCPScopesSupported when non-empty, else PRMScopes, and sets MCPCall beside WithDispatch" +affects: [32.1-02, 32.1-03, 32.1-04, 32.1-05] + +actuals: + tokens: 17695 + tasks: 3 + commits: 3 +plan_head_before: 2cea3127cc2a2da5f8e0ea6ec47356bfaecc5c67 +plan_head_after: 876f0f09f2c4a42864941118d12d636123af1594 + +tech-stack: + added: [] + patterns: + - "MCPAdvertisedScopes is a second interface; MCPTokenVerifier stays MCPVerifyToken and MCPResource" + - "One resolvedScopes slice drives MCP metadata and the authorize allow-list" + - "WithMCPCall is set on the dispatch context immediately before WithDispatch; a nil context is not replaced" + +key-files: + created: + - modules/pact/mcp_call_test.go + - modules/pier/mcp_seam_test.go + modified: + - modules/pact/capabilities.go + - modules/pact/mcp.go + - modules/pact/README.md + - modules/pier/dispatch.go + - modules/pier/prm.go + - modules/pier/session.go + - modules/pier/stateless.go + - modules/pier/README.md + - modules/wristband/authorize.go + - modules/wristband/server.go + - modules/wristband/consent.go + - modules/wristband/mcp_scopes_smoke_test.go + - modules/wristband/README.md + - docs/services/mcp.md + - docs/services/oauth-server.md + - docs/services/authentication.md + +key-decisions: + - "Empty MCP Options.ScopesSupported resolves to read, write, ai, offline_access, backend; a non-empty list, including DefaultOptions, is used as given" + - "Advertised scopes are pact.MCPAdvertisedScopes, not a new method on pact.MCPTokenVerifier" + - "Stateless tool calls set callInfo.stateless so MCPCall.SessionID stays empty" + +patterns-established: + - "Pattern: Metadata and allowedAuthorizeScopes both call Server.resolvedScopes" + - "Pattern: pier mountPRM type-asserts MCPAdvertisedScopes and clones a non-empty MCPScopesSupported, else PRMScopes" + +requirements-completed: [] + +coverage: + - id: D1 + description: "An empty MCP Options.ScopesSupported advertises and accepts the five fixed names including backend; DefaultOptions still refuses backend; OIDC discovery stays openid, profile, email" + verification: + - kind: unit + ref: modules/wristband/mcp_scopes_smoke_test.go#TestMCPScopesSmoke + status: pass + - kind: unit + ref: modules/wristband/oidc_discovery_test.go#TestDiscoveryFields + status: pass + human_judgment: false + - id: D2 + description: "pier copies MCPScopesSupported onto protected-resource metadata when non-empty and otherwise keeps PRMScopes, and one frontend tools/call observes MCPCall beside the bouncer dispatch marker" + verification: + - kind: unit + ref: modules/pier/mcp_seam_test.go#TestMCPSeam + status: pass + human_judgment: false + - id: D3 + description: "WithMCPCall then MCPCallFrom round-trips Subject.Guard, SessionID and Tool; a context without a call answers ok false" + verification: + - kind: unit + ref: modules/pact/mcp_call_test.go#TestMCPCallRoundTrip + status: pass + human_judgment: false + - id: D4 + description: "PendingRequestView.OfflineAccess is set from the pending row; IssueCode's signature is unchanged" + verification: + - kind: unit + ref: go test -short ./modules/wristband + status: pass + human_judgment: true + rationale: "No test asserts that PendingRequest copies OfflineAccess; the package suite only proves the field compiles and existing consent tests still pass" + - id: D5 + description: "pact, pier and wristband READMEs and the three service docs name the new identifiers and the mcp_scopes rule without naming a consuming application" + verification: + - kind: other + ref: go test ./cmd/summer -run TestDocsTree && go run ./cmd/summer docs:build --check + status: pass + human_judgment: false + +duration: 12min +completed: 2026-10-28 +status: complete +--- + +# Phase 32.1 Plan 01: MCP scope list and call context Summary + +**One resolved MCP scope list drives wristband metadata and authorize, pier copies it from the verifier, and every in-process tool call carries pact.MCPCall** + +## Performance + +- **Duration:** 12 min +- **Started:** 2026-10-28T13:25:44Z +- **Completed:** 2026-10-28T13:37:30Z +- **Tasks:** 3 +- **Files modified:** 18 + +## Accomplishments + +- `pact` publishes `MemberTokenScopeCatalog`, `MCPAdvertisedScopes`, `MCPCall` (`WithMCPCall` / `MCPCallFrom`) and `FrontendTokenScope.Implies`. `MCPTokenVerifier` is still only `MCPVerifyToken` and `MCPResource`. +- An empty or nil MCP `Options.ScopesSupported` resolves to `read`, `write`, `ai`, `offline_access`, `backend` for both metadata and the authorize allow-list. `DefaultOptions` stays the four-name list and still refuses `backend`. OIDC discovery stays `openid`, `profile`, `email`. +- pier copies `MCPScopesSupported` into protected-resource metadata when that slice is non-empty, otherwise `PRMScopes`. `dispatcher.run` stores `MCPCall` immediately before `bouncer.WithDispatch`. Stateless calls leave `SessionID` empty. +- `PendingRequestView.OfflineAccess` is the pending row's flag. `IssueCode`'s signature is unchanged. + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Tracer — one advertised scope list and one MCP call on the dispatch context** - `63d05e8` (feat) +2. **Task 2: Expose offline_access on the pending consent view** - `9e29d6f` (feat) +3. **Task 3: Document the seam in READMEs and docs** - `876f0f0` (docs) + +## Files Created/Modified + +- `modules/pact/mcp.go` - catalog and advertised-scope interfaces, `MCPCall`, `WithMCPCall`, `MCPCallFrom` +- `modules/pact/capabilities.go` - `FrontendTokenScope.Implies` +- `modules/pact/mcp_call_test.go` - round-trip and absent-context cases +- `modules/wristband/server.go` - unexported `resolvedScopes` used by metadata +- `modules/wristband/authorize.go` - allow-list uses that same slice +- `modules/wristband/consent.go` - `PendingRequestView.OfflineAccess` +- `modules/wristband/mcp_scopes_smoke_test.go` - empty list expects the five names and accepts backend +- `modules/pier/prm.go` - copy `MCPScopesSupported` or keep `PRMScopes` +- `modules/pier/dispatch.go` - `WithMCPCall` immediately before `WithDispatch`; blank the ephemeral audit word +- `modules/pier/session.go` - copy the SDK session id only when the call is not stateless +- `modules/pier/stateless.go` - mark `callInfo.stateless` before the handler runs +- `modules/pier/mcp_seam_test.go` - metadata, PRM and one tool call that observes `MCPCall` +- `modules/pact/README.md`, `modules/pier/README.md`, `modules/wristband/README.md` - the new identifiers and the advertised-set rule +- `docs/services/mcp.md`, `docs/services/oauth-server.md`, `docs/services/authentication.md` - the same rule, `MCPCallFrom`, and the unchanged ceiling and OIDC list + +## Decisions Made + +None beyond the plan's recorded discretion. `MemberTokenScopeCatalog.MemberTokenScopes` is not named `FrontendTokenScopes`. `MCPAdvertisedScopes` is a separate interface. Empty MCP options resolve to the five fixed names; a non-empty list is cloned as given. `filter.go` and `oidc_options.go` were not edited. + +## Deviations from Plan + +None - plan executed exactly as written. + +## Issues Encountered + +None + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness + +The framework seam for SC-1 and SC-2 is in place. Later plans in this phase can publish the member-token catalog, build the advertised list from `golem15.oauthserver.mcp_scopes`, and read `MCPCallFrom` on a dispatched route. The read/write ceiling and the OIDC discovery list are unchanged. + +## Self-Check: PASSED + +- FOUND: modules/pact/mcp.go +- FOUND: modules/pact/mcp_call_test.go +- FOUND: modules/pier/mcp_seam_test.go +- FOUND: modules/wristband/consent.go +- FOUND: modules/pact/README.md +- FOUND: docs/services/mcp.md +- FOUND: 63d05e8 +- FOUND: 9e29d6f +- FOUND: 876f0f0 + +--- +*Phase: 32.1-mcp-oauth-for-plugin-apps-declared-scopes-mcp-subject-on-con* +*Completed: 2026-10-28*