docs(19.7-01): complete framework seams plan

This commit is contained in:
Jakub Zych
2026-10-22 16:45:12 +02:00
parent fab99e0a00
commit 3746675c72
5 changed files with 406 additions and 18 deletions

View File

@@ -1454,11 +1454,11 @@ Plans:
3. `tools/list` lists only the tools the session's principals may call (frontend permissions through the merged group/user permissions, backend permissions and token scope through cabana), and a call runs through the route's own middleware.
4. An editor signs in once through `wristband` OAuth as a frontend user; with a verified link to a backend user, backend-guarded tools such as Journal's run as that admin, and removing the link or the grant removes them. The phase gets a security review; the MCP SDK dependency has a decision note before code lands; unit tests are the last plan.
**Plans:** 5 plans
**Plans:** 1/5 plans executed
Plans:
**Wave 1**
- [ ] 19.7-01-PLAN.md — Framework seams: bouncer dispatch marker, surf handler lookup and streaming routes, tagged pier mount hook, SDK-free pact MCP contracts, My account contribution seam, prompted record action, wristband scope set
- [x] 19.7-01-PLAN.md — Framework seams: bouncer dispatch marker, surf handler lookup and streaming routes, tagged pier mount hook, SDK-free pact MCP contracts, My account contribution seam, prompted record action, wristband scope set
**Wave 2** *(blocked on Wave 1 completion)*
- [ ] 19.7-02-PLAN.md — The `pier` module behind `summercms_mcp` (go-sdk, hybrid stateful/stateless, Redis sessions, fan-out, PRM) and the framework release carrying 19.6.1 + 19.7-01/02
@@ -1607,7 +1607,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 →
| 19.5. Social login in sm-user-plugin | 5/5 | Complete | 2026-10-21 |
| 19.6. Two-factor auth and device flow in sm-user-plugin | 0/TBD | Not started | - |
| 19.6.1. Backend Administrators screen (INSERTED) | 5/5 | Complete | 2026-10-22 |
| 19.7. Generic MCP module | 0/TBD | Not started | - |
| 19.7. Generic MCP module | 1/5 | In Progress| |
| 19.8. Console commands overhaul (INSERTED) | 6/6 | In Progress| |
| 20. Demo instance with every shared plugin | 0/TBD | Not started | - |
| 30. Płytarium cutover | 0/TBD | Not started | - |

View File

@@ -2,18 +2,18 @@
msd_state_version: "1.0"
milestone: v1.0
current_phase: "19.7"
current_phase_name: generic-mcp-module
current_phase_name: Generic MCP module (INSERTED)
status: executing
stopped_at: Phase 19.6.1 complete, ready to plan Phase 19.3
last_updated: "2026-10-22T13:46:11.458Z"
stopped_at: Completed 19.7-01-PLAN.md
last_updated: "2026-10-22T14:44:55.276Z"
last_activity: 2026-10-22
last_activity_desc: Phase 19.6.1 complete, transitioned to Phase 19.3
state_head: 365342257273448bc9ac8ad6818294e2e659bade
last_activity_desc: Phase 19.7 execution started
state_head: fab99e0a00bf4277ab4e659998267970b4a9aeb6
progress:
total_phases: 45
completed_phases: 37
total_plans: 238
completed_plans: 229
completed_plans: 230
gsd_state_version: "1.0"
milestone_name: milestone
---
@@ -25,14 +25,14 @@ milestone_name: milestone
See: .planning/PROJECT.md (updated 2026-10-22)
**Core value:** An existing WinterCMS-shaped app can be ported plugin by plugin to a single Go binary without its frontend noticing: the PHP version's API contract is the acceptance test.
**Current focus:** Phase 19.3 — Framework transport and infrastructure for WavePath
**Current focus:** Phase 19.7 — Generic MCP module (INSERTED)
## Current Position
Phase: 19.7 (generic-mcp-module) — READY TO EXECUTE
Plan: Not started
Phase: 19.7 (Generic MCP module (INSERTED)) — EXECUTING
Plan: 2 of 5
Status: Ready to execute
Last activity: 2026-10-22 — Phase 19.6.1 complete, transitioned to Phase 19.3
Last activity: 2026-10-22 — Phase 19.7 execution started
Progress: [████████░░] 82%
@@ -276,6 +276,7 @@ Progress: [████████░░] 82%
| Phase 19.8 P05 | 40min | 7 tasks | 33 files |
| Phase 19.6.1 P04 | 18 min | 3 tasks | 10 files |
| Phase 19.6.1 P05 | 4 min | 2 tasks | 7 files |
| Phase 19.7 P01 | 52 min | 5 tasks | 64 files |
## Accumulated Context
@@ -942,6 +943,8 @@ Recent decisions affecting current work:
- [Phase 19.6.1]: cabana wraps contributed Administrators Runs; guardOutranked stays unexported and contributors do not own the check — D-19 gap contract: wrap in cabana so a contribution to summercms.cabana.users cannot skip outranking
- [Phase 19.6.1]: Persist picked relation labels on the form via FORM_REMEMBER_LABEL; keep the one-panel FormGrid (do not v-show inactive tabs) — UAT G-19.6.1-T2 is labeling only; v-show of inactive tabs would change trigger hide-tab, ML locale reset, and field mount cost
- [Phase 19.6.1]: Administrators and Roles are cabana screens behind backend.manage_users / backend.manage_roles with no default role grant; privilege rules run in the save tx; last-superuser uses pg_advisory_xact_lock; contributed Administrators Runs are wrapped with guardOutranked.
- [Phase 19.7]: 19.7-01: planner decisions P-01..P-04 implemented: bouncer.Dispatch context-only marker honoured by Registry.Middleware and cabana.ResolveBackend; account contributions as pact.HasAccountContributions; prompted record actions post payload as a JSON string; pact MCP types carry the principal as any and stay SDK-free
- [Phase 19.7]: 19.7-01: wristband authorize allow-list is exactly Options.ScopesSupported (empty keeps the PHP default); surf stream routes recover with an opaque 500 only before the first byte; account contribution Run runs without a transaction only when no database is published
### Pending Todos
@@ -1041,6 +1044,6 @@ Items acknowledged and carried forward from previous milestone close:
## Session Continuity
Last session: 2026-10-22T13:36:16.546Z
Stopped at: Phase 19.6.1 complete, ready to plan Phase 19.3
Last session: 2026-10-22T14:44:54.330Z
Stopped at: Completed 19.7-01-PLAN.md
Resume file: None

View File

@@ -1,10 +1,10 @@
---
schema_version: 1
open_count: 16
open_count: 20
waived_count: 0
fixed_count: 7
total_count: 23
last_updated: 2026-10-18T13:42:55.096Z
total_count: 27
last_updated: 2026-10-22T14:44:56.679Z
---
# Broken Windows Ledger
@@ -38,6 +38,10 @@ last_updated: 2026-10-18T13:42:55.096Z
| 21 | 16.2 | unrun-verify | .planning/phases/16.2-plugin-provided-field-types-and-the-blog-editor-formwidget/16.2-06-PLAN.md | | sm-journal-plugin: 16.2-06 TestCategoriesAdminBlogEditor (categories_admin_editor_test.go) and the rewritten three-stage TestPostsAdminResaveKeepsHTML compile under go vet but need a Docker host; run bash scripts/check-phase16.2.sh postgres | open | | 2026-10-10T14:17:06.621Z | |
| 22 | 19.1 | deviation | sm-forum-plugin/go.mod | | go.mod lists bluemonday and goldmark as // indirect via breakwater; no direct require | open | | 2026-10-18T13:42:54.900Z | |
| 23 | 19.1 | deviation | classes/lifecycle.go | | Task 4 methods besides DeleteContext live in classes/lifecycle.go | open | | 2026-10-18T13:42:55.096Z | |
| 24 | 19.7 | deviation | modules/cabana | | 19.7-01: capabilities_test field-count assertion updated 2->3 for AdminRecordActionInput.Value | open | | 2026-10-22T14:44:56.001Z | |
| 25 | 19.7 | deviation | modules/cabana | | 19.7-01: admin route inventories (phase09Routes, tokenRouteClasses, CSRF walk 35, phase10 want list, conformCases) extended for the two account contribution routes | open | | 2026-10-22T14:44:56.218Z | |
| 26 | 19.7 | deviation | modules/cabana | | 19.7-01: TestRecordActionPromptRun added on Postgres to prove Run receives the trimmed prompt value | open | | 2026-10-22T14:44:56.449Z | |
| 27 | 19.7 | unrun-verify | scripts/check-phase19.6.1.sh | | 19.7-01: check-phase19.6.1.sh --all stopped by the host (memory pressure) after the cabana and surf Postgres suites passed; re-run on the Docker host | open | | 2026-10-22T14:44:56.679Z | |
````json
[
@@ -339,6 +343,58 @@ last_updated: 2026-10-18T13:42:55.096Z
"recorded_at": "2026-10-18T13:42:55.096Z",
"resolved_at": null,
"milestone": "v1.0"
},
{
"id": 24,
"kind": "deviation",
"phase": "19.7",
"file": "modules/cabana",
"line": null,
"description": "19.7-01: capabilities_test field-count assertion updated 2->3 for AdminRecordActionInput.Value",
"status": "open",
"reason": "",
"recorded_at": "2026-10-22T14:44:56.001Z",
"resolved_at": null,
"milestone": "v1.0"
},
{
"id": 25,
"kind": "deviation",
"phase": "19.7",
"file": "modules/cabana",
"line": null,
"description": "19.7-01: admin route inventories (phase09Routes, tokenRouteClasses, CSRF walk 35, phase10 want list, conformCases) extended for the two account contribution routes",
"status": "open",
"reason": "",
"recorded_at": "2026-10-22T14:44:56.218Z",
"resolved_at": null,
"milestone": "v1.0"
},
{
"id": 26,
"kind": "deviation",
"phase": "19.7",
"file": "modules/cabana",
"line": null,
"description": "19.7-01: TestRecordActionPromptRun added on Postgres to prove Run receives the trimmed prompt value",
"status": "open",
"reason": "",
"recorded_at": "2026-10-22T14:44:56.449Z",
"resolved_at": null,
"milestone": "v1.0"
},
{
"id": 27,
"kind": "unrun-verify",
"phase": "19.7",
"file": "scripts/check-phase19.6.1.sh",
"line": null,
"description": "19.7-01: check-phase19.6.1.sh --all stopped by the host (memory pressure) after the cabana and surf Postgres suites passed; re-run on the Docker host",
"status": "open",
"reason": "",
"recorded_at": "2026-10-22T14:44:56.679Z",
"resolved_at": null,
"milestone": "v1.0"
}
]
````

View File

@@ -0,0 +1,305 @@
---
phase: 19.7-generic-mcp-module
plan: 01
subsystem: auth
tags: [bouncer, surf, cabana, pact, wristband, cooler, mcp, admin-spa, streaming, account-contributions]
requires:
- phase: 19.6.1
provides: pact.HasAdminActionExtensions record-action seam, Administrators screen, guardOutranked wrap
- phase: 19.4
provides: wristband MCP Server with Options.ScopesSupported
- phase: 16.3
provides: cabana personal API tokens, interactiveOnly, ResolveBackend
provides:
- bouncer.Dispatch / WithDispatch / Dispatched (D-17 context-only pre-resolved principal marker), honoured by Registry.Middleware and cabana.ResolveBackend
- cabana.ResolveBackendToken, ScopeLimitFor, BackendPrincipal (additive resolvers for pier)
- surf.Router.Handler and compileHandlers (one compiled chain per route), surf.Router.MountStream and recoverStream (unbuffered route kind), surf.MCPBuildTag and the mountPier hook (D-30, D-33)
- wristband parseAuthorizeScopes over Options.ScopesSupported (D-21 framework side); cooler.KeyPrefix
- pact MCP contracts (HasMCP, MCPTool, MCPPrompt, MCPGuard(s), MCPTokenVerifier, MCPPersonalTokenResolver, MCPLinkedBackendResolver, MCPSubjectChanged, FrontendPermission) with no SDK, jsonschema-go or bouncer import
- pact.HasAccountContributions seam and the cabana routes GET /account/contributions, POST /account/contributions/{name}/actions/{action} (D-04 part 1)
- prompted record actions (pact.AdminActionPrompt, AdminRecordActionInput.Value, RecordAction.prompt, CRUDService.RecordActionWithValue) and backend pact.MCPSubjectChanged events (D-34, D-26)
- admin SPA AccountContributions card, ConfirmDialog prompt input, RecordActions payload; rebuilt modules/boardwalk/dist
affects: [19.7-02 pier module and framework release, 19.7-03 sm-user-plugin link and frontend permissions, 19.7-04 oauthserver and Journal, 19.7-05 unit tests and security review]
actuals:
tokens: 55621
tasks: 5
commits: 6
plan_head_before: a74a218cd99a04eb29fc66525f04f0be7e453b9f
plan_head_after: fab99e0a00bf4277ab4e659998267970b4a9aeb6
tech-stack:
added: []
patterns:
- "Context-only dispatch marker under an unexported key, honoured only for the exact guard names it lists; no header, cookie or query is ever read for it"
- "surf compiles each route chain once (compileHandlers) and exposes it by method + pattern for in-process dispatch; the mux reuses the same chains"
- "Build-tag hook: a package-level func var set by the tagged file, nil in the untagged one; config true without the tag fails boot naming the tag"
- "Plugin-facing MCP types live in pact as plain structs and interfaces (Principal carried as any) so no plugin links the SDK"
- "Boot-time compile of plugin contributions with cabana: plugin %s ... error prefixes and per-request translateKey for labels"
- "Prompted confirm: ConfirmRequest.prompt + useConfirm.value; the dialog owns the input, the caller reads the value inside its action"
key-files:
created:
- modules/bouncer/dispatch.go
- modules/cabana/token_resolver.go
- modules/cabana/account_contributions.go
- modules/cabana/subject_events.go
- modules/surf/streaming.go
- modules/surf/pier_off.go
- modules/pact/mcp.go
- modules/wristband/mcp_scopes_smoke_test.go
- admin/src/components/account/AccountContributions.vue
- admin/tests/account/AccountContributions.test.ts
- admin/tests/form/RecordActionsPrompt.test.ts
- modules/cabana/record_action_prompt_postgres_test.go
modified:
- modules/bouncer/registry.go
- modules/cabana/token_guard.go
- modules/cabana/actions.go
- modules/cabana/crud.go
- modules/cabana/schema_types.go
- modules/cabana/http.go
- modules/cabana/admin_openapi.go
- modules/cabana/backend_users_admin.go
- modules/cabana/backend_roles_admin.go
- modules/cabana/account_tokens.go
- modules/cabana/contracts.go
- modules/surf/router.go
- modules/surf/routetable.go
- modules/wristband/authorize.go
- modules/cooler/cooler.go
- modules/pact/capabilities.go
- admin/src/components/ui/confirm.ts
- admin/src/components/ui/ConfirmDialog.vue
- admin/src/components/form/RecordActions.vue
- admin/src/views/AccountView.vue
- admin/openapi/admin.json
- admin/src/api/schema.d.ts
- modules/boardwalk/dist
key-decisions:
- "P-01 implemented: a prompted record action is pact.AdminRecordAction.Prompt; the SPA posts the value as the existing body key payload (JSON string) and cabana accepts payload only for such an action, trimmed, non-empty, capped at MaxLength runes (0 means 255)"
- "P-02 implemented: My account contributions are a plugin interface (pact.HasAccountContributions), not an AdminController extension; cabana never names the contributing plugin"
- "P-03 implemented: bouncer.Dispatch{Guards, Principal, Credential}; Dispatched answers only for a guard name the marker lists exactly; cabana.ResolveBackend honours a backend marker and refuses a frontend one before reading any header"
- "P-04 implemented: every plugin-facing MCP type is in pact with Principal as any; a go list -deps test keeps pact free of the SDK, jsonschema-go and bouncer"
- "An account contribution action runs inside lagoon.Transaction when the app publishes a *gorm.DB; an admin booted without a database runs Run on the request context, so the -short smoke exercises the whole handler"
- "Backend subject events fire inside the saving transaction through app.Events; a listener error is logged, never fails the save (every MCP request re-checks anyway, D-08)"
- "wristband's authorize allow-list is exactly Options.ScopesSupported; an empty list keeps the PHP default so TestPhase08Coverage and every existing test are unchanged"
- "The stream route kind recovers a panic with the opaque 500 only before the first header, byte or flush; after that it only cuts the body short"
patterns-established:
- "Tracer first: Task 1 proved marker -> compiled chain -> guard -> handler end to end before any expansion"
- "Route inventories (phase09Routes, tokenRouteClasses, phase10 CSRF walk and want list, conformCases) are extended in the same commit as a new admin route"
requirements-completed: [SC-1, SC-3, SC-4, D-04, D-09, D-17, D-28, D-30, D-33, D-34, D-36]
coverage:
- id: D1
description: "In-process dispatch marker: a marker-carrying request runs a guarded route through surf's compiled chain as the pre-resolved principal; forged headers and cookies are 401; cabana.ResolveBackend honours a backend marker and refuses a frontend one"
requirement: "D-17"
verification:
- kind: unit
ref: "modules/surf/dispatch_smoke_test.go#TestDispatchSmoke"
status: pass
- kind: unit
ref: "modules/bouncer/dispatch_smoke_test.go#TestDispatchedSmoke"
status: pass
- kind: unit
ref: "modules/cabana/dispatch_smoke_test.go#TestDispatchMarkerSmoke"
status: pass
human_judgment: false
- id: D2
description: "surf.Router.Handler lookup, MountStream streaming route kind flushing mid-handler with panic recovery before and after the first byte, mcp.enabled inert without the tag and a boot error naming summercms_mcp with it"
requirement: "SC-1"
verification:
- kind: unit
ref: "modules/surf/streaming_smoke_test.go#TestStreamingRouteSmoke"
status: pass
- kind: unit
ref: "modules/surf/pier_mount_smoke_test.go#TestPierMountSmoke"
status: pass
human_judgment: false
- id: D3
description: "wristband authorize accepts exactly Options.ScopesSupported (backend only when listed, default unchanged); cooler.KeyPrefix exported"
requirement: "D-36"
verification:
- kind: unit
ref: "modules/wristband/mcp_scopes_smoke_test.go#TestMCPScopesSmoke"
status: pass
- kind: unit
ref: "go test -count=1 -short ./modules/wristband/... ./modules/cooler/..."
status: pass
human_judgment: false
- id: D4
description: "SDK-free pact contracts for MCP tools, prompts, guards, verifiers, link resolver, subject events, frontend permissions, prompted record actions and account contributions, discoverable by type assertion"
requirement: "D-28"
verification:
- kind: unit
ref: "modules/pact/capabilities_test.go#TestPhase197ContractsOnSamplePlugin"
status: pass
- kind: unit
ref: "modules/pact/capabilities_test.go#TestMCPGuardsRegistry"
status: pass
- kind: unit
ref: "modules/pact/capabilities_test.go#TestPactStaysSDKFree"
status: pass
human_judgment: false
- id: D5
description: "cabana lists and runs plugin account contributions behind interactiveOnly, refuses personal tokens, 403/404/409 paths, boot refuses duplicate or malformed sections; existing /account*, /auth/me and record payloads unchanged (account.go byte-identical to 51d088a)"
requirement: "D-04"
verification:
- kind: unit
ref: "modules/cabana/account_contributions_smoke_test.go#TestAccountContributionsSmoke"
status: pass
- kind: integration
ref: "modules/cabana/openapi_conformance_test.go#TestPhase10OpenAPIConformance (GET /account/contributions, POST .../actions/{action} cases)"
status: pass
- kind: other
ref: "git diff --quiet 51d088a -- modules/cabana/account.go"
status: pass
human_judgment: false
- id: D6
description: "Prompted record action: the record response carries prompt with max_length only for prompted actions, the route accepts payload only for them (422 on empty, non-string, over-long), Run receives the trimmed Value, plain actions keep refusing payload"
requirement: "D-34"
verification:
- kind: unit
ref: "modules/cabana/record_action_prompt_smoke_test.go#TestRecordActionPromptSmoke"
status: pass
- kind: integration
ref: "modules/cabana/record_action_prompt_postgres_test.go#TestRecordActionPromptRun"
status: pass
human_judgment: false
- id: D7
description: "Backend pact.MCPSubjectChanged events on token revoke, Administrators saves changing role/permissions/superuser, Roles saves changing grants, deactivation and deletion"
requirement: "D-36"
verification:
- kind: integration
ref: "go test -count=1 ./modules/cabana (Postgres; the 19.6.1 Administrators and Roles suites pass with the hooks in place)"
status: pass
human_judgment: true
rationale: "The hooks compile and the lifecycle suites pass through them, but no test yet asserts the fired events themselves; plan 05 (unit tests) adds the listener assertions"
- id: D8
description: "Admin SPA: My account shows contributed sections with status, actions, toast, new-tab URL and visible link; the confirm dialog of a prompted record action shows one text input, confirm disabled until typed, posts {payload}; plain actions post {}; dist rebuilt and matching"
requirement: "SC-4"
verification:
- kind: unit
ref: "admin/tests/account/AccountContributions.test.ts"
status: pass
- kind: unit
ref: "admin/tests/form/RecordActionsPrompt.test.ts"
status: pass
- kind: other
ref: "scripts/check-admin-dist.sh"
status: pass
human_judgment: true
rationale: "The plan's human-check (an unchanged My account page without contributors, the prompt input on a preview screen) is visual; plan 03's user plugin provides the first real contributor and prompted action to look at"
duration: 52 min
completed: 2026-10-22
status: complete
---
# Phase 19.7 Plan 01: Framework seams for the MCP module Summary
**D-17 dispatch marker in bouncer and cabana, surf's compiled-chain lookup plus an unbuffered streaming route kind and the tagged pier hook, SDK-free pact MCP contracts, the My account contribution seam with prompted record actions on cabana and in the SPA, wristband's configurable scope set and cooler.KeyPrefix, all additive and all green on the existing suites**
## Performance
- **Duration:** 52 min
- **Started:** 2026-10-22T13:54:19Z
- **Completed:** 2026-10-22T14:46:00Z
- **Tasks:** 5
- **Files modified:** 64 hand-written (68 with the rebuilt dist)
## Accomplishments
- Tracer (Task 1): a request carrying `bouncer.WithDispatch` runs `GET /acme/items/{id}` through `surf.Router.Handler` as principal 42 with the path value set; the same request without the marker, with forged `X-Summer-Dispatch`/`Authorization: Dispatch`/cookie headers, or with a marker for another guard is 401. `cabana.ResolveBackend` honours a `backend` marker, refuses a frontend one and keeps the cover check on a scoped principal. Additive `ResolveBackendToken`, `ScopeLimitFor`, `BackendPrincipal` reuse the published resolver guard.
- surf: `compileHandlers` builds each chain once for both BuildRouter's validation and `compile`; `MountStream` + `recoverStream` serve on the real writer (first event received while the handler waits; a buffered route in the same test holds even its headers); `mountPier` hook with `mcp.enabled` false inert and the untagged error naming `summercms_mcp`.
- wristband: `parseAuthorizeScopes(raw, allowed)` with `allowedAuthorizeScopes()` from `Options.ScopesSupported`; `backend` accepted only when a host lists it, metadata lists it, default server still answers `scope contains an unsupported value`. `cooler.KeyPrefix` exported.
- pact: every MCP, frontend-permission, account-contribution and prompt type with doc comments naming the boot failures; `TestPactStaysSDKFree` runs `go list -deps`.
- cabana: `compileAccountContributions` at Activate, the two account routes (swag-annotated, conformance and inventory tests extended), `RecordAction.prompt`, `promptValue` validation (`backend::lang.form.action_prompt_required` / `_too_long` en+pl), `RecordActionWithValue`, `fireSubjectChanged` wired into token revoke, Administrators and Roles saves, deactivate and delete (record and bulk through the shared lifecycle `run`). `account.go` untouched.
- SPA: `AccountContributions.vue` mounted after `ApiTokensPanel`, prompt input in `ConfirmDialog`, `RecordActions` posting `{payload}` or `{}`, 1490 vitest tests green, dist rebuilt and matching.
## Task Commits
1. **Task 1: Tracer, dispatch marker and compiled handler lookup** - `1bd91a4` (feat)
2. **Task 2: Streaming routes, MCP mount hook, configurable authorize scopes** - `5c15dea` (feat)
3. **Task 3: SDK-free pact contracts** - `6887abf` (feat)
4. **Task 4: Account contributions, prompted record actions, backend subject events** - `9f14cf6` (feat)
5. **Task 5: Admin SPA** - `9a5f27e` (feat) and `fab99e0` (chore: rebuilt `modules/boardwalk/dist`)
**Plan metadata:** see the docs commit after this file.
## Files Created/Modified
- `modules/bouncer/dispatch.go` - `Dispatch`, `WithDispatch`, `Dispatched`; `registry.go` honours it before `authenticate`
- `modules/cabana/token_guard.go` - marker branch in `ResolveBackend`; `logTokenAuthAt`; `token_resolver.go` - the three additive resolvers
- `modules/surf/router.go`, `routetable.go`, `streaming.go`, `pier_off.go` - handler map, `Handler`, stream kind, `MCPBuildTag`, `mountPier`
- `modules/wristband/authorize.go` - allow-list from options; `modules/cooler/cooler.go` - `KeyPrefix`
- `modules/pact/mcp.go`, `capabilities.go` - the contracts
- `modules/cabana/account_contributions.go`, `subject_events.go`, `actions.go`, `crud.go`, `schema_types.go`, `http.go`, `admin_openapi.go`, `backend_users_admin.go`, `backend_roles_admin.go`, `account_tokens.go`, `contracts.go`, `action_extensions.go`, `extension.go`
- `modules/phrasebook/backend/lang/{en,pl}/lang.yaml` - the two prompt phrases
- `admin/src/components/ui/confirm.ts`, `ConfirmDialog.vue`, `form/RecordActions.vue`, `account/AccountContributions.vue`, `views/AccountView.vue`, `api/types.ts`; regenerated `admin/openapi/admin.json`, `admin/src/api/schema.d.ts`; rebuilt `modules/boardwalk/dist`
- READMEs of bouncer, cabana, surf, wristband, cooler, pact; `docs/services/{authentication,routing,oauth-server}.md`, `docs/plugins/registration.md`, `docs/backend/{admin-controllers,users-and-permissions}.md`
- Smoke tests: `TestDispatchSmoke`, `TestDispatchedSmoke`, `TestDispatchMarkerSmoke`, `TestStreamingRouteSmoke`, `TestPierMountSmoke`, `TestMCPScopesSmoke`, `TestAccountContributionsSmoke`, `TestRecordActionPromptSmoke` (all `-short`), `TestRecordActionPromptRun` (Postgres), two vitest files
## Decisions Made
See `key-decisions` above: planner decisions P-01 to P-04 implemented as written; the account action transaction fallback without a database; events fired inside the saving transaction; the stream recovery rule; the wristband default kept.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] `TestPhase121ContractsOnSampleController` asserted `AdminRecordActionInput` has 2 fields**
- **Found during:** Task 3
- **Issue:** the test pins the input's shape; this plan adds `Value`
- **Fix:** assertion updated to 3 with the comment that there is still no id list
- **Files modified:** modules/pact/capabilities_test.go
- **Verification:** `go test -short ./modules/pact/...`
- **Committed in:** 6887abf
**2. [Rule 3 - Blocking] Admin route inventories refused the two new routes**
- **Found during:** Task 4
- **Issue:** `phase09Routes`, `tokenRouteClasses`, the CSRF walk count (34), the phase 10 unsafe-route `want` list and `conformCases` each fail when a route is added without an entry
- **Fix:** entries added (both routes `classInteractive`; CSRF count 35; conformance cases with a section the `conformPlugin` contributes), so the gates keep protecting the inventory
- **Files modified:** modules/cabana/security_coverage_test.go, token_routes_test.go, phase10_csrf_test.go, phase10_coverage_test.go, openapi_conformance_test.go
- **Verification:** `go test -short ./modules/cabana/` and the Postgres conformance run
- **Committed in:** 9f14cf6
**3. [Rule 2 - Missing critical] Postgres proof that `Run` receives the prompt value**
- **Found during:** Task 4
- **Issue:** there is no DB-free GORM driver, so the `-short` smoke can prove the route's validation and the show payload but not the `Run` call; the plan's "hands Value == acme to Run" needs a record load
- **Fix:** `TestRecordActionPromptRun` on the 19.6.1 `screensEnv` harness (skips under `-short`), run through Docker and green
- **Files modified:** modules/cabana/record_action_prompt_postgres_test.go
- **Verification:** `go test -count=1 -run TestRecordActionPromptRun ./modules/cabana`
- **Committed in:** 9f14cf6
---
**Total deviations:** 3 auto-fixed (1 bug in a shape-pinning test, 1 blocking inventory, 1 missing proof)
**Impact on plan:** No scope change; every deviation keeps an existing gate meaningful or adds the proof the plan asked for.
## Issues Encountered
- `TestStreamingRouteSmoke` first deadlocked: the buffered comparison route holds even its response headers until the handler returns, so `client.Get` blocks; the GET now runs in a goroutine and the test asserts nothing arrives within 300 ms, then everything after the release.
- `scripts/check-phase19.6.1.sh --all` (the plan's Docker-host gate) was stopped by the host while the session was idle, after `go test -count=1 ./modules/cabana ./modules/surf` had already passed (93.9 s and 15.5 s). Its `sc1` stage (account.go untouched) is proven separately by `git diff --quiet 51d088a -- modules/cabana/account.go`; the full gate should be re-run on the Docker host before the plan 02 release.
- The plan's acceptance grep `json:"prompt,omitempty"` counts two lines in `schema_types.go`; the second is the pre-existing `FormField.Prompt` tag (logged in `deferred-items.md`, resolved).
## User Setup Required
None - no external service configuration required.
## Known Stubs
None. `AccountContributions.vue` renders nothing on an empty or failed list by design (a page without contributors is unchanged).
## Next Phase Readiness
- Plan 02 (pier) can build on: `surf.Router.Handler` + `MountStream` + `mountPier` (set in a `pier_on.go` under `//go:build summercms_mcp`), `bouncer.WithDispatch`, `cabana.ResolveBackendToken`/`ScopeLimitFor`/`BackendPrincipal`, `pact.MCPGuards` (find-or-publish), `pact.MCPSubjectChanged` listeners, `cooler.KeyPrefix`.
- Plan 03 (sm-user-plugin): `pact.HasFrontendPermissions`, `pact.HasAccountContributions` for "Link frontend account", a prompted `link_frontend_account` and a plain `unlink_frontend_account` on `cabana.AdministratorsControllerID`, `pact.MCPLinkedBackendResolver`, `pact.MCPPersonalTokenResolver`.
- Plan 04 (oauthserver): append `backend` to the MCP server's `ScopesSupported`; publish `pact.MCPTokenVerifier`.
- Plan 05: unit tests for the seams (event assertions, streaming edge cases), the gate grep that only `modules/pier` calls `WithDispatch` outside tests, and the deferred `gofmt` drift in cabana (`deferred-items.md`).
- Re-run `bash scripts/check-phase19.6.1.sh --all` on the Docker host (stopped by the host this session, see Issues).
---
*Phase: 19.7-generic-mcp-module*
*Completed: 2026-10-22*
## Self-Check: PASSED
All 12 listed created files exist on disk and all 6 task commits (1bd91a4, 5c15dea, 6887abf, 9f14cf6, 9a5f27e, fab99e0) are in history; measured commits a74a218..HEAD = 6.

View File

@@ -0,0 +1,24 @@
# Deferred items
Out-of-scope discoveries made while executing phase 19.7 plans. Nothing here
was changed by the plan that recorded it.
## Deferred Items
- Pre-existing `gofmt` drift in cabana (found during 19.7-01 Task 1)
status: open
**What:** `gofmt -l modules/cabana` lists `account_tokens.go`,
`account_tokens_test.go`, `field_trigger_smoke_test.go`,
`token_postgres_smoke_test.go`, `token_routes_test.go` and
`token_scopes_test.go` as unformatted before this plan touched the module
(alignment of map literals and struct tags). Left as is: reformatting
would add diff noise unrelated to the plan (a whole-file `gofmt -w` on
`token_routes_test.go` rewrote 132 lines and was reverted). A one-line
`gofmt -w` on those six files in a `style(cabana)` commit of its own fixes it.
- `AdminRecordAction.Prompt` acceptance grep counted two `prompt,omitempty`
tags in `schema_types.go` (found during 19.7-01 Task 4)
status: resolved
**What:** the second match is the pre-existing `FormField.Prompt` JSON tag
at `schema_types.go:376`, not a duplicate of the new `RecordAction.Prompt`;
the criterion's "prints 1" was written without that line in mind. No change
needed.