From 94ee2cee037ae8cc6446985f5c2eac9a35d89a77 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Fri, 23 Oct 2026 22:05:22 +0200 Subject: [PATCH] docs(19.7): phase verification (human_needed) --- .../19.7-VERIFICATION.md | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 .planning/phases/19.7-generic-mcp-module/19.7-VERIFICATION.md diff --git a/.planning/phases/19.7-generic-mcp-module/19.7-VERIFICATION.md b/.planning/phases/19.7-generic-mcp-module/19.7-VERIFICATION.md new file mode 100644 index 00000000..50adddf5 --- /dev/null +++ b/.planning/phases/19.7-generic-mcp-module/19.7-VERIFICATION.md @@ -0,0 +1,183 @@ +--- +phase: 19.7-generic-mcp-module +verified: 2026-10-23T22:30:00Z +status: human_needed +score: 7/8 roadmap success-criterion truths verified, 1 needs a human decision (SC-3 wording, override suggested); 66/66 plan must-have truths verified (automated parts; four have browser/real-client UAT items) +covered_files: [".planning/phases/19.7-generic-mcp-module/19.7-01-PLAN.md",".planning/phases/19.7-generic-mcp-module/19.7-01-SUMMARY.md",".planning/phases/19.7-generic-mcp-module/19.7-02-PLAN.md",".planning/phases/19.7-generic-mcp-module/19.7-02-SUMMARY.md",".planning/phases/19.7-generic-mcp-module/19.7-03-PLAN.md",".planning/phases/19.7-generic-mcp-module/19.7-03-SUMMARY.md",".planning/phases/19.7-generic-mcp-module/19.7-04-PLAN.md",".planning/phases/19.7-generic-mcp-module/19.7-04-SUMMARY.md",".planning/phases/19.7-generic-mcp-module/19.7-05-PLAN.md",".planning/phases/19.7-generic-mcp-module/19.7-05-SUMMARY.md",".planning/phases/19.7-generic-mcp-module/19.7-06-PLAN.md",".planning/phases/19.7-generic-mcp-module/19.7-06-SUMMARY.md",".planning/phases/19.7-generic-mcp-module/19.7-07-PLAN.md",".planning/phases/19.7-generic-mcp-module/19.7-07-SUMMARY.md",".planning/phases/19.7-generic-mcp-module/19.7-08-PLAN.md",".planning/phases/19.7-generic-mcp-module/19.7-08-SUMMARY.md",".planning/phases/19.7-generic-mcp-module/19.7-09-PLAN.md",".planning/phases/19.7-generic-mcp-module/19.7-09-SUMMARY.md","README.md","admin/openapi/admin.json","admin/src/api/schema.d.ts","admin/src/api/types.ts","admin/src/app/hotkeys.ts","admin/src/components/account/AccountContributions.vue","admin/src/components/form/RecordActions.vue","admin/src/components/ui/ConfirmDialog.vue","admin/src/components/ui/confirm.ts","admin/src/views/AccountView.vue","admin/tests/account/AccountContributions.test.ts","admin/tests/app/hotkeysEditing.test.ts","admin/tests/fixtures/lang.json","admin/tests/form/RecordActionsPrompt.test.ts","docs/backend/admin-controllers.md","docs/backend/users-and-permissions.md","docs/plugins/registration.md","docs/services/authentication.md","docs/services/mcp.md","docs/services/oauth-server.md","docs/services/routing.md","docs/site.yaml","go.mod","go.sum","modules/bouncer/README.md","modules/bouncer/auth_time_smoke_test.go","modules/bouncer/auth_time_test.go","modules/bouncer/dispatch.go","modules/bouncer/dispatch_smoke_test.go","modules/bouncer/dispatch_test.go","modules/bouncer/jwt.go","modules/bouncer/mint.go","modules/bouncer/refresh.go","modules/bouncer/registry.go","modules/cabana/README.md","modules/cabana/account_contributions.go","modules/cabana/account_contributions_hardening_test.go","modules/cabana/account_contributions_smoke_test.go","modules/cabana/account_contributions_test.go","modules/cabana/account_token_events_test.go","modules/cabana/account_tokens.go","modules/cabana/action_extensions.go","modules/cabana/actions.go","modules/cabana/admin_openapi.go","modules/cabana/administrators_harness_test.go","modules/cabana/backend_roles_admin.go","modules/cabana/backend_users_admin.go","modules/cabana/contracts.go","modules/cabana/crud.go","modules/cabana/dispatch_marker_test.go","modules/cabana/dispatch_smoke_test.go","modules/cabana/extension.go","modules/cabana/http.go","modules/cabana/openapi_conformance_test.go","modules/cabana/phase10_coverage_test.go","modules/cabana/phase10_csrf_test.go","modules/cabana/record_action_prompt_localized_test.go","modules/cabana/record_action_prompt_postgres_test.go","modules/cabana/record_action_prompt_smoke_test.go","modules/cabana/record_action_prompt_test.go","modules/cabana/schema_types.go","modules/cabana/scope_limit_for_test.go","modules/cabana/security_coverage_test.go","modules/cabana/subject_events.go","modules/cabana/subject_events_test.go","modules/cabana/token_guard.go","modules/cabana/token_resolver.go","modules/cabana/token_resolver_remote_test.go","modules/cabana/token_routes_test.go","modules/cabana/token_scope_name_test.go","modules/cabana/token_scopes.go","modules/cooler/README.md","modules/cooler/cooler.go","modules/pact/README.md","modules/pact/capabilities.go","modules/pact/capabilities_test.go","modules/pact/frontend_token_scopes_test.go","modules/pact/mcp.go","modules/phrasebook/backend/lang/en/lang.yaml","modules/phrasebook/backend/lang/pl/lang.yaml","modules/pier/README.md","modules/pier/audit.go","modules/pier/backend_guard.go","modules/pier/catalog.go","modules/pier/catalog_path_fields_test.go","modules/pier/catalog_test.go","modules/pier/config.go","modules/pier/dispatch.go","modules/pier/dispatch_hardening_test.go","modules/pier/dispatch_test.go","modules/pier/endpoint_guards_test.go","modules/pier/events.go","modules/pier/example_mcp_test.go","modules/pier/export_test.go","modules/pier/fanout_test.go","modules/pier/filter.go","modules/pier/fingerprint_test.go","modules/pier/guard_rules_test.go","modules/pier/guards.go","modules/pier/helpers_test.go","modules/pier/host.go","modules/pier/identity.go","modules/pier/identity_smoke_test.go","modules/pier/lock_scope_test.go","modules/pier/personal_binding_test.go","modules/pier/pier.go","modules/pier/prm.go","modules/pier/prompt_ceiling_test.go","modules/pier/reaper_smoke_test.go","modules/pier/reaper_test.go","modules/pier/reserve_test.go","modules/pier/session.go","modules/pier/sessions_smoke_test.go","modules/pier/sessions_test.go","modules/pier/stateless.go","modules/pier/stateless_fanout_test.go","modules/pier/stateless_smoke_test.go","modules/pier/store.go","modules/pier/store_memory.go","modules/pier/store_redis.go","modules/pier/token_events_test.go","modules/pier/tools_list_test.go","modules/pier/tracer_smoke_test.go","modules/pier/widening_test.go","modules/surf/README.md","modules/surf/dispatch_smoke_test.go","modules/surf/pier_mount_smoke_test.go","modules/surf/pier_mount_test.go","modules/surf/pier_off.go","modules/surf/pier_on.go","modules/surf/router.go","modules/surf/routetable.go","modules/surf/streaming.go","modules/surf/streaming_smoke_test.go","modules/surf/streaming_test.go","modules/towel/README.md","modules/towel/client_ip_test.go","modules/towel/context.go","modules/wristband/README.md","modules/wristband/authorize.go","modules/wristband/mcp_scopes_smoke_test.go","modules/wristband/mcp_scopes_test.go","scripts/check-phase19.7.sh"] +covered_digest: "v2:sha256:8b0458a677fb6443ec7e72a71659b194af68e396fe6fd552a4b468d6cc888192" +behavior_unverified: 0 +overrides_applied: 0 +decision_coverage: + honored: 35 + total: 35 + not_honored: [] +human_verification: + - test: "Decide on SC-3's wording: frontend tool filtering uses group grants (what user.permission enforces), not MergedPermissions (groups plus the user's own allow/deny). Accept the override below, or ask for MergedPermissions in both the route middleware and the MCP guard (a behaviour change of a core plugin)." + expected: "An override entry is added to this frontmatter (suggested YAML in the report), or a follow-up is opened" + why_human: "The code follows decision D-35 / P-14 recorded in 19.7-CONTEXT.md and 19.7-RESEARCH.md Open Question 4; ROADMAP SC-3 still says 'merged group/user permissions'. Only the user can accept the deviation (overrides need accepted_by)." + - test: "Real MCP client against the local demo (DEPLOY.md, 'MCP endpoint', 'Locally'): add http://127.0.0.1:8097/mcp to Claude Code or Claude Desktop, sign in on /connect as a frontend user asking for backend, see only user_me; in the admin's My account start 'Link frontend account', finish on /link-admin with a fresh sign-in, reconnect (consent shows 'also act as admin '), see the journal_ tools, create a draft post and find it in the admin; unlink and see the journal_ tools disappear." + expected: "One connector, one OAuth sign-in; journal_ tools appear only with a verified link plus the backend scope and disappear after unlinking (the session ends, a new session lists user_me only)" + why_human: "Plan 04 Task 5 human check, never run (deferred-items.md 'Real MCP client check of the demo not run'). TestMCPEndToEnd mints the access token through the plugin's MCP store instead of a browser consent; the authorize, consent and code-exchange handlers are tested separately (TestMCPBackendBinding). No test drives a real third-party client through the browser pages." + - test: "Consent page with a changed link, in a browser: open /connect for a request asking for backend, then change the user's link to another administrator (Administrators screen) before clicking Allow." + expected: "The decision answers 409 backend_link_changed, the page reloads the request and shows the linkChanged notice naming the new administrator; no grant is issued for the old one" + why_human: "The 409 is pinned server-side (TestMCPBackendBinding 'decide refuses an administrator the page did not show') and consentDecisionBody by consent.test.ts; the page's reload and notice are UI behaviour in a real browser" + - test: "Link page 'Signed in as', in a browser: start 'Link frontend account' on My account, open the #code=... URL while signed in as a different frontend user than intended." + expected: "/link-admin shows the request and 'Signed in as ' before the confirm button, keeps the code across a sign-in (sessionStorage), and a session older than the request gets the stale-session message" + why_human: "Visual and navigation behaviour of the site page; the server rules (auth_time, POST show, fragment code) are tested in TestBackendLink and linkCode.test.ts" + - test: "RB-WR-01 in Firefox: in the admin, on a record form with a plugin field type that renders inside a shadow root (taglist or another widget), type into the inner input and press Ctrl+S (and Ctrl+Enter) with a pending taglist draft." + expected: "The pending draft is committed before the save; plain keys typed inside the shadow root (letters, Esc, /, ?) trigger no shortcut" + why_human: "Pinned in happy-dom only (admin vitest); shadow-DOM focus and composedPath behaviour differs between happy-dom and real browsers" +--- + +# Phase 19.7: Generic MCP module Verification Report + +**Phase Goal:** A SummerCMS application can expose its plugins' APIs as an opt-in MCP endpoint inside the binary. Plugins declare their tools in Go, and an editor connects one MCP client once to reach both frontend-user tools and the backend tools of their linked admin account. +**Verified:** 2026-10-23 +**Status:** human_needed +**Re-verification:** No, initial verification (no earlier 19.7-VERIFICATION.md) + +Repositories checked: summercms.go (HEAD 33c40ff), sm-user-plugin (sm-demo-app/plugins/golem15/user, 0428093), sm-oauthserver-plugin (9813c1d, go.work locally retargeted as recorded), sm-journal-plugin (521df9a), sm-demo-app (40ed518; plugin submodule checkouts ahead of their pins, main.go regenerated by a stale CLI, both uncommitted), vue-demo-app (9c247ab, the committed pointer). + +## Goal Achievement + +### Observable Truths: roadmap success criteria (the contract) + +| # | Truth | Status | Evidence | +|---|-------|--------|----------| +| SC-1 | An application that enables the module in config serves an MCP endpoint; with it disabled (default) nothing is mounted and nothing changes | ✓ VERIFIED | `modules/surf/router.go:699-706` mounts only when `app.Config.Bool("mcp.enabled")`; nil hook without the tag gives a boot error that names the tag. `modules/pier` is imported only by `modules/surf/pier_on.go` (`//go:build summercms_mcp`); `go list -deps ./modules/surf` lists 0 go-sdk packages untagged and lists them with `-tags summercms_mcp`. I ran `TestPierMount` (untagged: "off leaves the route table unchanged", "enabled without the tag fails boot"; tagged: "enabled mounts the endpoint as stream routes") and `TestPierMountSmoke`: PASS. In the demo, `config/mcp.yaml` sets `enabled: true` and `summer.yaml` sets `build.tags: [summercms_mcp]`; TestMCPEndToEnd serves /mcp and the PRM. | +| SC-2 | A plugin declares tools through an optional pact capability (name, description, input struct reflected to a JSON schema, guard, route); plugins that declare none expose none | ✓ VERIFIED | `modules/pact/mcp.go:18` `HasMCP`, `MCPTool{Name, Title, Description, Guard, Method, Path, Input, Output, Permissions, …}`. pact imports neither the SDK nor jsonschema-go (enforced by `capabilities_test.go:644` and the gate's marker stage). `modules/pier/catalog.go:104` skips plugins without `pact.HasMCP`, and `:199` calls `jsonschema.ForType`. TestCatalog covers prefixing and boot refusals (duplicate, bad name, no token guard and no Guard, path field rules). Declared by user (`user_me`) and Journal (7 tools and 1 prompt). | +| SC-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 | ? UNCERTAIN (human decision) | Filtering is per request identity (`modules/pier/filter.go` `allowedReport`: subject under the guard, read/write ceiling, then the provider). Backend: `backend_guard.go` uses `cabana.BackendPrincipal`, `cabana.Allows` and `cabana.ScopeLimitFor` by method. A call goes through the compiled chain: `dispatch.go:173` `compiledHandler` → `host.Handler`, `:208` `bouncer.WithDispatch` (the only non-test caller across all repos), `:245` `handler.ServeHTTP`. **Deviation:** the frontend check is `classes.PrincipalByID` → `UserPermissionGrants` (group grants only, `user_lookup.go:54-99`), not `MergedPermissions` (`classes/permissions.go:24`, with the user's own allow/deny). This is deliberate: D-35 / P-14 mirror what the `user.permission` middleware enforces, so the listed set equals the callable set. The roadmap wording was never updated. The listing half is VERIFIED; the "merged" clause needs an override (see Gaps Summary). | +| SC-4a | 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 | ✓ VERIFIED (automated) + UAT item | `identity.go:resolveOAuth`/`consentedBackendSubject` gives a backend subject only with the `backend` scope plus a link that names the consented admin (`pact.MCPGrant.BackendUserID`). oauthserver `controllers/mcp_consent_api.go` is built on `wristband.Server`, and `classes/mcp_verifier.go` checks prefix, hash, revoked, expiry, kind=mcp client, active user, ban and cutoff. `TestMCPBackendBinding` runs PKCE authorize → consent decide with the frontend JWT → code exchange → rotation. I ran `TestMCPEndToEnd/journal_tools_as_the_linked_admin`: `journal_create_post` writes a draft authored by the linked admin id. PASS. The real-client and browser run is a human item. | +| SC-4b | Removing the link or the grant removes the backend tools | ✓ VERIFIED | I ran TestMCPEndToEnd "a link change ends the backend grant" (the session ends, the old token gets 401, and relinking does not revive it), "revoked grant ends the session", and TestMCPRevocationEndToEnd "(b) unlinking ends the backend grant…" and "(a) a ban … ends the grant": all PASS. Wiring: oauthserver `plugin.go:103-105` listens on `AccessRevokedEvent`, `BackendLinkedEvent` and `BackendUnlinkedEvent` (`classes/cascade.go`); pier `events.go:45-60` closes or re-filters and `forgetLink` drops the 5 s link cache. | +| SC-4c | The phase gets a security review | ✓ VERIFIED | `19.7-SECURITY-REVIEW.md`: 66 distinct T-19.7-NN ids (through T-19.7-65), with corrected verdicts after the code review. `19.7-SECURITY.md`: `threats_open: 0`, accepted risks AR-19.7-01..03 (superuser verdict AR-19.7-02). Two deep reviews followed: 19.7-REVIEW.md, then 19.7-REREVIEW.md (1 critical, 10 warnings), all fixed with failing-first tests per 19.7-REREVIEW-FIX.md; the residuals are in deferred-items.md. | +| SC-4d | The MCP SDK dependency has a decision note before code lands | ✓ VERIFIED | `.planning/notes/2026-10-22-mcp-go-sdk.md` committed 3ea82a1 at 2026-10-22 18:00:16; the first go.mod/pier code is cfdbaf0 at 18:15:59. The human legitimacy approval of go-sdk v1.8.0 and jsonschema-go v0.4.3 is recorded in the note; STACK.md rows 56-57. | +| SC-4e | Unit tests are the last plan | ✓ VERIFIED | Plan 05 (unit tests, gate, security review) closes the first execution and plan 09 closes the gap closure. The re-review fixes after plan 09 each carry a test, and the gate requires them by name (f824d93). | + +**Score:** 7/8 roadmap truths verified (1 UNCERTAIN pending an override decision); 0 present-but-behavior-unverified. + +### Observable Truths: plan must-haves + +Each plan's truths were checked against the code. Behavior-dependent truths (state transitions, cancellation and cleanup invariants) count as verified only through a named test that the phase gate requires by name and that passed in this run, plus the targeted runs listed under Behavioral Spot-Checks. + +| Plan | Truths | Status | Key evidence | +|------|--------|--------|--------------| +| 01 seams | 7 | ✓ 7/7 | `bouncer/dispatch.go` (unexported key, Guards exact match); `registry.go:86` and `cabana/token_guard.go:209` consult `Dispatched`; `surf/streaming.go` MountStream, `routetable.go` Handler; `pier_off.go`; pact types; `cabana/account_contributions.go` + http.go routes; RecordActions `payload`; wristband `ScopesSupported`. verify.artifacts 10/10, key links 5/5. | +| 02 pier | 10 | ✓ 10/10 | pier.go Mount, catalog, dispatch (recorder, no Authorization/Cookie), session.go (StreamableServerTransport, binding 403), stateless.go, store_redis.go, prm.go, events.go. TestToolsList, TestDispatch, TestSessions, TestFanOut and TestEndpointGuards pass (`go test ./modules/pier`). D-31 release truth: v0.4.4 is an annotated tag on origin (63a19ef), contains 806e87c and the 19.6.1 commits, and the changelog at https://summercms.io/changelog lists v0.4.4. By user decision, v0.5.0 carries the rest. | +| 03 user plugin | 7 | ✓ 7/7 | `classes/backend_links.go` and the migration; `controllers/backend_link_controller.go:104-119` (`VerifyAuthTime`, `ConfirmLinkRequest`); `backend_links_admin.go` (prompted `link_frontend_account`, Danger `unlink_frontend_account`); `plugin.go:153-179` publishes MCPGuards, the resolver and BackendLinks; `users_admin_controller.go:466` FrontendPermissionCatalog; `mcp.go` user_me. Released as v0.1.10, pinned in sm-demo-app, fonoteka.go and sm-bm-app. Group-grant semantics: see SC-3. | +| 04 oauthserver/journal/demo | 7 | ✓ 7/7 (UAT item for /connect) | The verifier, consent `backend_admin` and journal `mcp.go` (`Guard backend`, access_posts) checked by hand (cross-repo links are outside the tool's root). `connect.vue:56,74` calls `/oauth/mcp/requests/`; `summer.yaml` sets tags; `config/mcp.yaml`; the deploy check is OK (gate). Releases: oauthserver v0.1.2 and journal v0.2.3, pinned in the demo. | +| 05 tests/gate/review | 4 | ✓ 4/4 | `scripts/check-phase19.7.sh` (fail-closed, self-test); helpers_test miniredis harness; SECURITY-REVIEW; VALIDATION `nyquist_compliant: true`. | +| 06 pier hardening | 9 | ✓ 9/9 | `session.go:492` reap, started at `pier.go:145`; `store.go:65` Reserve, `session.go:156`; Redis Lua NewScript; filter `Writes`; identity `TokenID == 0` refusal. Gate requires TestSessionReaper, reserve, widening, lock-scope, dispatch hardening and prompt ceiling tests by name. | +| 07 auth/admin | 8 | ✓ 8/8 | bouncer `mint.go` ClaimAuthTime, `jwt.go` VerifyAuthTime, refresh carries auth_time; `pact.HasFrontendTokenScopes`, ValidTokenScopeName; token-specific revoke events (TestTokenEvents); `towel.ClientIP`; account contribution URL rule and per-section `error`; hotkeys; OpenAPI and dist checks in the gate. | +| 08 plugins/demo | 8 | ✓ 8/8 | auth_time confirm (409 stale_session), fragment code plus POST show (`linkCode.ts readLinkCode`), `FireAccessRevoked` and cascade, member token scopes (`api_token_scopes.go`, `plugin.go:128` CompileAPITokenScopes, `/tokens/scopes` route), organisation logo (`organisation_logo.go`, `fields.yaml`), Journal draft_post `Writes: true`, nginx body cap (40ed518), additive check (gate plugins stage). | +| 09 gap tests/gate | 6 | ✓ 6/6 | `member_token_scopes_test.go` (E-2 list verbatim), `TestMCPRevocationEndToEnd`, the gate's workspace and unreleased-pin policy (self-test), SECURITY-REVIEW through T-19.7-65, VALIDATION rows, deferred-items.md. The gate --all passes in this run (see Probe Execution). | + +### Required Artifacts + +| Artifact | Status | Details | +|----------|--------|---------| +| Framework artifacts of plans 01, 02, 05, 06, 07 and 09 | ✓ VERIFIED | `verify.artifacts`: 10/10, 11/11, 5/5, 5/5, 6/6, 4/4 (exists, plus the contains pattern) | +| Cross-repo artifacts of plans 03, 04 and 08 | ✓ VERIFIED | `verify.artifacts`: 7/7, 7/7, 6/6 | +| `modules/pier` (9.1k lines incl. tests) | ✓ VERIFIED | substantive; wired only through `surf/pier_on.go` | + +### Key Link Verification + +| From | To | Via | Status | +|------|----|-----|--------| +| Plans 01, 02, 05, 07, 09 key links (21) | | | ✓ WIRED (verify.key-links) | +| `pier/pier.go` | `session.go` | `go p.sessions.reap(...)` (pier.go:145) | ✓ WIRED (manual; tool pattern was not RE2) | +| `pier/session.go` | `store.go` | `res.Reserve(...)` (session.go:156) | ✓ WIRED (manual) | +| user `plugin.go` | `classes/backend_links.go`, `mcp_guard.go` | `app.Publish[pact.MCPLinkedBackendResolver]` (179), MCPGuards find-or-publish (153-155) | ✓ WIRED (manual) | +| user `backend_link_controller.go` | `classes/backend_links.go`, bouncer `jwt.go` | `ConfirmLinkRequest(..., authTime)` (119), `bouncer.VerifyAuthTime` (104) | ✓ WIRED (manual) | +| user `users_admin_controller.go` | `frontend_permission_catalog.go` | `classes.FrontendPermissionCatalog` (466) | ✓ WIRED (manual) | +| user `plugin.go` | `classes/api_token_scopes.go` | `classes.CompileAPITokenScopes(activated)` (128) | ✓ WIRED (manual) | +| oauthserver `plugin.go` | `classes/mcp_verifier.go` | `app.Publish[pact.MCPTokenVerifier]` (95) | ✓ WIRED (manual) | +| oauthserver `mcp_consent_api.go` | pact | `app.Lookup[pact.MCPLinkedBackendResolver]` (189) | ✓ WIRED (manual) | +| oauthserver `plugin.go` | user `classes/events.go` | `Listen[*userclasses.AccessRevokedEvent / BackendLinkedEvent / BackendUnlinkedEvent]` (103-105) | ✓ WIRED (manual) | +| vue `connect.vue` | oauthserver `routes.go:80-81` | GET/POST `/oauth/mcp/requests/{request}`, body from `consentDecisionBody` (echoes `backend_admin`) | ✓ WIRED (manual) | +| demo `summer.yaml` | `surf/pier_on.go` | `build.tags: [summercms_mcp]` | ✓ WIRED (manual) | + +### Data-Flow Trace (Level 4) + +| Artifact | Data | Source | Real data | Status | +|----------|------|--------|-----------|--------| +| tools/list | allowed tools | `catalog.allowedReport` → guard providers → `cabana.BackendPrincipal` / `PrincipalByID` (DB) | yes (e2e on Postgres) | ✓ FLOWING | +| tools/call result | route body | compiled route chain → recorder → structuredContent | yes (journal_create_post row checked in DB) | ✓ FLOWING | +| Identity.Backend | linked admin | `pact.MCPLinkedBackendResolver` → `golem15_user_backend_links` | yes | ✓ FLOWING | +| connect.vue consent | `backend_admin`, scopes | `MCPConsentShow` JSON | yes | ✓ FLOWING | + +### Behavioral Spot-Checks + +| Behavior | Command | Result | Status | +|----------|---------|--------|--------| +| pier package | `go test -count=1 ./modules/pier/` | ok 4.9s | ✓ PASS | +| surf/bouncer/pact/wristband | `go test -count=1 -short ./modules/{surf,bouncer,pact,wristband}/` (+ surf tagged) | ok | ✓ PASS | +| SC-1 mount on/off | `go test -run TestPierMount` untagged and `-tags summercms_mcp` | subtests listed above PASS | ✓ PASS | +| SC-4 end to end | `go test -tags summercms_mcp -run 'TestMCPEndToEnd$\|TestMCPRevocationEndToEnd$' .` in sm-demo-app (Postgres via Docker) | 11 + 4 subtests PASS | ✓ PASS | +| SDK linked only with the tag | `go list -deps ./modules/surf \| grep -c modelcontextprotocol` | 0 untagged, >0 tagged | ✓ PASS | +| Marker setter callers | grep `WithDispatch(` in non-test Go across all repos | only `modules/pier/dispatch.go:208` | ✓ PASS | +| v0.4.4 changelog | `curl https://summercms.io/changelog` | 200, contains v0.4.4 | ✓ PASS | + +### Probe Execution + +| Probe | Command | Result | Status | +|-------|---------|--------|--------| +| `scripts/check-phase19.7.sh` (phase gate) | `bash scripts/check-phase19.7.sh --all` (Docker host) | exit 0; every stage PASS (self-test, go, plugins, consumers, admin, docs, marker, docker); last line "Phase 19.7 gate passed WITH UNRELEASED PINS: the release pins are not yet proven (plugins/golem15/journal plugins/golem15/oauthserver plugins/golem15/user)" — the expected pre-release verdict | ✓ PASS (pre-release; release pins proven only by PHASE197_RELEASED=1 --consumers) | + +### Decision Coverage + +All 35 trackable CONTEXT.md decisions are honored by shipped artifacts (`check.decision-coverage-verify`). + +### Test Quality Audit + +| Area | Finding | Verdict | +|------|---------|---------| +| Skips | The only skip in the phase e2e files is `testing.Short()` → `t.Skip("Postgres")`, and the non-short run passed; the gate refuses a skipped, zero-match or missing required test | OK | +| Circularity | Expected values are fixtures (users, admins, grants) written by the tests; no captured-baseline pattern | OK | +| Assertion level | e2e asserts tool names, the DB row author, 401 after unlink, the session ending: behavioral | OK | +| Gap | TestMCPEndToEnd mints the access token through `MCPStore.Mint` instead of the browser consent; the consent and exchange handlers are covered in oauthserver's TestMCPBackendBinding. The composition through a real client is UAT. | WARNING → human item | + +### Anti-Patterns Found + +| File | Line | Pattern | Severity | Impact | +|------|------|---------|----------|--------| +| Framework files touched by the phase (171 scanned); changed files in user, oauthserver, journal, demo and vue-demo-app | — | TBD/FIXME/XXX/TODO/HACK/placeholder | none found | — | +| sm-demo-app `plugins/demo/content/ROADMAP.md` | 469+ | `Requirements: TBD` | ℹ️ Info | Demo content text, not a debt marker | +| sm-demo-app `main.go` (uncommitted) | — | regenerated by the stale installed `summer` (2026-10-20 binary; sunscreen/compass.Load form instead of diary/bonfire) | ℹ️ Info | Another session's working-tree change (19.7-04-SUMMARY item 12). It compiles and the tests pass. Do not commit; restore or regenerate with the current CLI before the demo release. | +| sm-oauthserver-plugin `go.work` (uncommitted) | — | local retarget to the canonical user checkout | ℹ️ Info | Recorded in deferred-items.md; never commit | + +### Requirements Coverage + +ROADMAP lists "Requirements: TBD" for 19.7 and no plan declares requirement IDs, so there are no orphaned requirements. Coverage is measured against the success criteria and the plan must-haves above. + +### Human Verification Required + +1. **SC-3 wording decision (override).** The code mirrors `user.permission` (group grants), per D-35. Accept it with: + ```yaml + overrides: + - must_have: "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" + reason: "D-35 / P-14 (19.7-CONTEXT.md, RESEARCH Open Question 4): frontend tool filtering mirrors what the user.permission middleware enforces (group grants via PrincipalByID), so tools/list equals the callable set; MergedPermissions is used by no route, and switching would change a core plugin's behaviour" + accepted_by: "{your name}" + accepted_at: "{ISO timestamp}" + ``` + Alternatively, open a follow-up to move both `user.permission` and the MCP guard to `MergedPermissions`. +2. **Real MCP client against the local demo.** Connect once, see the frontend tools, link an admin, see the journal_ tools, draft a post, unlink, see them go (plan 04 Task 5, never run). +3. **Consent page 409 backend_link_changed in a browser.** The page reloads and shows the notice. +4. **/link-admin "Signed in as" and stale-session handling in a browser.** +5. **RB-WR-01 in Firefox.** Save shortcuts with a pending taglist draft inside a shadow root; plain keys inside the shadow root are muted. + +Optional human review named by 19.7-REREVIEW-FIX.md: the RC-CR-01 and RC-WR-01 revocation logic and the per-user advisory locks. The pre-migration backend grants losing backend reach until reconnect is a behaviour change to mention in the release notes. + +### Gaps Summary + +No blocking gaps. Every artifact exists, is substantive and is wired. Every key link holds across the six repositories. The behavior-dependent truths are backed by named tests that the gate requires and that passed in this run, plus my own runs of the pier package and the two demo end-to-end tests on Postgres. + +One roadmap truth (SC-3) has a recorded, intentional deviation in a parenthetical: frontend permissions use group grants, not MergedPermissions. It is reported as UNCERTAIN, with an override suggested, not as FAILED: the observable contract (the listing equals what the route's own middleware lets the principal call) holds, and the deviation was decided before planning. + +Release state is not a verification gap, per the release order in deferred-items.md. The gap-closure work in all repos is untagged and unpushed. Demo plugin checkouts are ahead of their pins (the gate's "WITH UNRELEASED PINS" verdict). Release builds that pin summercms v0.4.4 lack every fix since. framework v0.5.0 → plugin tags (oauthserver needs the user tag first and a `GOWORK=off go build` check, RC-IN-07) → app bumps → `PHASE197_RELEASED=1 … --consumers` remain to run after UAT, on the user's go-ahead. + +Open deferred items that a later phase or the release must still carry (none blocks the goal): MCP grants missing from the authorized-apps list (AR-19.7-03), `admin:token:revoke` firing no MCP event (RB-IN-03), stateful sessions without the SDK protocol-version context, `notifications/initialized` landing on another instance, the RA-IN-04 fallback cap race, RC-CR-01 (cutoff on auth_time), RC-IN-01 (nginx access_log for the legacy GET link route), RC-IN-06, B-IN-05, and the conditional superuser refusal switch. + +--- + +_Verified: 2026-10-23_ +_Verifier: Claude (msd-verifier)_