diff --git a/.planning/notes/2026-10-23-frontend-token-scopes.md b/.planning/notes/2026-10-23-frontend-token-scopes.md new file mode 100644 index 00000000..06c9f9c3 --- /dev/null +++ b/.planning/notes/2026-10-23-frontend-token-scopes.md @@ -0,0 +1,51 @@ +# Decision: member token scopes use a separate pact capability + +**Date:** 2026-10-23 +**Status:** Accepted +**Context:** Phase 19.7 gap closure, escalation E-2 (`19.7-GAP-INPUTS.md`), planner decision P-30 in plan 19.7-07 Task 2 + +## Context + +sm-servers-app escalated E-2: member personal API tokens (`usr_`), which the user plugin issues, can only carry the fixed scopes `read`, `write` and `ai`. A plugin cannot declare its own, so a narrow agent token (`servers:read`, `servers:write`) falls back to the generic `write`, and every application sharing the token table gives the same scope a different meaning. + +What was asked: + +- the user plugin builds its allowed scope list from the built-ins plus plugin-declared scopes, validates names the way cabana does (pattern, no duplicates, no clash with the built-ins) and fails boot on a bad one; +- minting, checking (`middleware.Scope`, `APIToken.HasScope`) and listing accept the declared scopes with no special casing; +- a discovery endpoint or helper returns the scopes with name, translated label and description. + +What was not asked for: + +- a mint-time hook refusing a scope by the member's role; +- any change to backend `g15_` tokens or the cabana token catalog; +- any change to `tokens_valid_after` handling. + +The escalation left one design point open: reuse `pact.TokenScope`, or a twin for the frontend catalog. + +## Options + +1. **Reuse `pact.HasTokenScopes`.** The user plugin reads the same capability cabana reads. +2. **An audience field on `pact.TokenScope`** (`Audience: "backend" | "frontend"`), with each catalog skipping the other audience's entries. +3. **A separate capability**, `pact.FrontendTokenScope{Name, Label, Description}` and `pact.HasFrontendTokenScopes{ FrontendTokenScopes() []FrontendTokenScope }`, mirroring the existing `pact.Permission` / `pact.FrontendPermission` pair. + +## Decision and reasons + +Option 3, a separate capability. pact also gains `pact.ValidTokenScopeName(name string) bool`, carrying cabana's existing pattern `^[a-z][a-z0-9_-]*(:[a-z][a-z0-9_-]*)?$`, so both catalogs validate names identically. + +1. **The escalation asks for no change to the `g15_` catalog.** Reusing `HasTokenScopes` would show every member scope in the admin's token catalog and every admin scope in the member list. The two token kinds belong to different principals (an administrator against cabana permissions, a member against frontend routes); one list cannot describe both. +2. **The admin type's fields do not apply to a member token.** `pact.TokenScope` carries `Permissions` (cabana permission patterns) and `Methods` (an HTTP method filter). A member route's `user.scope:` middleware alone decides what a member scope allows. cabana's boot validation also requires both fields to be non-empty and the patterns to match registered permissions, so a member scope could not even be declared through it. +3. **An audience field leaves fields meaningless for one audience.** `Permissions` and `Methods` would be dead for `frontend`, cabana would have to skip entries silently, and a typo in the audience would move a scope between catalogs without an error. + +## Consequences + +- The user plugin (sm-user-plugin, plan 19.7-08 Task 3) compiles `HasFrontendTokenScopes` from the active plugins, validates each name with `pact.ValidTokenScopeName`, refuses duplicates and clashes with `read`, `write` and `ai`, and fails boot on a bad scope. It owns minting, checking, listing and the discovery endpoint. +- The framework never reads `HasFrontendTokenScopes`; cabana's `g15_` catalog is unchanged in behaviour and in every error message (it now calls `pact.ValidTokenScopeName` instead of its own copy of the pattern). +- `pact.ValidTokenScopeName` is the single name rule; a future change to the pattern changes both catalogs together. +- Reversibility is costly: `pact.FrontendTokenScope`, `pact.HasFrontendTokenScopes` and `pact.ValidTokenScopeName` are published framework API, so a reshape needs a deprecation cycle. + +## Related + +- `.planning/phases/19.7-generic-mcp-module/19.7-GAP-INPUTS.md`, section "E-2" +- `.planning/phases/19.7-generic-mcp-module/19.7-07-PLAN.md`, P-30 and Task 2 +- `.planning/phases/19.7-generic-mcp-module/19.7-08-PLAN.md`, Task 3 (the user plugin side) +- `modules/pact/capabilities.go`, `modules/cabana/token_scopes.go`