docs(19.7): gap-closure inputs: review findings and two sm-servers-app escalations
This commit is contained in:
64
.planning/phases/19.7-generic-mcp-module/19.7-GAP-INPUTS.md
Normal file
64
.planning/phases/19.7-generic-mcp-module/19.7-GAP-INPUTS.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Phase 19.7 gap-closure inputs
|
||||
|
||||
Recorded 2026-10-23 from the user's answer at the code-review gate. The gap-closure plans cover three sources:
|
||||
|
||||
1. **Code review** (`19.7-REVIEW.md`): fix every critical and warning finding (3 critical, 17 warnings) before v0.5.0. Info findings are fixed where cheap; the rest go to `deferred-items.md` with a reason.
|
||||
2. **Escalation E-1** (sm-servers-app): organisation logo.
|
||||
3. **Escalation E-2** (sm-servers-app): plugin-declared scopes for member (`usr_`) API tokens.
|
||||
|
||||
sm-user-plugin is a core plugin used by many applications: every change is additive (no removed export, route, column or payload field; no changed response shape).
|
||||
|
||||
v0.5.0 is tagged only after the gap closure, a fresh code review of the changes, phase verification, Nyquist validation and UAT are all green (user decision 2026-10-23).
|
||||
|
||||
## E-1: Organisation logo (sm-user-plugin)
|
||||
|
||||
Organisations get a `logo` file field (an image attachment through `lagoon/attach`), editable in the backend Organisations form, so an application can upload an organisation's logo and show it on the frontend. The frontend organisation payloads gain the logo (URL or attachment object, following how the plugin already exposes user avatars), as an added field only.
|
||||
|
||||
## E-2: Plugin-declared scopes for member (`usr_`) API tokens
|
||||
|
||||
Escalated by sm-servers-app (PxPx Servers Inventory). Affects sm-user-plugin v0.1.9+ and `pact.HasTokenScopes`. Priority: blocking for their Phase 02.1 (agent component reporting).
|
||||
|
||||
### Problem
|
||||
|
||||
Member personal API tokens (`usr_`) can only carry the fixed scopes `read`, `write` and `ai`. A plugin cannot declare its own scopes for them.
|
||||
|
||||
- `sm-user-plugin/classes/api_tokens.go`: `AllowedAPITokenScopes = []string{"read", "write", "ai"}`. `MintAPIToken` refuses anything else.
|
||||
- `sm-user-plugin/controllers/api_tokens.go`, `apiTokenScopes`: `POST /_user/api/v1/tokens` answers 422 "The selected scope is invalid." for any other value.
|
||||
- `pact.HasTokenScopes` (`TokenScopes() []pact.TokenScope`) exists, but only `cabana/token_scopes.go` reads it, for the backend `g15_` token catalog. The user plugin ignores it.
|
||||
|
||||
A plugin that wants a narrow agent token (for sm-servers-app: `servers:read` and `servers:write`) has to fall back to the generic `write`. Every app sharing the token table then gives the same scope a different meaning, and a token's scopes no longer say what it is for.
|
||||
|
||||
### Use case
|
||||
|
||||
Organisation members mint a personal token from the app's Nuxt screen and hand it to a recon agent. The agent calls:
|
||||
|
||||
- `GET /_servers/api/v1/servers?host=&port=` with scope `servers:read`
|
||||
- `PUT /_servers/api/v1/servers/{id}/components/report` with scope `servers:write`
|
||||
|
||||
The routes are guarded by `APITokenGuard` plus `middleware.Scope("servers:write")`. The app also checks the member's live role, so no mint-time role check is asked for.
|
||||
|
||||
### Ask
|
||||
|
||||
1. **Declaring:** the user plugin builds its allowed scope list from the built-in `read`, `write` and `ai` plus every active plugin's `pact.HasTokenScopes`. It reuses `pact.TokenScope`, or a user-plugin twin if the backend and frontend catalogs should stay apart. It validates names the way cabana does (pattern, no duplicates, no clash with the built-ins) and fails boot on a bad one.
|
||||
2. **Minting:** `POST /_user/api/v1/tokens` and `MintAPIToken` accept the declared scopes.
|
||||
3. **Checking:** `middleware.Scope` and `APIToken.HasScope` work with the declared scopes, with no special casing.
|
||||
4. **Listing:** `GET /_user/api/v1/tokens` returns them in `scopes`, as it does today.
|
||||
5. **Discovery:** an endpoint (for example `GET /_user/api/v1/tokens/scopes`, behind `jwt.auth`) or an exported helper that returns the allowed scopes with name, translated label and description, so a frontend can render them without hard-coding.
|
||||
|
||||
### Not asked for
|
||||
|
||||
- A mint-time hook to refuse a scope based on the user's role.
|
||||
- Any change to backend `g15_` tokens or the cabana catalog.
|
||||
- Any change to `tokens_valid_after` handling (G8 stays as discussed).
|
||||
|
||||
### Acceptance
|
||||
|
||||
- A plugin that declares `servers:write` can mint a `usr_` token with that scope through `POST /tokens`, and a route with `middleware.Scope("servers:write")` accepts it.
|
||||
- A token without the scope gets 403 "Missing required scope: servers:write".
|
||||
- An undeclared scope still answers 422 at mint time.
|
||||
- Two plugins declaring the same scope name, or a plugin redeclaring `read`, `write` or `ai`, fails boot.
|
||||
- The discovery answer lists the built-ins and the declared scopes with labels.
|
||||
|
||||
### Open design point for the planner
|
||||
|
||||
`pact.HasTokenScopes` today feeds the backend `g15_` catalog in cabana. If the user plugin also reads it, a scope a plugin declares for member tokens would also appear in the admin token catalog, and the reverse. Decide between reusing the interface (one catalog, both token kinds) and a separate frontend interface. The escalation does not ask for any change to the backend catalog, so a separate interface (or an audience field) is likely the safer choice. Record the choice as a decision.
|
||||
Reference in New Issue
Block a user