diff --git a/.planning/phases/19.7-generic-mcp-module/19.7-01-PLAN.md b/.planning/phases/19.7-generic-mcp-module/19.7-01-PLAN.md
index d07002f6..707a97ef 100644
--- a/.planning/phases/19.7-generic-mcp-module/19.7-01-PLAN.md
+++ b/.planning/phases/19.7-generic-mcp-module/19.7-01-PLAN.md
@@ -301,14 +301,14 @@ Output: the seams above, their smoke tests (unit tests proper are plan 05), READ
Docs: pact README API reference rows for every new type and interface; `docs/plugins/registration.md` capability table rows for HasMCP, HasFrontendPermissions, HasAccountContributions, and a sentence on AdminRecordAction.Prompt. The MCP usage page itself is plan 02's `docs/services/mcp.md`.
- go vet ./modules/pact/... && go test -count=1 -short ./modules/pact/... && go list -deps ./modules/pact | grep -c 'modelcontextprotocol\|jsonschema-go\|modules/bouncer' | grep -qx 0 && go test -count=1 -run '^TestDocsTree$' ./cmd/summer && go run ./cmd/summer docs:build --check
- non-zero exit, "FAIL" in the output, or the dependency grep count is not 0
+ go vet ./modules/pact/... && go test -count=1 -short ./modules/pact/... && DEPS=$(go list -deps ./modules/pact) && ! printf '%s\n' "$DEPS" | grep -q 'modelcontextprotocol\|jsonschema-go\|modules/bouncer' && go test -count=1 -run '^TestDocsTree$' ./cmd/summer && go run ./cmd/summer docs:build --check
+ non-zero exit, "FAIL" in the output, `go list -deps` failing (the captured assignment short-circuits the chain), or any dependency line matching modelcontextprotocol, jsonschema-go or modules/bouncer
- `grep -c "type HasMCP interface\|type MCPTool struct\|type MCPPrompt struct\|type MCPGuard interface\|type MCPGuards struct\|type MCPTokenVerifier interface\|type MCPLinkedBackendResolver interface\|type MCPSubjectChanged struct\|type HasFrontendPermissions interface" modules/pact/mcp.go` prints 9
- `grep -c "type AdminActionPrompt struct\|type HasAccountContributions interface\|type AccountContribution struct\|type AccountAction struct\|type AccountActionResult struct" modules/pact/capabilities.go` prints 5
- `grep -c "Prompt \*AdminActionPrompt" modules/pact/capabilities.go` prints 1 and `grep -c "Value string" modules/pact/capabilities.go` prints at least 1
- - `go list -deps ./modules/pact | grep -c 'modelcontextprotocol\|jsonschema-go\|modules/bouncer'` prints 0
+ - `DEPS=$(go list -deps ./modules/pact) && ! printf '%s\n' "$DEPS" | grep -q 'modelcontextprotocol\|jsonschema-go\|modules/bouncer'` exits 0 (a failing `go list` fails the criterion rather than passing it)
- `grep -c "HasMCP\|HasFrontendPermissions\|HasAccountContributions" docs/plugins/registration.md` prints at least 3
- `go test -count=1 -short ./modules/pact/...` passes
@@ -355,7 +355,7 @@ Output: the seams above, their smoke tests (unit tests proper are plan 05), READ
- `grep -c "func fireSubjectChanged(" modules/cabana/subject_events.go` prints 1 and `grep -c "fireSubjectChanged(" modules/cabana/account_tokens.go modules/cabana/backend_users_admin.go modules/cabana/backend_roles_admin.go` shows a non-zero count for each of the three files
- `grep -c "action_prompt_required" modules/phrasebook/backend/lang/en/lang.yaml modules/phrasebook/backend/lang/pl/lang.yaml` prints 1 for each file
- `grep -c "AccountContributionView" admin/openapi/admin.json` prints a non-zero count and `scripts/check-admin-openapi.sh --check` exits 0
- - account.go untouched by the phase: `git log --oneline 51d088a..HEAD -- modules/cabana/account.go | wc -l` prints 0 (51d088a is the framework HEAD when this plan was written)
+ - account.go untouched by the phase: `git rev-parse -q --verify '51d088a^{commit}' >/dev/null && git diff --quiet 51d088a -- modules/cabana/account.go` exits 0 (51d088a is the framework HEAD when this plan was written; the file must be byte-identical to that commit, and an unknown base or a git error exits non-zero instead of passing)
- Both smoke tests print `--- PASS`
cabana lists and runs plugin account contributions behind interactiveOnly, accepts a prompt value only for prompted record actions, fires backend subject-changed events, and the OpenAPI document, schema types and docs are regenerated and committed; existing account and record payloads unchanged.
diff --git a/.planning/phases/19.7-generic-mcp-module/19.7-02-PLAN.md b/.planning/phases/19.7-generic-mcp-module/19.7-02-PLAN.md
index c5a9def3..cfb3ae49 100644
--- a/.planning/phases/19.7-generic-mcp-module/19.7-02-PLAN.md
+++ b/.planning/phases/19.7-generic-mcp-module/19.7-02-PLAN.md
@@ -118,7 +118,7 @@ must_haves:
-Build `modules/pier`, the opt-in MCP endpoint inside a SummerCMS binary, on the official go-sdk v1.8.0, behind the `summercms_mcp` build tag (D-33) and the `mcp.enabled` flag (D-30): plugin-declared tools and prompts compiled from `pact.HasMCP` (D-10..D-16, D-28), per-request identity from an `oas_at_` verifier or, behind a flag, personal tokens (D-19), a backend subject resolved through the published link resolver on every request (D-08), per-principal filtering with the read/write ceiling (D-20, D-23) and the backend scope (D-21), in-process dispatch through surf's compiled handler with the bouncer marker (D-17), a Redis-backed rehydrating session router with a memory fallback (D-25, D-27), pub/sub fan-out of `list_changed` (D-26), the stateless 2026-07-28 path (D-32), protected resource metadata (RFC 9728), and an audit line per call (D-23). Then the framework release that carries 19.6.1 (executed, unreleased: 38 commits since v0.4.3) together with plans 01 and 02.
+Build `modules/pier`, the opt-in MCP endpoint inside a SummerCMS binary, on the official go-sdk v1.8.0, behind the `summercms_mcp` build tag (D-33) and the `mcp.enabled` flag (D-30): plugin-declared tools and prompts compiled from `pact.HasMCP` (D-10..D-16, D-28), per-request identity from an `oas_at_` verifier or, behind a flag, personal tokens (D-19), a backend subject resolved through the published link resolver on every request (D-08), per-principal filtering with the read/write ceiling (D-20, D-23) and the backend scope (D-21), in-process dispatch through surf's compiled handler with the bouncer marker (D-17), a Redis-backed rehydrating session router with a memory fallback (D-25, D-27), pub/sub fan-out of `list_changed` (D-26), the stateless 2026-07-28 path (D-32), protected resource metadata (RFC 9728), and an audit line per call (D-23). Then the framework release that carries 19.6.1 (executed, unreleased; the commits carried are whatever `git log --oneline v0.4.3..HEAD` lists at tag time, not a count fixed here) together with plans 01 and 02.
**Planner decisions recorded here:**
- **P-05 (config keys, Claude's discretion):** namespace `mcp.*` (feature namespaces like `http.*`, `cache.*`, `realtime.*`): `mcp.enabled` (false), `mcp.path` (`/mcp`), `mcp.allow_personal_tokens` (false), `mcp.sessions.store` (`memory`|`redis`, default `memory`; A7: the single-instance demo has no Redis), `mcp.sessions.idle_ttl` (`30m`), `mcp.sessions.max_per_binding` (20), `mcp.resource` (default `app.url` + `mcp.path`, no trailing slash), `mcp.throttle` (`120,1`, surf's inline spelling), `mcp.allowed_origins` (default: the origin of `app.url`), `mcp.call_timeout` (`30s`), `mcp.max_result_bytes` (1048576).
@@ -220,7 +220,7 @@ Output: `modules/pier` with README, the tagged `pier_on.go`, root README row, `d
- `head -1 modules/surf/pier_on.go` prints `//go:build summercms_mcp` and `grep -c "mountPier = " modules/surf/pier_on.go` prints 1
- `grep -c "func Mount(" modules/pier/pier.go` prints 1; `grep -c "jsonschema.ForType" modules/pier/catalog.go` prints at least 1; `grep -c "httptest.NewRecorder" modules/pier/dispatch.go` prints at least 1; `grep -c "StreamableServerTransport" modules/pier/session.go` prints at least 1
- `grep -c "WithDispatch(" modules/pier/dispatch.go` prints at least 1 and `grep -rl "WithDispatch(" modules --include=*.go | grep -v _test.go | grep -v '^modules/bouncer/' | grep -v '^modules/pier/' | wc -l` prints 0
- - `go build -o /tmp/claude-1000/hello-notag ./examples/hello 2>/dev/null || go -C examples/hello build -o /tmp/claude-1000/hello-notag . ; go version -m /tmp/claude-1000/hello-notag | grep -c modelcontextprotocol` prints 0
+ - `OUT=$(mktemp -d) && go -C examples/hello build -o "$OUT/hello-notag" . && test -x "$OUT/hello-notag" && go version -m "$OUT/hello-notag" > "$OUT/mods.txt" && ! grep -q modelcontextprotocol "$OUT/mods.txt"` exits 0 (the untagged hello binary must build and its module list must be readable before the absence of the SDK counts; a failed build or an absent binary fails the criterion instead of passing it)
- `grep -c "^\*\*Name:\*\*" modules/pier/README.md` prints 1 and `grep -c "\[pier\](modules/pier/README.md)" README.md` prints 1
- `go test -count=1 -short -run '^TestPierTracerSmoke$' -v ./modules/pier 2>&1 | grep -q -- '^--- PASS: TestPierTracerSmoke'`
@@ -310,8 +310,8 @@ Output: `modules/pier` with README, the tagged `pier_on.go`, root README row, `d
Task 5: Decide the framework release version and give the go-ahead to tag and push
- Tag and push the framework release that carries Phase 19.6.1 (Administrators and Roles screens, 38 commits since v0.4.3) together with 19.7 plans 01 and 02 (the seams and the pier module), and post the changelog entry the same day.
- The tag is a one-way door: consumers pin it, the plugin releases of plans 03 and 04 require it, and `summercms.io/changelog` announces it. The planner proposes a minor bump (P-09): a new module `pier`, new `pact` capabilities (HasMCP, HasFrontendPermissions, HasAccountContributions, the record action Prompt), additive surf/bouncer/cabana/wristband/cooler API. Nothing is pushed before you answer. Pre-checks the executor runs before asking: clean `git status` for the framework paths, `go vet ./... && go test -count=1 ./...` (Docker suites included), `scripts/check-admin-dist.sh`, `scripts/check-admin-openapi.sh --check`, `go run ./cmd/summer docs:build --check`, `bash scripts/check-phase19.6.1.sh --all`, and `go build -tags summercms_mcp ./...`.
+ Tag and push the framework release that carries Phase 19.6.1 (Administrators and Roles screens; the commits carried are those `git log --oneline v0.4.3..HEAD` lists at tag time, pasted with their measured count into the checkpoint message) together with 19.7 plans 01 and 02 (the seams and the pier module), and post the changelog entry the same day.
+ The tag is a one-way door: consumers pin it, the plugin releases of plans 03 and 04 require it, and `summercms.io/changelog` announces it. The planner proposes a minor bump (P-09): a new module `pier`, new `pact` capabilities (HasMCP, HasFrontendPermissions, HasAccountContributions, the record action Prompt), additive surf/bouncer/cabana/wristband/cooler API. Nothing is pushed before you answer. Pre-checks the executor runs before asking: clean `git status` for the framework paths, `go vet ./... && go test -count=1 ./...` (Docker suites included), `scripts/check-admin-dist.sh`, `scripts/check-admin-openapi.sh --check`, `go run ./cmd/summer docs:build --check`, `bash scripts/check-phase19.6.1.sh --all`, and `go build -tags summercms_mcp ./...`; then `git log --oneline v0.4.3..HEAD`, whose list and line count the executor pastes into the checkpoint message as the measured content of the tag (concurrent sessions add commits, so no count is assumed in advance).
- Task 4: Give the go-ahead to tag and push oauthserver and Journal
- Tag and push sm-oauthserver-plugin (backend scope, oas_at_ verifier, MCP consent JSON, revoked events) and sm-journal-plugin (MCP tools and prompt, optionalBackendPrincipal fix), both pinned to the plan-02 framework tag, then bump the demo's pointers.
- Both releases are additive (plan 05's gate proves no removed export since d31f8e7 and ffa2c8f). Proposed versions (P-20): oauthserver v0.1.2, Journal v0.2.3. The demo then pins both tags in two commits (go.mod, go.sum, submodule pointer each) and the vue-demo-app submodule pointer in a third. Pre-checks before asking: clean trees in both plugin repos, `go vet ./... && go test -count=1 ./...` on Docker in each, READMEs updated, the demo e2e green with the submodules at the exact commits to be tagged.
+ Task 4: Give the go-ahead for every push of Task 5: tag and push oauthserver and Journal, push vue-demo-app
+ One go-ahead covering exactly the three pushes Task 5 performs, in three repositories: (1) sm-oauthserver-plugin (backend scope, oas_at_ verifier, MCP consent JSON, revoked events) — `git push origin master --tags` with its new tag; (2) sm-journal-plugin (MCP tools and prompt, optionalBackendPrincipal fix) — `git push origin main --tags` with its new tag; (3) vue-demo-app (the `/connect` consent page, the `/link-admin` proof page, the `connect.*` and `linkAdmin.*` i18n keys from Task 2) — `git -C ../sm-demo-app/vue-demo-app push origin master`, no tag. Both plugins are pinned to the plan-02 framework tag. The demo's three pointer bumps are local commits in sm-demo-app; sm-demo-app itself is neither pushed nor deployed in this plan (both are recorded in deferred-items.md for a separate go-ahead).
+ Both plugin releases are additive (plan 05's gate proves no removed export since d31f8e7 and ffa2c8f). Proposed versions (P-20): oauthserver v0.1.2, Journal v0.2.3. The demo then pins both tags in two commits (go.mod, go.sum, submodule pointer each) and the pushed vue-demo-app commit in a third. Pre-checks before asking: clean trees in both plugin repos and in `../sm-demo-app/vue-demo-app`, `go vet ./... && go test -count=1 ./...` on Docker in each plugin, `npm --prefix ../sm-demo-app/vue-demo-app run test` and `run build` green, READMEs updated, the demo e2e green with the submodules at the exact commits to be tagged and pushed. The checkpoint message lists the three repositories, their branches and the two versions so the answer names every push (CLAUDE.md: no push, tag or deploy without the user's go-ahead).
- Answer tag-both (optionally with other version numbers) or hold
+ Answer tag-both (optionally with other version numbers; tag-both covers the vue-demo-app push as well) or holdTask 5: Release both plugins, pin them in the demo, document nginx and deploy, and hand the demo to a real MCP client
- Pushed plugin tags are pinned by the demo and announced; the nginx locations open the MCP paths on the public demo once deployed.
+ Pushed plugin tags are pinned by the demo and announced; the pushed vue-demo-app commit publishes the consent and link pages; the nginx locations open the MCP paths on the public demo once deployed.The Task 4 checkpoint answered tag-both (or named versions); `git -C ../sm-oauthserver-plugin status --short`, `git -C ../sm-journal-plugin status --short` and `git -C ../sm-demo-app/vue-demo-app status --short` are empty; `git -C ../sm-demo-app status --short` shows only this plan's files../sm-oauthserver-plugin/go.mod, ../sm-oauthserver-plugin/go.sum, ../sm-journal-plugin/go.mod, ../sm-journal-plugin/go.sum, ../sm-demo-app/go.mod, ../sm-demo-app/go.sum, ../sm-demo-app/plugins/golem15/oauthserver, ../sm-demo-app/plugins/golem15/journal, ../sm-demo-app/vue-demo-app, ../sm-demo-app/deploy/nginx/demo.summercms.io.conf, ../sm-demo-app/DEPLOY.md, ../sm-demo-app/config/http.yaml, .planning/phases/19.7-generic-mcp-module/deferred-items.md
@@ -285,9 +285,9 @@ Output: the verifier, the scope, the consent endpoints and pages, Journal's tool
- summercms.go/docs/services/mcp.md § Operations (plan 02)
- Releases: in each plugin repo `go.mod` already requires the plan-02 tag (Tasks 1-3); run the pre-checks, write the annotated tag message, `git tag -a ` and `git push origin --tags` (oauthserver master, Journal main). Demo pins, three commits: `chore: bump oauthserver to ` (go.mod require, go.sum, submodule at the tag), `chore: bump journal to `, `chore: bump vue-demo-app (MCP consent and link pages)` (the vue-demo-app submodule after its commit is pushed to its master, which is this task's fourth push). Verify `go build -tags summercms_mcp ./... && go test -count=1 -short ./...` in the demo after each pin.
+ Releases: in each plugin repo `go.mod` already requires the plan-02 tag (Tasks 1-3); run the pre-checks, write the annotated tag message, `git tag -a ` and `git push origin --tags` (oauthserver master, Journal main). Then `git -C ../sm-demo-app/vue-demo-app push origin master` for the Task 2 commit with the `/connect` and `/link-admin` pages (no tag). These three pushes are exactly the ones the Task 4 go-ahead named; no other repository is pushed by this task. Demo pins, three local commits in sm-demo-app: `chore: bump oauthserver to ` (go.mod require, go.sum, submodule at the tag), `chore: bump journal to `, `chore: bump vue-demo-app (MCP consent and link pages)` (the submodule at the pushed commit). sm-demo-app itself is not pushed and not deployed here; both wait for the user's go-ahead and go to deferred-items.md below. Verify `go build -tags summercms_mcp ./... && go test -count=1 -short ./...` in the demo after each pin.
- nginx (`deploy/nginx/demo.summercms.io.conf`): add Go locations `location = /mcp` (proxy with `proxy_buffering off`, `proxy_read_timeout 1h`, `proxy_http_version 1.1`, `chunked_transfer_encoding on` for SSE), `location ^~ /.well-known/oauth-protected-resource`, `location = /.well-known/oauth-authorization-server`, and `location ^~ /oauth/mcp/` (authorize, register, token and the consent JSON), each with the same `proxy_pass` and headers as the existing Go blocks (the ACME location stays first); run `scripts/check-deploy.sh`. DEPLOY.md: a new section "MCP endpoint" after Realtime: what is mounted, the `summercms_mcp` build tag already in `summer.yaml`, the `.env` keys (`SUMMER_MCP__RESOURCE=https://demo.summercms.io/mcp`, `SUMMER_GOLEM15__OAUTHSERVER__MCP__RESOURCE` the same, `SUMMER_MCP__SESSIONS__STORE=memory` with the Redis alternative per P-19), the nginx install step (reuse the "When deploy/nginx/... changed" instructions), how an editor connects (`https://demo.summercms.io/mcp`, OAuth sign-in, `/connect`, linking an admin from My account), and a smoke (`curl -sS https://demo.summercms.io/.well-known/oauth-protected-resource/mcp`). Update the Overview sentence that says `/oauth` and `/.well-known` are not routed to Go. Record the two unresolved operational items in `.planning/phases/19.7-generic-mcp-module/deferred-items.md` (framework repo): the deploy itself (the user's go-ahead) and Redis on rome if a second instance is ever added.
+ nginx (`deploy/nginx/demo.summercms.io.conf`): add Go locations `location = /mcp` (proxy with `proxy_buffering off`, `proxy_read_timeout 1h`, `proxy_http_version 1.1`, `chunked_transfer_encoding on` for SSE), `location ^~ /.well-known/oauth-protected-resource`, `location = /.well-known/oauth-authorization-server`, and `location ^~ /oauth/mcp/` (authorize, register, token and the consent JSON), each with the same `proxy_pass` and headers as the existing Go blocks (the ACME location stays first); run `scripts/check-deploy.sh`. DEPLOY.md: a new section "MCP endpoint" after Realtime: what is mounted, the `summercms_mcp` build tag already in `summer.yaml`, the `.env` keys (`SUMMER_MCP__RESOURCE=https://demo.summercms.io/mcp`, `SUMMER_GOLEM15__OAUTHSERVER__MCP__RESOURCE` the same, `SUMMER_MCP__SESSIONS__STORE=memory` with the Redis alternative per P-19), the nginx install step (reuse the "When deploy/nginx/... changed" instructions), how an editor connects (`https://demo.summercms.io/mcp`, OAuth sign-in, `/connect`, linking an admin from My account), and a smoke (`curl -sS https://demo.summercms.io/.well-known/oauth-protected-resource/mcp`). Update the Overview sentence that says `/oauth` and `/.well-known` are not routed to Go. Record the unresolved operational items in `.planning/phases/19.7-generic-mcp-module/deferred-items.md` (framework repo): pushing sm-demo-app's three pointer commits and the deploy itself (each needs the user's go-ahead), and Redis on rome if a second instance is ever added.
Local hand-off for the manual check: build the demo (`summer build` in the demo), start it against the compose Postgres with `.env` values for the resource URLs pointing at `http://127.0.0.1:8097/mcp`, start the Nuxt site (`npm --prefix ../sm-demo-app/vue-demo-app run dev`), seed through `demo:seed`, create a frontend user and an admin with Journal access, and leave both processes running for the human check below. Stop them after the check.
diff --git a/.planning/phases/19.7-generic-mcp-module/19.7-05-PLAN.md b/.planning/phases/19.7-generic-mcp-module/19.7-05-PLAN.md
index 92a5eea9..8968cd78 100644
--- a/.planning/phases/19.7-generic-mcp-module/19.7-05-PLAN.md
+++ b/.planning/phases/19.7-generic-mcp-module/19.7-05-PLAN.md
@@ -33,6 +33,10 @@ files_modified:
- .planning/phases/19.7-generic-mcp-module/19.7-SECURITY.md
- .planning/phases/19.7-generic-mcp-module/19.7-SECURITY-REVIEW.md
- .planning/phases/19.7-generic-mcp-module/deferred-items.md
+ - .planning/todos/pending/2026-10-17-plugin-registered-frontend-permissions.md
+ - .planning/todos/pending/2026-10-17-frontend-backend-account-link.md
+ - .planning/todos/done/2026-10-17-plugin-registered-frontend-permissions.md
+ - .planning/todos/done/2026-10-17-frontend-backend-account-link.md
autonomous: true
requirements: [SC-1, SC-2, SC-3, SC-4, D-05, D-08, D-17, D-20, D-21, D-22, D-25, D-26, D-27, D-31, D-36]
@@ -230,7 +234,7 @@ Output: named tests in six repositories, the gate script, the filled validation
Task 4: Security review of T-19.7-01..41 and T-19.7-SC: link proof, dispatch marker, session binding, scope ceilings and superuser reach
- .planning/phases/19.7-generic-mcp-module/19.7-SECURITY.md, .planning/phases/19.7-generic-mcp-module/19.7-SECURITY-REVIEW.md, .planning/phases/19.7-generic-mcp-module/deferred-items.md
+ .planning/phases/19.7-generic-mcp-module/19.7-SECURITY.md, .planning/phases/19.7-generic-mcp-module/19.7-SECURITY-REVIEW.md, .planning/phases/19.7-generic-mcp-module/deferred-items.md, .planning/todos/pending/2026-10-17-plugin-registered-frontend-permissions.md, .planning/todos/pending/2026-10-17-frontend-backend-account-link.md, .planning/todos/done/2026-10-17-plugin-registered-frontend-permissions.md, .planning/todos/done/2026-10-17-frontend-backend-account-link.md
- .planning/phases/19.4-oidc-provider-on-wristband/19.4-SECURITY.md and 19.4-SECURITY-REVIEW.md (the two formats, the Method section, the gaps-fixed table, the evidence table columns)
- The `` blocks of 19.7-01-PLAN.md, 19.7-02-PLAN.md, 19.7-03-PLAN.md and 19.7-04-PLAN.md (T-19.7-01..38 and T-19.7-SC) and this plan's (T-19.7-39..41)
@@ -240,10 +244,12 @@ Output: named tests in six repositories, the gate script, the filled validation
For each threat: read the implementing code and record file and line; find the pinning test (from Tasks 1-3 or the plans' smokes); where the code is right but no test pins it, add the test in the owning repo as its own commit; where the code is wrong, fix it in its own `fix(` commit and re-run the gate. Cross-cutting sweeps the review must perform and record: (1) link proof — no code path other than `ConfirmLinkRequest` and `SetLink` writes `golem15_user_backend_links`, `SetLink` is reachable only from the permission-guarded record action, and the confirm compares `iat`; (2) marker — the only non-test caller of the bouncer dispatch setter is `modules/pier/dispatch.go` (the gate's marker stage), no header or query is read for it, `ResolveBackend` refuses a frontend marker, the inner request has no Authorization or Cookie; (3) session binding — binding compared on every stateful request, DELETE and revocation propagate through the store, rehydration never trusts stored identity; (4) scope ceilings — `read` hides non-read-only tools, `backend` without a link yields no backend subject, consent drops `backend` without a link, personal tokens carry their own scopes and are off by default; (5) superuser reach (P-23) — weigh the optional `mcp.refuse_superuser_backend` switch and record the verdict with reasons; implementing it is a `fix(pier)` plus README and docs, and the patch tag it needs goes to deferred-items for the user's go-ahead (raise a blocking-human checkpoint before any tag or push); (6) logs — no token, argument or body in any pier, verifier or link log line; (7) no-store on every consent and link response that carries a code or token. Write `19.7-SECURITY-REVIEW.md` (Method, Gaps fixed with commit table, Threat register with Mitigation as built / Implementation / Test / Status, Cross-cutting findings, Accepted risks with reasons, Residual items) and `19.7-SECURITY.md` (frontmatter `phase: "19.7"`, `slug`, `status: verified`, `threats_open: 0`, `asvs_level: 1`, `created`; Trust Boundaries, Threat register summary, Accepted risks, Audit trail). Add any residual operational item (the deploy, Redis on rome, the superuser switch tag if adopted) to deferred-items.md.
+
+ Close the two folded todos in this task's planning-docs commit (D-01; CLAUDE.md rule 4): `git mv .planning/todos/pending/2026-10-17-plugin-registered-frontend-permissions.md .planning/todos/done/` and `git mv .planning/todos/pending/2026-10-17-frontend-backend-account-link.md .planning/todos/done/` (the closed directory is `.planning/todos/done/`, per `.planning/todos/README.md`), then end each file with a `## Closed ` note naming what shipped and where: for the first, the `pact.HasFrontendPermissions` capability (plan 01) and the user plugin's catalog in the permission editors (plan 03); for the second, the `golem15_user_backend_links` table, the proof round-trip, the published link resolver and the Administrators-screen set/remove actions (plan 03), each with the sm-user-plugin version from plan 03's SUMMARY. The g15office side — moving tasks `hmguritm` and `tqyvnzxx` into the phase's milestone task `dcnv4mha` with the one-line comment, and the phase-advance comment — is not done by the executor: the orchestrator's phase-completion step owns it (CLAUDE.md rule 4, `.planning/G15OFFICE.md`); record the two todo task IDs and the phase task ID in the SUMMARY so that step has them.
- test -f .planning/phases/19.7-generic-mcp-module/19.7-SECURITY.md && test -f .planning/phases/19.7-generic-mcp-module/19.7-SECURITY-REVIEW.md && grep -q "threats_open: 0" .planning/phases/19.7-generic-mcp-module/19.7-SECURITY.md && for n in $(seq -w 1 41); do grep -q "T-19.7-$n" .planning/phases/19.7-generic-mcp-module/19.7-SECURITY-REVIEW.md || { echo "missing T-19.7-$n"; exit 1; }; done && grep -q "T-19.7-SC" .planning/phases/19.7-generic-mcp-module/19.7-SECURITY-REVIEW.md && bash scripts/check-phase19.7.sh --all
- non-zero exit, a "missing T-19.7-NN" line, or a gate stage printing FAIL
+ test -f .planning/phases/19.7-generic-mcp-module/19.7-SECURITY.md && test -f .planning/phases/19.7-generic-mcp-module/19.7-SECURITY-REVIEW.md && grep -q "threats_open: 0" .planning/phases/19.7-generic-mcp-module/19.7-SECURITY.md && for n in $(seq -w 1 41); do grep -q "T-19.7-$n" .planning/phases/19.7-generic-mcp-module/19.7-SECURITY-REVIEW.md || { echo "missing T-19.7-$n"; exit 1; }; done && grep -q "T-19.7-SC" .planning/phases/19.7-generic-mcp-module/19.7-SECURITY-REVIEW.md && test -f .planning/todos/done/2026-10-17-plugin-registered-frontend-permissions.md && test -f .planning/todos/done/2026-10-17-frontend-backend-account-link.md && ! test -e .planning/todos/pending/2026-10-17-plugin-registered-frontend-permissions.md && ! test -e .planning/todos/pending/2026-10-17-frontend-backend-account-link.md && bash scripts/check-phase19.7.sh --all
+ non-zero exit, a "missing T-19.7-NN" line, a folded todo still under todos/pending/ or absent under todos/done/, or a gate stage printing FAIL
- Every id T-19.7-01 through T-19.7-41 and T-19.7-SC appears in 19.7-SECURITY-REVIEW.md with an Implementation cell naming a file and line and a Test cell naming a test, and a Status of verified, fixed in commit , or accepted with a reason
@@ -251,8 +257,9 @@ Output: named tests in six repositories, the gate script, the filled validation
- `grep -c "threats_open: 0" .planning/phases/19.7-generic-mcp-module/19.7-SECURITY.md` prints 1 and `grep -c "^status: verified" .planning/phases/19.7-generic-mcp-module/19.7-SECURITY.md` prints 1
- Every fix or added test pin named in the Gaps-fixed table is a commit that exists (`git -C cat-file -t ` prints commit)
- `bash scripts/check-phase19.7.sh --all` is green after the review's commits
+ - `grep -c "^## Closed" .planning/todos/done/2026-10-17-plugin-registered-frontend-permissions.md .planning/todos/done/2026-10-17-frontend-backend-account-link.md` prints 1 per file, neither file exists under `.planning/todos/pending/`, and the SUMMARY names g15office `hmguritm`, `tqyvnzxx` and `dcnv4mha` as the tasks the orchestrator updates at phase completion
- Every threat of the phase is verified, fixed or accepted with evidence; the security contract reports no open threat; the gate is green after the review.
+ Every threat of the phase is verified, fixed or accepted with evidence; the security contract reports no open threat; the gate is green after the review; the two folded todos are closed under todos/done/ with the g15office update left to the orchestrator's phase-completion step.
@@ -263,7 +270,7 @@ Output: named tests in six repositories, the gate script, the filled validation
**Gate**: `scripts/check-phase19.7.sh` (stages self-test, go, plugins, consumers, docker, admin, docs, marker; env `PHASE197_*`, `USER_PLUGIN`, `OAUTHSERVER_PLUGIN`, `JOURNAL_PLUGIN`, `DEMO_APP`, `FONOTEKA`, `BM_APP`)
**Framework tests**: `modules/pier/helpers_test.go` (fakeGuard, fakeVerifier, fakeLinkResolver, fakePersonalResolver, newInstance, twoInstances, sdkClient), `TestToolsList`, `TestCatalog`, `TestDispatch`, `TestGuardRules`, `TestSessions`, `TestFanOut` (pier); `TestDispatchMarker` (bouncer and cabana); `TestStreamingRoute`, `TestPierMount` (surf); `TestMCPScopes` (wristband); `TestAccountContributions`, `TestRecordActionPrompt` (cabana); vitest `admin/tests/account/AccountContributions.test.ts`, `admin/tests/form/RecordActionsPrompt.test.ts`
**Plugin and demo tests**: `TestBackendLink`, `TestFrontendPermissionCatalog`, `TestUserMCPGuard` (sm-user-plugin); `TestMCPTokenVerifier`, `TestMCPConsent` (sm-oauthserver-plugin); `TestJournalMCP` (sm-journal-plugin); `TestMCPEndToEnd` extended (sm-demo-app, tagged)
-**Documents**: `19.7-VALIDATION.md` filled (`nyquist_compliant: true`), `19.7-SECURITY.md`, `19.7-SECURITY-REVIEW.md`, `deferred-items.md`
+**Documents**: `19.7-VALIDATION.md` filled (`nyquist_compliant: true`), `19.7-SECURITY.md`, `19.7-SECURITY-REVIEW.md`, `deferred-items.md`; the two folded todos moved to `.planning/todos/done/` with their `## Closed` notes
**Possible fixes**: `fix(` commits in the owning repositories; the optional `mcp.refuse_superuser_backend` key if the review adopts it (recorded with its tag need)
diff --git a/.planning/phases/19.7-generic-mcp-module/19.7-RESEARCH.md b/.planning/phases/19.7-generic-mcp-module/19.7-RESEARCH.md
index c988d589..9f18ebee 100644
--- a/.planning/phases/19.7-generic-mcp-module/19.7-RESEARCH.md
+++ b/.planning/phases/19.7-generic-mcp-module/19.7-RESEARCH.md
@@ -553,22 +553,26 @@ Permissions [VERIFIED: sm-journal-plugin/controllers/api/auth.go:26-30]: `permAc
| A8 | Granting `backend` only when a link exists at consent time (and dropping it otherwise) is acceptable UX | Security Domain | A later link would need re-consent (step-up) |
| A9 | Per-call binary growth of ~2.9 MiB is acceptable if pier is always linked | Open Question 2 | Every app grows even with pier disabled |
-## Open Questions
+## Open Questions (RESOLVED)
+
+All eight questions were resolved at plan confirmation on 2026-10-21 and recorded in 19.7-CONTEXT.md § "Decisions after research" (D-32..D-35; D-36 records the agreed plan split). Each question below ends with its resolution.
1. **Stateful-only (D-25) vs hybrid with the 2026-07-28 stateless protocol.**
- What we know: the current spec removed sessions; the SDK serves 2026-07-28 only statelessly; SDK clients fall back to 2025-11-25 against a stateful server (probe).
- What's unclear: whether the user wants pier to also answer 2026-07-28 clients (stateless dispatch per request, `subscriptions/listen` for push), which is additive to D-25's stateful path.
- Recommendation: ship D-25 as locked; design the identity/dispatch/filter layers so a stateless handler can reuse them; raise it at plan confirmation. Do not silently adopt the hybrid.
+ - **RESOLVED (D-32):** hybrid adopted. pier also answers 2026-07-28 clients through a stateless path next to the stateful one, sharing identity, dispatch and per-principal filtering; push for stateless clients uses `subscriptions/listen`.
2. **Always link pier, or behind a build tag?**
- What we know: linking the SDK costs ~2.9 MiB stripped; the framework already has the opt-in build-tag pattern (`summercms_s3`: `s3_on.go //go:build summercms_s3`, `s3_off.go //go:build !summercms_s3`, `summer.yaml build.tags`) [VERIFIED: modules/lagoon/attach/s3_on.go:1, s3_off.go:1, bucket.go:27-28].
- What's unclear: SC-1 says "enables the module in config"; a tag adds a build step.
- Recommendation: ask the user; if always linked, keep all plugin-facing types SDK-free anyway (they must be, for the tag option to remain open).
-3. **D-04 "backend Users screen" does not exist.** cabana mounts no backend-users controller; admins come from `admin:create`/`admin:reset-password` commands [VERIFIED: modules/cabana/http.go:247-330, commands.go:21,45]. 19.6's Reset 2FA uses the user plugin's **own** Users screen (`AdminRecordActions` + a published `TwoFactorAdmin` slot), not a generic cabana seam (19.6-01-PLAN :49, :270), so there is nothing to reuse beyond that pattern. Recommendation: My Profile gets the generic cabana seam (D-04 part 1); admin set/remove goes on the sm-user-plugin **frontend** Users screen as a record action "Remove backend link" (Danger, `Applies` only when linked, permission) plus a way to set it (a form field with `DropdownOptions` of backend admins, or an `AdminAction` widget), and/or a `user:link-backend` console command. Needs user confirmation.
-4. **Frontend permission semantics** (Pitfall 3): mirror the route (group grants) — recommended — or change `user.permission` to `MergedPermissions` (behaviour change in a core plugin).
-5. **Session binding granularity** (A3): `(client_id, user_id)` vs token hash.
-6. **`ai` scope**: ignore for tools (recommended) or require it for some tool class.
-7. **MCP consent surface.** wristband sends the browser to `{issuer}/connect?request=` [VERIFIED: modules/wristband/authorize.go:174 — `spa := s.opts.Issuer + "/connect?request=" + rfc3986Escape(requestID)`]. Recommendation: sm-oauthserver-plugin adds JSON consent endpoints for the MCP server (show/decide, `jwt.auth`, mirroring fonoteka's `ConsentShow`/`ConsentStore` contract) that include the linked admin's display name when `backend` is requested; vue-demo-app adds `/connect` and the link-proof page. The demo's nginx does not route `/oauth` or `/.well-known` to Go yet (sm-demo-app/DEPLOY.md:34), so the deploy plan must add `/mcp`, `/.well-known/oauth-protected-resource*`, `/.well-known/oauth-authorization-server` and `/oauth/mcp/*`.
-8. **Permission code spelling.** Existing backend codes are `golem15.users.*` (`golem15.users.access_users`, `…access_groups`, `…access_settings`, `…impersonate_user`, `…manage_privileged_groups` [VERIFIED: fonoteka.go/plugins/golem15/user/admin_permissions.go:17-41]); 19.6 adds `golem15.users.reset_two_factor`. Recommend `golem15.users.manage_backend_links` (CONTEXT's example `golem15.user.…` would break the pattern).
+ - **RESOLVED (D-33):** build tag. pier compiles in only with `summercms_mcp` in `summer.yaml` `build.tags`; without the tag the binary carries no go-sdk; `mcp.enabled` still defaults to off; `pact` types stay SDK-free.
+3. **D-04 "backend Users screen" does not exist.** cabana mounts no backend-users controller; admins come from `admin:create`/`admin:reset-password` commands [VERIFIED: modules/cabana/http.go:247-330, commands.go:21,45]. 19.6's Reset 2FA uses the user plugin's **own** Users screen (`AdminRecordActions` + a published `TwoFactorAdmin` slot), not a generic cabana seam (19.6-01-PLAN :49, :270), so there is nothing to reuse beyond that pattern. Recommendation: My Profile gets the generic cabana seam (D-04 part 1); admin set/remove goes on the sm-user-plugin **frontend** Users screen as a record action "Remove backend link" (Danger, `Applies` only when linked, permission) plus a way to set it (a form field with `DropdownOptions` of backend admins, or an `AdminAction` widget), and/or a `user:link-backend` console command. Needs user confirmation. **RESOLVED (D-34):** admins set and remove links on the Phase 19.6.1 Administrators screen through its record-action seam (a prompted set action and a remove action behind `golem15.users.manage_backend_links`); My Profile keeps the generic cabana account-contribution seam (D-04 part 1). No new cabana backend-users screen.
+4. **Frontend permission semantics** (Pitfall 3): mirror the route (group grants) — recommended — or change `user.permission` to `MergedPermissions` (behaviour change in a core plugin). **RESOLVED (D-35):** mirror the route. Frontend tool filtering uses group grants, exactly what `user.permission` enforces; `MergedPermissions` is not changed.
+5. **Session binding granularity** (A3): `(client_id, user_id)` vs token hash. **RESOLVED (D-35):** `(client_id, user_id)`, so sessions survive token refresh.
+6. **`ai` scope**: ignore for tools (recommended) or require it for some tool class. **RESOLVED (D-35):** ignored; `ai` plays no part in the read/write ceiling.
+7. **MCP consent surface.** wristband sends the browser to `{issuer}/connect?request=` [VERIFIED: modules/wristband/authorize.go:174 — `spa := s.opts.Issuer + "/connect?request=" + rfc3986Escape(requestID)`]. Recommendation: sm-oauthserver-plugin adds JSON consent endpoints for the MCP server (show/decide, `jwt.auth`, mirroring fonoteka's `ConsentShow`/`ConsentStore` contract) that include the linked admin's display name when `backend` is requested; vue-demo-app adds `/connect` and the link-proof page. The demo's nginx does not route `/oauth` or `/.well-known` to Go yet (sm-demo-app/DEPLOY.md:34), so the deploy plan must add `/mcp`, `/.well-known/oauth-protected-resource*`, `/.well-known/oauth-authorization-server` and `/oauth/mcp/*`. **RESOLVED (D-35):** JSON consent endpoints in sm-oauthserver-plugin with the `/connect` page (and the `/link-admin` proof page) in vue-demo-app, plan 04 Task 2; the nginx locations are plan 04 Task 5.
+8. **Permission code spelling.** Existing backend codes are `golem15.users.*` (`golem15.users.access_users`, `…access_groups`, `…access_settings`, `…impersonate_user`, `…manage_privileged_groups` [VERIFIED: fonoteka.go/plugins/golem15/user/admin_permissions.go:17-41]); 19.6 adds `golem15.users.reset_two_factor`. Recommend `golem15.users.manage_backend_links` (CONTEXT's example `golem15.user.…` would break the pattern). **RESOLVED (D-35):** `golem15.users.manage_backend_links`.
## Environment Availability