docs(19.7-03): sm-user-plugin v0.1.10 released and pinned in five apps; cabana subject-event bug deferred
Plan 03 summary: verified account link, Administrators-screen link actions, plugin frontend permissions, user MCP guard and user_me in sm-user-plugin v0.1.10 (requires SummerCMS v0.4.4). deferred-items: cabana fires pact.MCPSubjectChanged as a value inside the transaction while pier listens for a pointer; fix before v0.5.0.
This commit is contained in:
215
.planning/phases/19.7-generic-mcp-module/19.7-03-SUMMARY.md
Normal file
215
.planning/phases/19.7-generic-mcp-module/19.7-03-SUMMARY.md
Normal file
@@ -0,0 +1,215 @@
|
||||
---
|
||||
phase: 19.7-generic-mcp-module
|
||||
plan: 03
|
||||
subsystem: user-plugin
|
||||
tags: [sm-user-plugin, mcp, backend-link, frontend-permissions, guard-provider, personal-tokens, release, pointer-bumps]
|
||||
|
||||
requires:
|
||||
- phase: 19.7-01
|
||||
provides: AccountContributions and AdminActionExtensions seams, prompted record actions, pact MCP contracts (MCPGuard, MCPGuards, MCPLinkedBackendResolver, MCPPersonalTokenResolver, MCPSubjectChanged, HasFrontendPermissions)
|
||||
- phase: 19.7-02
|
||||
provides: modules/pier (catalog, guard providers, per-request identity, subject-event fan-out), released opt-in in framework v0.4.4
|
||||
provides:
|
||||
- sm-user-plugin v0.1.10 (git.golem15.com/golem15/sm-user-plugin), tagged and pushed, requiring SummerCMS v0.4.4
|
||||
- verified frontend-to-backend account link (tables, one-shot hashed code, fresh-sign-in proof, classes.BackendLinks published as pact.MCPLinkedBackendResolver)
|
||||
- Administrators-screen record actions link_frontend_account (prompted) and unlink_frontend_account behind golem15.users.manage_backend_links
|
||||
- My account section frontend_account (link, unlink)
|
||||
- classes.FrontendPermissionCatalog (seeded rows plus plugin codes; seeded row wins)
|
||||
- classes.MCPUserGuard (guard "user", usr_ personal-token resolver), GET /_user/api/v1/token/me, MCP tool user_me
|
||||
- pointer bumps to v0.1.10 + summercms v0.4.4 in sm-demo-app, fonoteka.go, sm-bm-app, sm-grzybyfunkcjonalne-app, sm-summercmsio-app (not pushed)
|
||||
affects: [19.7-04 oauthserver verifier and Journal (consumes the link resolver and user guard), 19.7-05 unit tests and security review, framework v0.5.0 at phase completion (cabana subject-event fix first)]
|
||||
|
||||
actuals:
|
||||
tokens: 26468
|
||||
tasks: 5
|
||||
commits: 6
|
||||
plan_head_before: 3828a65481190acf0b450e5f880cf85a14d63ad8
|
||||
plan_head_after: bfcfea4ed7d45374e9584557412b88797b051741
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Subject events fire as *pact.MCPSubjectChanged (pointer) after commit through lagoon.AfterCommit"
|
||||
- "Fresh-sign-in proof: confirm route behind jwt.auth re-reads the bearer and requires iat (seconds) >= the link request's created_at (seconds)"
|
||||
- "Guard provider registers itself into the find-or-publish *pact.MCPGuards registry and doubles as pact.MCPPersonalTokenResolver"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- ../sm-demo-app/plugins/golem15/user/updates/202610220001_create_backend_links.go
|
||||
- ../sm-demo-app/plugins/golem15/user/models/backend_link.go
|
||||
- ../sm-demo-app/plugins/golem15/user/models/backend_link_request.go
|
||||
- ../sm-demo-app/plugins/golem15/user/classes/backend_links.go
|
||||
- ../sm-demo-app/plugins/golem15/user/classes/subject_events.go
|
||||
- ../sm-demo-app/plugins/golem15/user/classes/frontend_permission_catalog.go
|
||||
- ../sm-demo-app/plugins/golem15/user/classes/mcp_guard.go
|
||||
- ../sm-demo-app/plugins/golem15/user/controllers/backend_link_controller.go
|
||||
- ../sm-demo-app/plugins/golem15/user/controllers/token_me.go
|
||||
- ../sm-demo-app/plugins/golem15/user/account_contributions.go
|
||||
- ../sm-demo-app/plugins/golem15/user/backend_links_admin.go
|
||||
- ../sm-demo-app/plugins/golem15/user/mcp.go
|
||||
- ../sm-demo-app/plugins/golem15/user/backend_link_smoke_test.go
|
||||
- ../sm-demo-app/plugins/golem15/user/backend_link_admin_smoke_test.go
|
||||
- ../sm-demo-app/plugins/golem15/user/mcp_guard_smoke_test.go
|
||||
- ../sm-demo-app/plugins/golem15/user/frontend_permission_catalog_smoke_test.go
|
||||
modified:
|
||||
- ../sm-demo-app/plugins/golem15/user/plugin.go
|
||||
- ../sm-demo-app/plugins/golem15/user/routes.go
|
||||
- ../sm-demo-app/plugins/golem15/user/admin_permissions.go
|
||||
- ../sm-demo-app/plugins/golem15/user/classes/events.go
|
||||
- ../sm-demo-app/plugins/golem15/user/classes/privileged.go
|
||||
- ../sm-demo-app/plugins/golem15/user/controllers/api_tokens.go
|
||||
- ../sm-demo-app/plugins/golem15/user/controllers/users_admin_controller.go
|
||||
- ../sm-demo-app/plugins/golem15/user/controllers/usergroups_admin_controller.go
|
||||
- ../sm-demo-app/plugins/golem15/user/config/config.yaml
|
||||
- ../sm-demo-app/plugins/golem15/user/lang/en/lang.yaml
|
||||
- ../sm-demo-app/plugins/golem15/user/lang/pl/lang.yaml
|
||||
- ../sm-demo-app/plugins/golem15/user/README.md
|
||||
- ../sm-demo-app/plugins/golem15/user/go.mod
|
||||
- ../sm-demo-app/plugins/golem15/user/go.sum
|
||||
- "pinned-count tests: admin_harness_test.go, admin_registration_test.go, admin_users_test.go, password_routes_test.go, updates/organisations_test.go, updates/user_groups_test.go, updates/user_session_test.go"
|
||||
- "five apps: go.mod, go.sum, plugins/golem15/user"
|
||||
- .planning/phases/19.7-generic-mcp-module/deferred-items.md
|
||||
|
||||
key-decisions:
|
||||
- "P-10: golem15_user_backend_links (1:1, both sides unique) and golem15_user_backend_link_requests (64-hex code stored as SHA-256, request_ttl default 10m, one-shot, bound to the creating admin)"
|
||||
- "P-11: confirm behind jwt.auth, bearer re-read with bouncer.VerifyClaims; iat older than the request's created_at answers 409 stale_session"
|
||||
- "P-12: admin set by exact, case-insensitive email of an active user; no search or list; golem15.users.manage_backend_links with no default role; linked_by recorded"
|
||||
- "P-13: golem15.user.frontend_url added here (empty default falls back to app.url); link page is <frontend_url>/link-admin?code=<plain>"
|
||||
- "P-14: MCPAllows uses PrincipalByID group grants and cabana.Allows per code with AND, the same as user.permission middleware"
|
||||
- "P-15 (user, Task 4 checkpoint): release as v0.1.10 (v0.1.9 was taken by the public_routes release), bump all five applications"
|
||||
- "Plugin subject events fire as pointers after commit; cabana's value-typed in-transaction firing is a framework bug recorded in deferred-items.md, to fix before v0.5.0"
|
||||
|
||||
patterns-established:
|
||||
- "A plugin-side MCP guard provider: register into *pact.MCPGuards, answer MCPAllows with the same evaluation the route middleware runs, dispatch with an in-memory (ID 0) credential"
|
||||
|
||||
requirements-completed: [SC-4, D-01, D-03, D-04, D-05, D-06, D-07, D-08, D-09, D-15, D-16, D-17, D-18, D-23, D-26, D-34, D-35]
|
||||
|
||||
coverage:
|
||||
- id: D1
|
||||
description: "Self-service link: My account request, one-shot code, fresh sign-in confirm; replayed, expired and stale-session confirms refused; no email match or list on this path; unlink and delete remove the link"
|
||||
requirement: "D-05"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "../sm-demo-app/plugins/golem15/user/backend_link_smoke_test.go#TestBackendLinkSmoke"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D2
|
||||
description: "Administrators-screen link and unlink behind golem15.users.manage_backend_links, linked_by recorded, events fired, taken account refused"
|
||||
requirement: "D-34"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "../sm-demo-app/plugins/golem15/user/backend_link_admin_smoke_test.go#TestBackendLinkAdminSmoke"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D3
|
||||
description: "User guard: tools/list agrees with user.permission (group grants, AND across codes), every user.scope required, usr_ personal tokens resolved, user_me returns the payload"
|
||||
requirement: "D-35"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "../sm-demo-app/plugins/golem15/user/mcp_guard_smoke_test.go#TestUserMCPGuardSmoke"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D4
|
||||
description: "Plugin frontend permission codes appear in both editors next to seeded rows; seeded row wins"
|
||||
requirement: "D-09"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "../sm-demo-app/plugins/golem15/user/frontend_permission_catalog_smoke_test.go#TestFrontendPermissionCatalogSmoke"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D5
|
||||
description: "Additive release v0.1.10 on origin requiring summercms v0.4.4; five apps bumped one commit each, each builds and passes short tests"
|
||||
requirement: "D-01"
|
||||
verification:
|
||||
- kind: other
|
||||
ref: "git ls-remote --tags origin refs/tags/v0.1.10 (a2f8672); go build ./... && go test -count=1 -short ./... in each app"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
|
||||
duration: about 27 min of commit activity (first task commit 10:03Z to the last app bump 10:28Z, 2026-10-23), plus the Task 4 checkpoint wait
|
||||
completed: 2026-10-23
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Phase 19.7 Plan 03: sm-user-plugin account link, user MCP guard and v0.1.10 Summary
|
||||
|
||||
**sm-user-plugin v0.1.10 adds the verified frontend-to-backend account link. An admin starts it on My account and a frontend user confirms it with a one-shot hashed code and a fresh sign-in. The Administrators screen can link and unlink accounts under `golem15.users.manage_backend_links`. The release also adds plugin-declared frontend permission codes, the `user` MCP guard provider with `usr_` personal-token resolution, and the `user_me` tool. It requires SummerCMS v0.4.4 and is pinned in all five applications.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** about 27 min of commit activity on 2026-10-23 (10:03Z to 10:28Z), split by the Task 4 release checkpoint
|
||||
- **Completed:** 2026-10-23T10:28Z
|
||||
- **Tasks:** 5 (Task 4 was the release-version checkpoint, answered "v0110": v0.1.10, all five apps)
|
||||
- **Files:** 37 in the plugin (2,418 insertions, 61 deletions since v0.1.9), plus go.mod, go.sum and the submodule pointer in each of five apps
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- **Task 1 (tracer, `7ac80c9`):** migration `202610220001_create_golem15_user_backend_links` creates the two tables. `classes.BackendLinks` provides `CreateLinkRequest`, `LinkRequestByCode`, `ConfirmLinkRequest` (row lock, `iat` check), `SetLink`, `RemoveLink`, `LinkFor`, `LinkOf` and `LinkedBackendUser`, and is published both as `*classes.BackendLinks` and as `pact.MCPLinkedBackendResolver`. Routes `GET /_user/api/v1/backend-link/{code}` and `POST /_user/api/v1/backend-link/confirm`, the My account section `frontend_account`, `BackendLinkedEvent` and `BackendUnlinkedEvent`, and subject events after commit. Deleting or deactivating a frontend user removes its link.
|
||||
- **Task 2 (`0f545f5`, fix `dbb3d33`):** `classes.FrontendPermissionCatalog` feeds both permission editors. `classes.MCPUserGuard` is registered in `*pact.MCPGuards` and published as `pact.MCPPersonalTokenResolver`. This task also added `GET /_user/api/v1/token/me` and the `user_me` tool, plus subject events on token revoke and on Users and User Groups saves and deletes.
|
||||
- **Task 3 (`6f1cb1a`):** `golem15.users.manage_backend_links` (en and pl labels), and the record actions `link_frontend_account` (prompted, exact email) and `unlink_frontend_account` (danger) on cabana's Administrators controller.
|
||||
- **Task 4:** release checkpoint. The user chose v0.1.10 and all five applications.
|
||||
- **Task 5:** README (`59cc3bd`) and `chore: require SummerCMS v0.4.4` (`bfcfea4`). The full Docker suite passed (`GOWORK=off go vet ./... && GOWORK=off go test -count=1 ./...`; `TestBackendLinkSmoke`, `TestBackendLinkAdminSmoke`, `TestUserMCPGuardSmoke` and `TestFrontendPermissionCatalogSmoke` print `--- PASS`). Annotated tag `v0.1.10` was pushed with master (`git push origin master v0.1.10`, remote tag object a2f8672). Then one pointer-bump commit in each app.
|
||||
- Additive check: `git diff 3828a65 HEAD -- '*.go' ':!*_test.go'` removes no exported func or type.
|
||||
|
||||
## Task Commits
|
||||
|
||||
Plugin (`git.golem15.com/golem15/sm-user-plugin`, master, pushed):
|
||||
|
||||
1. **Task 1: verified link tracer** - `7ac80c9` (feat)
|
||||
2. **Task 2: guard provider, frontend permissions, user_me** - `0f545f5` (feat), fix `dbb3d33`
|
||||
3. **Task 3: Administrators-screen actions** - `6f1cb1a` (feat)
|
||||
4. **Task 4: release checkpoint** - no commit
|
||||
5. **Task 5: README and framework pin** - `59cc3bd` (docs), `bfcfea4` (chore), tag `v0.1.10`
|
||||
|
||||
Applications (`chore: bump user to v0.1.10 and require summercms v0.4.4`, not pushed):
|
||||
|
||||
| App | Commit | From |
|
||||
|---|---|---|
|
||||
| sm-demo-app | `985af53` | user v0.1.9, summercms v0.4.3 |
|
||||
| fonoteka.go | `06bc9d0` | user v0.1.0 in go.mod (submodule at v0.1.6), summercms v0.4.0 |
|
||||
| sm-bm-app | `d180d35` | user v0.1.8, summercms v0.4.3 |
|
||||
| sm-grzybyfunkcjonalne-app | `acbf68b` | user v0.1.8, summercms v0.4.3 |
|
||||
| sm-summercmsio-app | `a1a661d` | user v0.1.8, summercms v0.4.3 |
|
||||
|
||||
Each app ran `go build ./... && go test -count=1 -short ./...` at the new pins, and all passed. `go mod tidy` added the go-sdk v1.8.0 transitive set as indirect requirements, matching framework v0.4.4's go.mod. `commits: 6` is measured in the plugin repo (`git rev-list --count 3828a65..bfcfea4`). The five app commits are in addition to that count.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
1. **[Rule 3 - Blocking] backend_users FKs through a `DO $$` block.** The migration adds the foreign keys to `backend_users` only when `to_regclass('backend_users')` exists, so the plugin's own test databases (which do not carry cabana's tables) still migrate. (Task 1)
|
||||
2. **[Rule 1 - Bug] Updated test counts that the new rows and routes changed.** Route counts went from 18 to 21 and from 15 to 18, and the rollback counts, migration history and permission lists in the existing tests were updated to match. (Tasks 1-3)
|
||||
3. **[Rule 1 - Bug] Subject events fire as `*pact.MCPSubjectChanged` after commit.** pier listens for the pointer type, so the plugin fires a pointer through `lagoon.AfterCommit`. cabana's own value-typed, in-transaction firing never reaches pier. That is a framework bug, recorded in deferred-items.md and to be fixed before v0.5.0. (Task 1)
|
||||
4. **[Rule 2 - Security] The confirm route uses `classes.SessionToken` and requires the token's subject to equal the `jwt.auth` principal,** in addition to the `iat` check. (Task 1)
|
||||
5. **[Rule 1 - Bug] `iat` and `created_at` are compared truncated to seconds.** JWT `iat` has whole-second precision, so a sign-in in the same second as the request would otherwise count as stale. (Task 1)
|
||||
6. **[Rule 2 - Security] `CreateLinkRequest` deletes the admin's earlier unused requests,** so only the latest code is valid. (Task 1)
|
||||
7. **[Rule 2] The Users and UserGroups update hooks fire permissions events on every save.** They do not diff the permissions first, which is cheap and errs toward re-listing. (Task 2)
|
||||
8. **Recorded gap: the `user:token:revoke` console command fires no subject event.** Only the HTTP revoke does. (Task 2)
|
||||
9. **[Rule 1] A second link for an already linked admin answers 409 (the action's Applies check), not 422.** The 422 `already_linked` path is tested with a second admin targeting a frontend user who is already taken. (Task 3)
|
||||
10. **[Rule 1 - Bug, `dbb3d33`] Group-delete subject events moved to `FormBeforeDelete`.** This keeps the existing `FormAfterDelete` declaration unchanged, so the additive gate stays clean. Member ids are read inside the delete's transaction, and the events fire after commit. (Task 2 fix)
|
||||
11. **"Linked to <email>" is interpolated,** which produces a dev-only missing-translation warning for the composed string. (Task 1)
|
||||
12. **New export `ErrFrontendUserUnavailable`,** returned when the target frontend user is missing or inactive. This is additive. (Task 3)
|
||||
|
||||
Drift from the plan text: the plan proposed v0.1.9 and "the plan-02 tag". v0.1.9 had already been released for the public_routes switch, and plan 02 cut no tag (the framework shipped pier opt-in in v0.4.4), so the release is v0.1.10 against v0.4.4.
|
||||
|
||||
## Deferred Issues
|
||||
|
||||
- **Framework bug, cabana backend subject events (open, blocks v0.5.0):** `modules/cabana/subject_events.go` fires `pact.MCPSubjectChanged` as a value inside the save's transaction. `modules/pier/events.go` listens for `*pact.MCPSubjectChanged`. Because `festival.Bus` keys listeners by type, pier never receives cabana's backend deletion, permission, role and revocation events. The entry is in `.planning/phases/19.7-generic-mcp-module/deferred-items.md`.
|
||||
- The `user:token:revoke` console command fires no subject event (deviation 8). A follow-up can fire the event from the console path.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
None beyond the plan's threat model (T-19.7-22..29). The new routes, the guard provider and the admin actions are the surfaces that model covers.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
Plan 04 can look up `*classes.BackendLinks`, `pact.MCPLinkedBackendResolver`, the `user` guard in `*pact.MCPGuards` and `pact.MCPPersonalTokenResolver` from any of the five applications at v0.1.10. The application bump commits are local and wait for the user's push and per-app deploy.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- Plugin files present (models, classes, controllers, migration, smoke tests). Commits 7ac80c9, 0f545f5, 6f1cb1a, dbb3d33, 59cc3bd and bfcfea4 are in the plugin repo, and the tag v0.1.10 is on origin.
|
||||
- App commits 985af53, 06bc9d0, d180d35, acbf68b and a1a661d exist, and `git diff --quiet HEAD -- go.mod go.sum plugins/golem15/user` is clean in each app.
|
||||
@@ -44,3 +44,18 @@ was changed by the plan that recorded it.
|
||||
instance serves them. Sticky sessions or a per-request re-read of the
|
||||
stored `initialized` flag would close the gap; not needed for the
|
||||
single-instance demo.
|
||||
- cabana's backend subject events never reach pier (found during 19.7-03 Task 2)
|
||||
status: open
|
||||
**What:** `modules/cabana/subject_events.go` fires `pact.MCPSubjectChanged`
|
||||
as a value (`app.Events.Fire(ctx, pact.MCPSubjectChanged{...})`), while
|
||||
`modules/pier/events.go` listens for a pointer
|
||||
(`func(ctx, ev *pact.MCPSubjectChanged)`). festival.Bus keys listeners by
|
||||
the type parameter, so pier never sees the backend subject events cabana fires
|
||||
(admin deletion, permission and role changes, token revocation): no
|
||||
list_changed and no session close for the `backend` guard from cabana.
|
||||
cabana also fires inside the save's transaction instead of after commit,
|
||||
so a rolled-back save would still notify. sm-user-plugin v0.1.10 fires
|
||||
`*pact.MCPSubjectChanged` (pointer) through `lagoon.AfterCommit`, which
|
||||
pier does receive. Fix in the framework: fire `&pact.MCPSubjectChanged{}`
|
||||
from cabana and defer it with `lagoon.AfterCommit`, with a pier test that
|
||||
a cabana save reaches `handleEvent`. Must be fixed before v0.5.0.
|
||||
|
||||
Reference in New Issue
Block a user