From 362923cf5b4f314397835a695f624b29a207b8fa Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Tue, 20 Oct 2026 00:51:46 +0200 Subject: [PATCH] docs(19.4): re-verify phase after the additivity check fix --- .../19.4-VERIFICATION.md | 258 +++++++++--------- 1 file changed, 125 insertions(+), 133 deletions(-) diff --git a/.planning/phases/19.4-oidc-provider-on-wristband/19.4-VERIFICATION.md b/.planning/phases/19.4-oidc-provider-on-wristband/19.4-VERIFICATION.md index 3ba471fd..885b39cd 100644 --- a/.planning/phases/19.4-oidc-provider-on-wristband/19.4-VERIFICATION.md +++ b/.planning/phases/19.4-oidc-provider-on-wristband/19.4-VERIFICATION.md @@ -1,7 +1,7 @@ --- phase: 19.4-oidc-provider-on-wristband -verified: 2026-10-19T21:10:00Z -status: human_needed +verified: 2026-10-19T22:46:04Z +status: passed score: 15/15 must-haves verified covered_files: - .planning/phases/19.4-oidc-provider-on-wristband/19.4-01-PLAN.md @@ -57,236 +57,228 @@ covered_files: - modules/wristband/token.go - modules/wristband/token_test.go - scripts/check-phase19.4.sh -covered_digest: "v2:sha256:a3c1754c6cdf2bbf236657e6a4391c25f7f8ea05b97056c3acc1403f03ccc2db" +covered_digest: "v2:sha256:009523ce84f9c607d26ec0b3a9443808e28c76f58ffbabde99586a19b599cc87" behavior_unverified: 0 overrides_applied: 0 +re_verification: + previous_status: human_needed + previous_score: 15/15 + gaps_closed: [] + gaps_remaining: [] + regressions: [] deferred: - truth: "D-14 PHP parity recording: TestPHPParityReplay and TestIDTokenShapeParity green against recordings of the PHP Golem15.OAuthServer plugin" addressed_in: "WavePath cutover phase (not yet on the roadmap; preliminary task)" - evidence: "User decision 2026-10-19, recorded in 19.4-CONTEXT.md D-14 amendment and the amended 19.4-03/19.4-04 must-haves (commit 3386cbe); todo .planning/todos/pending/2026-10-19-record-php-oidc-parity-against-wavepath.md (g15office govfaqir). The phase-19.4 D-14 truth is the fail-closed harness, which is verified." -human_verification: + evidence: "User decision 2026-10-19, recorded in 19.4-CONTEXT.md D-14 amendment (line 51) and the amended 19.4-03/19.4-04 must-haves (commit 3386cbe); todo .planning/todos/pending/2026-10-19-record-php-oidc-parity-against-wavepath.md (g15office govfaqir). The phase-19.4 D-14 truth is the fail-closed harness, which is verified." +human_verified: - test: "Run the plugin in a local application (or keep the test harness server alive), set a site name, a logo and an accent colour in row 1 of golem15_oauthserver_settings, open an authorize URL for a test client, then open the inactive-client error page. Compare both side by side with the PHP consent.htm and error.htm pages in a browser." - expected: "Same structure and copy as PHP, branding (name, logo, colours, font, custom CSS) applied, error heading in the fixed danger colour #B91C1C." - why_human: "TestConsentPageBrandingSmoke and TestConsentPageSecurity prove the branding values reach the HTML and cannot inject markup; no test judges visual likeness to the PHP pages (D-03/D-14 say HTML need not be byte-identical)." + uat_row: 1 + recorded_at: "2026-10-19T22:23:50.000Z" - test: "Open the Clients and Branding screens in the embedded admin SPA as an administrator with manage_clients and manage_settings: create a client, check the one-time secret reveal callout, the scopes taglist, the authorized-users table on the form, the regenerate-secret danger button, and the Settings link to Branding with the logo upload." - expected: "Both screens render usably; the secret shows once and not on reload; regenerate asks for confirmation." - why_human: "TestClientsAdminSmoke and TestBrandingAdminSmoke drive cabana's JSON admin API only; no browser test looks at how the SPA renders these partials and widgets." + uat_row: 2 + recorded_at: "2026-10-19T22:23:50.000Z" - test: "Review the LLM-judge verdicts on the 11 judgment-tier prohibitions in the 'Prohibitions' section of this report and confirm or reject each." - expected: "Each prohibition held as stated (the verifier found evidence for all 11)." - why_human: "Judgment-tier prohibitions are non-authoritative when judged by the verifier and must not be absorbed silently into a pass (ADR-550 D4)." + uat_row: 3 + recorded_at: "2026-10-19T22:23:50.000Z" --- # Phase 19.4: OIDC provider on wristband Verification Report **Phase Goal:** `wristband` is a full OpenID Connect provider that existing relying parties can switch to without noticing: discovery, JWKS with asymmetric signing and key rotation, `id_token`, userinfo, resumable authorization with account switching, a consent store and pairwise subjects. The existing issuer, signing keys and client secrets carry over. -**Verified:** 2026-10-19T21:10:00Z -**Status:** human_needed -**Re-verification:** No, initial verification +**Verified:** 2026-10-19T22:46:04Z +**Status:** passed +**Re-verification:** Yes. The previous report (c86bcbf, `human_needed`, no gaps) went stale. Since then sm-user-plugin merged origin, the additivity check was fixed, the human UAT was recorded, and the plugins were pushed. -Repositories checked: summercms.go (`modules/wristband`, `modules/tide`, `scripts/check-phase19.4.sh`, `docs/`), `../sm-oauthserver-plugin` (13 commits, clean tree, not pushed), sm-user-plugin at `../fonoteka.go/plugins/golem15/user` (4 phase commits b82d85f, ee47440, 80b436d, 9335e80; ahead 7 / behind 1 of origin, not pushed), and `../fonoteka.go` (687e25a: explicit Resource). +## What changed since the previous report -**MVP mode note:** ROADMAP marks the phase `Mode: mvp`, but its goal is not in user-story form, and `/msd mvp-phase 19.4` was never run. Every plan records this and does not invent a story. The orchestrator asked for verification against the ROADMAP success criteria, so this report uses standard goal-backward verification. A relying-party flow table stands in for the User Flow Coverage table. +| Change | Where | Effect on the verdict | +| --- | --- | --- | +| sm-user-plugin merged `origin/master` as `4c5d3b8`, bringing in `24b16b1` (2026-10-07, pre-phase): `console.APITokenCommands` and `console.RequirePasswordChange` now take a `withDB` opener | `../fonoteka.go/plugins/golem15/user` | The merge resolution in `plugin.go` keeps both origin's `withDB` and the local `user:backfill-handles` (`git show --cc 4c5d3b8`). Outside `plugin.go`, `git diff 9335e80 4c5d3b8` is byte-identical to `git diff 7a6fa0e 24b16b1`, so the merge dropped nothing from origin. The phase's own commits (`c077029..9335e80`) are unchanged and remove only lines inside `applyAvatar`'s body and the `bulk("deactivate", ...)` entry. No consumer outside the user plugin calls the two changed constructors. | +| Gate additivity stage accepts both pre-phase bases | summercms.go `d9f7bfa` (`scripts/check-phase19.4.sh`), plugin `36c8c63` (`zero_edit_test.go`, `testdata/PHP_METHOD_MAP.md`) | Test and gate code only. A declaration now counts as removed only when it is removed against both `c077029` and `24b16b1`. I re-ran the negative cases: `PHASE194_USER_BASE=c077029` refuses with the two upstream signatures (exit 1), and an unknown ref refuses with "git diff nosuchref ... failed (exit 128)" (exit 1). The intersection could miss an export that only origin's side had and the merge dropped, so I diffed against `24b16b1` directly: no exported func, method or type was removed. | +| sm-user-plugin and sm-oauthserver-plugin pushed | both repos, `master...origin/master` even | Remote-ref reflogs show the first phase pushes at 2026-10-19 20:35:56Z (user plugin) and 20:36:30Z (oauthserver plugin). That is after plan 04 ended (86bd5c4, 19:46Z) and after the previous verification (c86bcbf, 20:14Z). It is also before the UAT pass (22:23:50Z), so the human confirmed the prohibitions with the pushes already done. No tag in any of the three repos contains a phase commit. summercms.go is not pushed (ahead 40). | +| 19.4-UAT.md complete: 3/3 pass (ae113d2) | phase dir | Step 8b settles all three earlier human items (see Human Verification). | +| 19.4-VALIDATION.md validated (8c202a2), 19.4-SECURITY.md `threats_open: 0` (ba5f7a6) | phase dir | Planning docs. They agree with the code evidence. | ## Goal Achievement ### Relying-party flow coverage +The ROADMAP marks the phase `Mode: mvp`, but its goal is not a user story and `/msd mvp-phase 19.4` was never run. This report uses standard goal-backward verification against SC-1..SC-5 and D-01..D-19, and this table stands in for User Flow Coverage. + | Step a relying party takes | Expected | Evidence | Status | | --- | --- | --- | --- | -| Fetch discovery | PHP field set and order, `max-age=3600, public`, public and pairwise subjects, algorithms from the ring | `oidc_discovery.go` (ordered struct, algs from `PublishedKeys`); `TestDiscoveryFields`, `TestDiscoveryEndpoint`, go-oidc `NewProvider` in `TestRelyingPartyInterop` | ✓ | -| Fetch JWKS | active plus retired-in-grace keys, RSA and EC JWKs with `use`/`alg`/`kid` | `jwkFor` in `oidc_keys.go`; `KeyRing.PublishedKeys` grace filter; `TestJWKSShapes`, `TestMixedRingJWKS`, `TestRotateAndPruneSmoke` | ✓ | -| Authorize, log in, consent | login handoff with only `redirect=`, resume, one-shot consent token, silent or incremental consent, switch account | `oidc_authorize.go` / `oidc_consent.go`; `TestLoginHandoffAndSwitchSmoke`, `TestResumeAndSilentConsent`, `TestSwitchAccount`, `TestConsentCSRF`, `TestIncrementalConsent` | ✓ | -| Exchange the code | client_secret_basic and post, PKCE S256, burn-then-check, id_token with `aud` string, `nonce`, `at_hash`, signed by the active kid | `oidc_token.go` `redeemCode`, `signIDToken`; go-oidc `Verify` plus `VerifyAccessToken` on RS256 and ES256 rings | ✓ | +| Fetch discovery | PHP field set and order, `max-age=3600, public`, public and pairwise subjects, algorithms from the ring | `oidc_discovery.go` (ordered struct, `discoveryCacheControl = "max-age=3600, public"`, algorithms from `PublishedKeys`); `TestDiscoveryFields`, `TestDiscoveryEndpoint`, go-oidc `NewProvider` in `TestRelyingPartyInterop` | ✓ | +| Fetch JWKS | active key plus retired keys inside the grace window, RSA and EC JWKs | `classes/keyring.go:111` (`active OR (retired_at IS NOT NULL AND retired_at > now-Grace)`, Grace default 24h); `TestJWKSShapes`, `TestMixedRingJWKS`, `TestRotateAndPruneSmoke` | ✓ | +| Authorize, log in, consent | login handoff with only `redirect=`, resume, one-shot consent token, silent or incremental consent, switch account | `oidc_authorize.go`, `oidc_consent.go`, plugin `classes/host.go`; `TestLoginHandoffAndSwitchSmoke`, `TestResumeAndSilentConsent`, `TestSwitchAccount`, `TestConsentCSRF`, `TestIncrementalConsent` | ✓ | +| Exchange the code | client_secret_basic and post, PKCE S256, burn-then-check, id_token with string `aud`, `nonce`, `at_hash`, signed by the active kid | `oidc_token.go:111` `ActiveSigningKey`, `oidc_keys.go:234-241`; go-oidc `Verify` plus `VerifyAccessToken` on RS256 and ES256 rings | ✓ | | Read userinfo | same `sub` as the id_token, uniform 401 | `oidc_userinfo.go`; `TestUserInfoUniform401`, `TestUserInfoEndpoint`, interop `provider.UserInfo` | ✓ | -| Keep working after cutover | imported kid, secret, live access token, codes still work | `console/import_winter.go`, `console/import_keys.go`; `TestImportWinterCutoverSmoke` | ✓ | +| Keep working after the cutover | imported kid, secret, live access token and codes still work | `console/import_winter.go`, `console/import_keys.go` (`lagoon.NewEncrypted`), issuer from `golem15.oauthserver.issuer` or `app.url` (`servers.go:71-75`); `TestImportWinterCutoverSmoke` | ✓ | ### Observable Truths | # | Truth | Status | Evidence | | --- | --- | --- | --- | -| 1 | SC-1 / D-16: a decision note settles hand-written wristband vs zitadel/oidc and names the JOSE/JWT dependency before code lands | ✓ VERIFIED | `.planning/notes/2026-10-19-oidc-on-wristband-golang-jwt.md` (756a97c) names golang-jwt/jwt/v5, coreos/go-oidc/v3 v3.21.0, x/oauth2 v0.37.0 and rejects zitadel. It is an ancestor of 659b42a, the first commit adding `oidc_keys.go`. STACK.md marks the zitadel row Superseded. The framework `go.mod`/`go.sum` are unchanged since 95051d2. | -| 2 | SC-2: discovery, JWKS, userinfo and id_token in the token response work with RS256 or ES256 keys; rotation publishes old and new key together | ✓ VERIFIED | `oidc_discovery.go`, `oidc_keys.go` (RS256/ES256 method derived from the key type, std-lib JWKs), `oidc_token.go`, `oidc_userinfo.go`; plugin `classes/keyring.go` (grace window). `TestRelyingPartyInterop` (go-oidc, RS256 and ES256 subtests), `TestMixedKeyRingSmoke`, `TestES256Ring`, `TestRotateAndPruneSmoke`, `TestRelyingPartyFlow` (rotation within grace) all pass in the gate's docker stage, re-run by this verifier. | -| 3 | SC-3: resume, switch account, consent list and revocation, pairwise and public subjects; a scheduled prune removes expired codes, tokens and requests | ✓ VERIFIED | `Resume`, `SwitchAccount`, `ConsentRequest`/`DecideConsent` in wristband; `controllers/authorizations.go` (GET list, DELETE with a `FOR UPDATE` lock, 404 `not_found`); `oidc_subject.go` `SubjectSector` plus `SubjectFor`; `schedule.go` `oauthserver:prune` with `pact.Daily()`, asserted through `p.(pact.HasSchedule)` in `TestScheduleSmoke`; `console/prune.go` (six kinds). Tests: `TestRevokeAuthorization`, `TestAuthorizationsSmoke`, `TestPairwiseSubjects`, `TestSubjectStability`, `TestPruneExpired`, `TestLoginHandoffAndSwitchSmoke`. | -| 4 | SC-4 / D-11 / D-19: an existing issuer, its signing keys (original kids) and client secrets import so relying parties keep working; the phase has a security review | ✓ VERIFIED | `console/import_winter.go` (six tables, ids, hashes, setval, refuse without --force, dry run, missing-user abort); `console/import_keys.go` reads PHP `keys.json` + `{kid}.key/.pub`. I checked the format against `wavepath.org/.../classes/KeyRing.php:20` and its `gmdate('c')` timestamps; the import keeps each kid, encrypts with `lagoon.NewEncrypted` and keeps retired state. Hash scheme identical (PHP `hash('sha256')` hex, same `oas_cs_`/`oas_ac_`/`oas_at_` prefixes). `TestImportWinterCutoverSmoke`: old access token reads userinfo, a pre-cutover id_token verifies against the Go JWKS, the old secret authenticates, the new id_token carries the imported kid. `19.4-SECURITY-REVIEW.md` covers ASVS L1 and T-19.4-01..39 plus SC, none open. | -| 5 | SC-5: wristband README and docs updated; new code has unit tests delivered in the last plan | ✓ VERIFIED | README summary row equals the module README sentence (diff empty); `docs/services/oauth-server.md` has the "OpenID Connect provider" section; `TestDocsTree` and `docs:build --check` pass. Plan 04 commits (dd16746, 34c124a, 1651ba0, 9335e80) hold tests, the method map and the gate only, with no production code. | -| 6 | D-01 / D-06: one wristband engine, two `*Server` flavours; the plugin publishes them as one `*Servers`; wristband imports no ORM, no framework module, no application | ✓ VERIFIED | Flavour dispatch at `authorize.go:35`, `token.go:67`, `register.go:61`, `server.go:179`; `servers.go:147-155` builds `NewOIDCServer` + `SetOIDCBackend` and `NewServer` + `SetBackend`. `go list -deps ./modules/wristband` shows no gorm or summercms module; the application-name grep over non-test wristband files and the docs page is empty. | -| 7 | D-09: existing `Backend`/`Tx` source-compatible; fonoteka compiles and passes with only an explicit Resource; `DefaultOptions().Resource` is empty | ✓ VERIFIED | fonoteka `oauth_store.go` unchanged since 10.2; fonoteka 687e25a touches only `plugin.go` (line 157) and `oauth_resource_test.go`. The gate's docker stage ran 37 fonoteka OAuth tests including `TestOAuthResourceExplicit` and `TestPhase08Coverage`: all pass. `TestDefaultOptionsResourceEmpty` passes. | -| 8 | D-07 / D-08: kind isolation; MCP refuses openid and oidc-kind clients; OIDC accepts only oidc kind; DCR never yields identity scopes | ✓ VERIFIED | `authorize.go:64`, `token.go:197` (Kind oidc counts as unknown); plugin stores filter `kind` in SQL. `TestKindIsolation`, `TestKindIsolationSmoke`, `TestMCPRefusesOpenID`, `TestMCPStoreSmoke` pass. | -| 9 | D-10 / D-12: keys in the DB, private PEM `lagoon.Encrypted` and `json:"-"`, lazy first key, one active key under races, RS256 default, ES256 configurable | ✓ VERIFIED | `classes/keyring.go` (`generateFirst` ON CONFLICT on the partial index, silent logger); the migration holds `oas_keys_one_active ... WHERE active`. `TestKeyRingFirstKeyRaceSmoke`, `TestKeyRingMultiInstanceRace`, `TestKeyRingPreviousAppKey`, `TestKeyRing` pass. | -| 10 | D-03 / D-04: branded server-rendered consent and error pages with sanitised branding and a one-shot token; redirect consent mode with JSON endpoints; anonymous users get the login URL with only `redirect=` | ✓ VERIFIED (visual likeness → human item 1) | `views/consent.html.tmpl` (hidden `request_id`/`consent_token`, deny first), `classes/branding.go` sanitisers to `template.CSS`, `controllers/consent_api.go`; `TestConsentPageBrandingSmoke`, `TestConsentPageSecurity`, `TestConsentRedirectMode`, `TestLoginHandoffAndSwitchSmoke`. | -| 11 | D-05: sm-user-plugin changes additive only; claims from its users; cascade on user.Deleting / user.Deactivating | ✓ VERIFIED | Every removed non-test line since c077029 is inside `applyAvatar`'s body or the `bulk("deactivate", ...)` entry, so no exported declaration was removed. This is stronger than the gate's grep, which misses var, const and field removals (IN-09). `RequestUser`, `EndRequestSession`, `AvatarURL`, `DeactivatingEventName`, `FireDeactivatingEvent` exist. `TestRequestUser`, `TestEndRequestSession`, `TestAvatarURL`, `TestDeactivatingEvent`, `TestUserDeleteCascade`, `TestClaimBuilder` pass. | -| 12 | D-15: random UUIDv4 subjects per (user, sector), public by default, pairwise opt-in with `ErrSectorRequired`; userinfo sub equals id_token sub | ✓ VERIFIED | `oidc_subject.go`; store `SubjectFor` ON CONFLICT then select; `TestOIDCSubjectIsRandomSmoke`, `TestPairwiseSubjectSmoke`, `TestPairwiseSubjects`. | -| 13 | D-17: refresh off by default; opt-in per client with rotation and lineage revocation | ✓ VERIFIED | `oidc_token.go:286`, `:303-425`; `TestRefreshOptIn`, `TestOIDCRefreshOptInSmoke`. Note: code-replay revocation misses rotated successors (WR-01, warning; see below). | -| 14 | D-18: PHP throttle buckets and keys; discovery and JWKS unthrottled | ✓ VERIFIED | `buckets.go:44` (30/20/60/120 per minute, PHP key prefixes); `routes.go` (body cap before `throttle:oauth-token`, none on discovery/JWKS); `TestBuckets`, `TestBucketKeysSmoke`, `TestTokenLimitsSmoke`. | -| 15 | D-14 (amended 2026-10-19): the parity harness ships and fails closed; recordings deferred to the WavePath cutover; NOT RUN does not hold the verdict | ✓ VERIFIED | Ten flow files, `rules.yaml`, `deviations.yaml` (11 reasons, all citing a D-number), runbook. This verifier ran the parity tests: they SKIP with "no PHP recordings" by default; under `PHASE194_REQUIRE_PARITY=1` both FAIL (exit 1). No flow holds `response:`, and no `.vars` file is tracked. The gate prints "NOT RUN parity: D-14 unproven (... deferred, see .planning/todos/pending/2026-10-19-record-php-oidc-parity-against-wavepath.md)". The todo exists (g15office govfaqir). PHP method map: 171 of 171 methods mapped (146 ported, 25 adapted, 0 not-applicable), enforced by `TestPHPMethodMap`. | +| 1 | SC-1 / D-16: a decision note settles hand-written wristband vs zitadel/oidc and names the JOSE/JWT dependency before code lands | ✓ VERIFIED | `.planning/notes/2026-10-19-oidc-on-wristband-golang-jwt.md` names golang-jwt/jwt/v5 and the test-only go-oidc and x/oauth2, and rejects zitadel. `git merge-base --is-ancestor 756a97c 659b42a` holds (re-run). Framework `go.mod`/`go.sum` unchanged since 95051d2 (re-checked). The gate's docs stage checks the note's dependency names (PASS). | +| 2 | SC-2: discovery, JWKS, userinfo and id_token work with RS256 or ES256 keys; rotation publishes old and new key together | ✓ VERIFIED | Code as in the flow table. In my `--all` run these passed: `TestRelyingPartyInterop` (RS256 and ES256 subtests), `TestMixedKeyRingSmoke`, `TestES256Ring`, `TestRotateAndPruneSmoke`, `TestRelyingPartyFlow` (rotation within grace), all in the docker stage (41 plugin tests). | +| 3 | SC-3: resume, switch account, consent list and revocation, pairwise and public subjects; a scheduled prune removes expired codes, tokens and requests | ✓ VERIFIED | `Resume`, `SwitchAccount`, `ConsentRequest`/`DecideConsent`; `controllers/authorizations.go`; `oidc_subject.go`; `schedule.go` (`pact.Daily`) → `console/prune.go`. Passed in my run: `TestRevokeAuthorization`, `TestAuthorizationsSmoke`, `TestPairwiseSubjects`, `TestSubjectStability`, `TestPruneExpired`, `TestScheduleSmoke`, `TestLoginHandoffAndSwitchSmoke`. | +| 4 | SC-4 / D-11 / D-19: an existing issuer, its signing keys (original kids) and client secrets import so relying parties keep working; the phase has a security review | ✓ VERIFIED | `console/import_winter.go`, `console/import_keys.go` (reads `keys.json`, keeps kids and retired state, encrypts with `lagoon.NewEncrypted`). `TestImportWinterCutoverSmoke` passed in my run: an old access token reads userinfo, a pre-cutover id_token verifies against the Go JWKS, the old secret authenticates, the next id_token carries the imported kid. `19.4-SECURITY-REVIEW.md` contains T-19.4-SC; `19.4-SECURITY.md` reports `threats_open: 0`. | +| 5 | SC-5: wristband README and docs updated; new code has unit tests delivered in the last plan | ✓ VERIFIED | Docs stage PASS: `TestDocsTree`, `docs:build --check`, 127 files name no consuming application. The plan 04 commits hold tests, the method map and the gate only. | +| 6 | D-01 / D-06: one wristband engine, two `*Server` flavours; the plugin publishes them as one `*Servers`; wristband imports no ORM, no framework module, no application | ✓ VERIFIED | Flavour dispatch in `authorize.go` (key-link pattern `flavourOIDC` found). Plugin `servers.go` calls `NewOIDCServer` + `SetOIDCBackend`; `plugin.go:82` calls `NewServers`. `go list -deps ./modules/wristband` lists no gorm and no summercms module other than wristband itself (re-run). | +| 7 | D-09: existing `Backend`/`Tx` source-compatible; fonoteka compiles and passes with only an explicit Resource; `DefaultOptions().Resource` is empty | ✓ VERIFIED | fonoteka `plugin.go` has `oauthOpts.Resource =`; no fonoteka commit after 687e25a touches `plugin.go` or `oauth_store.go`. The docker stage ran 37 fonoteka OAuth tests (incl. `TestOAuthResourceExplicit`, `TestPhase08Coverage`) and they passed against the merged user plugin checkout. `TestDefaultOptionsResourceEmpty` passed. | +| 8 | D-07 / D-08: kind isolation; MCP refuses openid and oidc-kind clients; OIDC accepts only oidc kind; DCR never yields identity scopes | ✓ VERIFIED | `TestKindIsolation`, `TestKindIsolationSmoke`, `TestMCPRefusesOpenID`, `TestMCPStoreSmoke` passed in my run. | +| 9 | D-10 / D-12: keys in the DB, private PEM `lagoon.Encrypted`, lazy first key, one active key under races, RS256 default, ES256 configurable | ✓ VERIFIED | The migration contains `WHERE active`; `TestKeyRingFirstKeyRaceSmoke`, `TestKeyRingMultiInstanceRace`, `TestKeyRingPreviousAppKey`, `TestKeyRing` passed in the docker stage. | +| 10 | D-03 / D-04: branded server-rendered consent and error pages with sanitised branding and a one-shot token; redirect consent mode with JSON endpoints; anonymous users get the login URL with only `redirect=` | ✓ VERIFIED (human UAT, row 1) | `views/consent.html.tmpl` contains `consent_token`; `TestConsentPageBrandingSmoke`, `TestConsentPageSecurity`, `TestConsentRedirectMode`, `TestLoginHandoffAndSwitchSmoke` passed. A human compared the pages with PHP in a browser (UAT row 1, pass). | +| 11 | D-05: sm-user-plugin changes additive only; claims from its users; cascade on user.Deleting / user.Deactivating | ✓ VERIFIED | The phase commits `c077029..9335e80` remove non-test lines only inside `applyAvatar` and the `bulk("deactivate", ...)` entry, so no export was removed. Against `24b16b1`, no exported declaration is removed either. The merge kept origin's change intact (see "What changed"). Userplugin stage PASS ("since c077029 24b16b1, 12 declarations added"); `TestZeroEditAdditivity`, `TestRequestUser`, `TestEndRequestSession`, `TestAvatarURL`, `TestDeactivatingEvent`, `TestUserDeleteCascade`, `TestClaimBuilder` passed. Listeners at plugin `plugin.go:93-94`. | +| 12 | D-15: random UUIDv4 subjects per (user, sector), public by default, pairwise opt-in with `ErrSectorRequired`; userinfo sub equals id_token sub | ✓ VERIFIED | `TestOIDCSubjectIsRandomSmoke`, `TestPairwiseSubjectSmoke`, `TestPairwiseSubjects` passed. | +| 13 | D-17: refresh off by default; opt-in per client with rotation and lineage revocation | ✓ VERIFIED | `TestRefreshOptIn`, `TestOIDCRefreshOptInSmoke` passed. WR-01 (code replay misses rotated successors) is still open; see Anti-Patterns. | +| 14 | D-18: PHP throttle buckets and keys; discovery and JWKS unthrottled | ✓ VERIFIED | `routes.go` contains `throttle:oauth-token`; `TestBuckets`, `TestBucketKeysSmoke`, `TestTokenLimitsSmoke` passed. | +| 15 | D-14 (amended 2026-10-19): the parity harness ships and fails closed; recordings deferred to the WavePath cutover; NOT RUN does not hold the verdict | ✓ VERIFIED | I ran both parity tests: with no environment variable they `--- SKIP` with "no PHP recordings"; with `PHASE194_REQUIRE_PARITY=1` both `--- FAIL`. The gate's parity stage fails with `PHASE194_REQUIRE_PARITY=1` (exit 1) and prints NOT RUN without it, naming the todo. `TestPHPMethodMap` passed (171/171). The todo and the CONTEXT amendment exist. | -**Score:** 15/15 truths verified (0 present, behavior-unverified) +**Score:** 15/15 truths verified (0 present but behavior-unverified) -All behavior-dependent truths (consent one-shot burn, code burn-then-check and replay revocation, refresh rotation and lineage, key-race single active key, cascade, rotation grace) are exercised by named tests the gate requires and that passed in this verifier's own `--all` run. +Every behavior-dependent truth (consent one-shot burn, code burn-then-check and replay revocation, refresh rotation and lineage, single active key under a race, cascade, rotation grace) is exercised by a named test that the gate requires and that passed in my `--all` run today. ### Deferred Items | # | Item | Addressed In | Evidence | | --- | --- | --- | --- | -| 1 | PHP parity recording and a green `TestPHPParityReplay` / `TestIDTokenShapeParity` | WavePath cutover phase (preliminary task, not yet on the roadmap) | User decision 2026-10-19; CONTEXT D-14 amendment; amended plan 03/04 must-haves (3386cbe); todo `2026-10-19-record-php-oidc-parity-against-wavepath.md`. The user ruled that this does not block the phase. It is not a gap or a human item. | +| 1 | PHP parity recording and a green `TestPHPParityReplay` / `TestIDTokenShapeParity` | WavePath cutover phase (preliminary task, not yet on the roadmap) | User decision 2026-10-19; CONTEXT D-14 amendment; amended plan 03/04 must-haves (3386cbe); todo `2026-10-19-record-php-oidc-parity-against-wavepath.md` (severity major, g15office govfaqir). | ### Required Artifacts | Artifact | Expected | Status | Details | | --- | --- | --- | --- | -| `.planning/notes/2026-10-19-oidc-on-wristband-golang-jwt.md` | SC-1 decision note | ✓ VERIFIED | all dependency names present; precedes code | -| `modules/wristband/oidc_options.go` | NewOIDCServer, SetOIDCBackend, OIDCHost, ConsentView, IssueOIDCClientCredentials | ✓ VERIFIED | used by plugin `servers.go`, `controllers/clients.go` | -| `modules/wristband/oidc_stores.go` | OIDCBackend, OIDCTx, records | ✓ VERIFIED | implemented by plugin `classes/oidc_store.go` | -| `modules/wristband/oidc_keys.go` | KeyStore, JWK, id_token signing, PEM parsing | ✓ VERIFIED | `jwt.MapClaims`; implemented by `classes/keyring.go` | -| `modules/wristband/oidc_authorize.go` / `oidc_consent.go` / `oidc_token.go` / `oidc_userinfo.go` / `oidc_subject.go` / `oidc_redirect.go` / `oidc_discovery.go` | protocol | ✓ VERIFIED | 6,915 lines across the oidc_*.go files; mounted by plugin routes | -| `../sm-oauthserver-plugin/plugin.go`, `servers.go`, `routes.go`, `buckets.go`, `schedule.go` | plugin wiring | ✓ VERIFIED | capability methods present; Requires golem15.user | -| `../sm-oauthserver-plugin/classes/{oidc_store,mcp_store,keyring,host,claims,branding,cascade,reveal}.go` | stores, key ring, pages, claims | ✓ VERIFIED | listeners registered at `plugin.go:93-94` | -| `../sm-oauthserver-plugin/updates/2026101900{01,02,03}_*.go` | raw SQL migrations | ✓ VERIFIED | `WHERE active` partial index present | -| `../sm-oauthserver-plugin/console/{import_winter,import_keys,import_settings,rotate_keys,prune}.go` | import, rotate, prune | ✓ VERIFIED | registered in `Commands()` | -| `../sm-oauthserver-plugin/controllers/{clients,branding,authorizations,consent_api}.go` + YAML | admin and JSON APIs | ✓ VERIFIED | `regenerate_secret` Danger action; SingletonRecord branding | -| `../sm-oauthserver-plugin/views/{consent,error}.html.tmpl` | branded pages | ✓ VERIFIED | embedded and executed by `classes/host.go` | -| `../sm-oauthserver-plugin/interop_test.go` | go-oidc interop | ✓ VERIFIED | RS256 and ES256 subtests; go-oidc only in `_test.go` | -| `../sm-oauthserver-plugin/parity_test.go` + `testdata/parity/**` | fail-closed harness | ✓ VERIFIED | see truth 15 | -| `../sm-oauthserver-plugin/testdata/PHP_METHOD_MAP.md`, `php_methods.txt`, `php_method_map_test.go` | case-by-case map | ✓ VERIFIED | 171/171 | -| `scripts/check-phase19.4.sh` | fail-closed gate | ✓ VERIFIED | `--all` exit 0 on this host | -| `.planning/phases/19.4-.../19.4-SECURITY-REVIEW.md` | ASVS L1 review | ✓ VERIFIED | every threat verified, fixed (test pin) or accepted | -| sm-user-plugin `classes/session_user.go`, `avatar.go`, `events.go`, `controllers/registration.go` | additive exports | ✓ VERIFIED | used by plugin `host.go`, `claims.go`, `cascade.go` | +| `.planning/notes/2026-10-19-oidc-on-wristband-golang-jwt.md` | SC-1 decision note | ✓ VERIFIED | contains `golang-jwt/jwt/v5`; precedes code | +| `modules/wristband/oidc_options.go`, `oidc_stores.go`, `oidc_keys.go`, `oidc_authorize.go`, `oidc_token.go`, `oidc_tracer_test.go` | plan 01 artifacts | ✓ VERIFIED | `verify.artifacts` valid for plan 01; 6,915 lines across `oidc_*.go` | +| `../sm-oauthserver-plugin/plugin.go`, `classes/oidc_store.go`, `classes/keyring.go`, migration `202610190001`, `views/consent.html.tmpl`, `interop_test.go` | plan 02 artifacts | ✓ VERIFIED | each contains its required pattern (checked by hand; the tool rejects paths outside the repo) | +| `../sm-oauthserver-plugin/console/{import_winter,import_keys,rotate_keys,prune}.go`, `controllers/clients.go`, `parity_test.go`, `19.4-SECURITY-REVIEW.md` | plan 03 artifacts | ✓ VERIFIED | patterns present | +| `scripts/check-phase19.4.sh`, `testdata/PHP_METHOD_MAP.md`, `php_method_map_test.go`, `modules/wristband/oidc_token_test.go` | plan 04 artifacts | ✓ VERIFIED | patterns present; gate `--all` exit 0 | +| sm-user-plugin `classes/session_user.go`, `avatar.go`, `events.go` | additive exports | ✓ VERIFIED | used by plugin `host.go`, `claims.go`, `cascade.go` | ### Key Link Verification | From | To | Via | Status | Details | | --- | --- | --- | --- | --- | -| `authorize.go` | `oidc_authorize.go` | flavour dispatch before MCP code | WIRED | `authorize.go:35` | -| `oidc_token.go` | `oidc_keys.go` | `ActiveSigningKey` → `signIDToken` | WIRED | interop verifies the signature | -| `oidc_discovery.go` | KeyStore | `PublishedKeys` | WIRED | `publishedKeys()` | -| fonoteka `plugin.go` | wristband `Options.Resource` | `oauthOpts.Resource =` | WIRED | `plugin.go:157` | -| plugin `servers.go` | `NewOIDCServer`/`SetOIDCBackend` | Boot | WIRED | boot fails on error | -| plugin `classes/host.go` | sm-user `RequestUser` / `EndRequestSession` | OIDCHost | WIRED | handoff and switch smokes | -| plugin `plugin.go` | sm-user `DeletingEvent` / `DeactivatingEvent` | `app.Events.Listen` | WIRED | `plugin.go:93-94`; `TestUserDeleteCascade` | -| plugin `routes.go` | `buckets.go` | `throttle:oauth-*` | WIRED | `TestTokenLimitsSmoke` gets a 429 after 20 attempts | -| plugin `schedule.go` | `console/prune.go` | `pact.Daily()` via `HasSchedule` | WIRED | asserted through the interface | -| plugin `console/import_keys.go` | `models/signing_key.go` | `lagoon.NewEncrypted` | WIRED | `import_keys.go:246` | +| `authorize.go` | `oidc_authorize.go` | flavour dispatch | WIRED | `verify.key-links` | +| `oidc_token.go` | `oidc_keys.go` | `ActiveSigningKey` | WIRED | `verify.key-links`; `oidc_token.go:111,316` | +| `oidc_discovery.go` | `oidc_keys.go` | `PublishedKeys` | WIRED | `verify.key-links` | +| fonoteka `plugin.go` | wristband `Options.Resource` | `oauthOpts.Resource =` | WIRED | grep (outside the repo) | +| plugin `servers.go` | `NewOIDCServer` / `SetOIDCBackend` | Boot through `NewServers` (`plugin.go:82`) | WIRED | grep | +| plugin `classes/host.go` | sm-user `RequestUser` / `EndRequestSession` | OIDCHost | WIRED | grep; handoff and switch smokes pass | +| plugin `classes/cascade.go` | sm-user `DeletingEvent` / `DeactivatingEvent` | `app.Events.Listen` at `plugin.go:93-94` | WIRED | `TestUserDeleteCascade` | +| plugin `routes.go` | `buckets.go` | `throttle:oauth-*` | WIRED | `TestTokenLimitsSmoke` | +| plugin `console/import_keys.go` | `models/signing_key.go` | `lagoon.NewEncrypted` | WIRED | grep | +| plugin `schedule.go` | `console/prune.go` | `pact.Daily()` | WIRED | `TestScheduleSmoke` | | plugin `controllers/clients.go` | `wristband.IssueOIDCClientCredentials` | FormBeforeCreate | WIRED | `TestClientsAdminSmoke` | -| `scripts/check-phase19.4.sh` | `TestPHPMethodMap` | plugin stage | WIRED | passes in the gate | +| `scripts/check-phase19.4.sh` | `TestPHPMethodMap` | plugin stage | WIRED | `verify.key-links`; passed in the gate | +| `php_method_map_test.go` | wristband test sources | `filepath.Join(summercmsRoot(), "modules", "wristband")` (line 83) | WIRED | the literal `modules/wristband` pattern does not match because the path is built with `filepath.Join`; the wiring itself is present | ### Data-Flow Trace (Level 4) | Artifact | Data Variable | Source | Produces Real Data | Status | | --- | --- | --- | --- | --- | -| JWKS / discovery algs | published keys | `golem15_oauthserver_keys` (active OR retired within grace) | yes (Postgres in tests) | ✓ FLOWING | -| id_token claims | identity claims | sm-user `users` + user groups + optional userfriends tables (`classes/claims.go`) | yes | ✓ FLOWING (WR-06: the communities read-failure fallback is not safe inside the Postgres transaction) | -| consent page | client, scopes, branding | `golem15_oauthserver_clients`, auth request, settings row 1 | yes | ✓ FLOWING | +| JWKS / discovery algorithms | published keys | `golem15_oauthserver_keys` (active OR retired within grace) | yes (Postgres in tests) | ✓ FLOWING | +| id_token claims | identity claims | sm-user `users`, user groups, optional userfriends tables (`classes/claims.go`) | yes | ✓ FLOWING (WR-06 caveat) | +| consent page | client, scopes, branding | clients table, auth request, settings row 1 | yes | ✓ FLOWING | | authorizations list | consents joined to clients | `golem15_oauthserver_consents` | yes | ✓ FLOWING | ### Behavioral Spot-Checks | Behavior | Command | Result | Status | | --- | --- | --- | --- | -| Full phase gate across four repositories, Docker included | `bash scripts/check-phase19.4.sh --all` | exit 0. PASS self-test, go (42 wristband + 1 tide), plugin (8), userplugin, fonoteka (3), docs, docker (41 plugin + 3 user plugin + 37 fonoteka OAuth). NOT RUN parity (deferred). | ✓ PASS | -| Parity skips without recordings | `go test -short -run '^(TestPHPParityReplay\|TestIDTokenShapeParity)$' -v .` (plugin) | both `--- SKIP` "no PHP recordings" | ✓ PASS | -| Parity fails closed | same with `PHASE194_REQUIRE_PARITY=1` | both `--- FAIL`, exit 1 | ✓ PASS | -| Decision note precedes code | `git merge-base --is-ancestor 756a97c 659b42a` | ancestor | ✓ PASS | -| wristband purity | `go list -deps ./modules/wristband \| grep -E 'gorm.io\|summercms/modules/'` | no match | ✓ PASS | -| Method map complete | row count vs `php_methods.txt` vs live PHP tree | 171 / 171 / 171 | ✓ PASS | +| Full phase gate across four repositories, Docker included (fresh run today) | `bash scripts/check-phase19.4.sh --all` | exit 0. PASS: self-test; go (42 wristband + 1 tide); plugin (8); userplugin (additive since `c077029 24b16b1`); fonoteka (3); docs (127 files); docker (41 plugin + 3 user plugin + 37 application OAuth). NOT RUN: parity (deferred). | ✓ PASS | +| Additivity check refuses a single base | `PHASE194_USER_BASE=c077029 bash scripts/check-phase19.4.sh userplugin` | lists the two upstream signatures, "refuse ...", exit 1 | ✓ PASS (fails closed) | +| Additivity check refuses an unknown ref | `PHASE194_USER_BASE="c077029 nosuchref" ... userplugin` | "refuse: git diff nosuchref ... failed (exit 128)", exit 1 | ✓ PASS (fails closed) | +| Parity stage fails closed | `PHASE194_REQUIRE_PARITY=1 bash scripts/check-phase19.4.sh parity` | "FAIL parity: no PHP recordings ...", exit 1 | ✓ PASS | +| Parity tests skip, then fail closed | `go test -short -run '^(TestPHPParityReplay\|TestIDTokenShapeParity)$' -v .` (plugin), then with `PHASE194_REQUIRE_PARITY=1` | 2 × SKIP "no PHP recordings", then 2 × FAIL | ✓ PASS | +| Merge kept origin intact | `diff <(git diff 7a6fa0e 24b16b1 -- . ':(exclude)plugin.go') <(git diff 9335e80 4c5d3b8 -- . ':(exclude)plugin.go')` | identical | ✓ PASS | +| No export removed against origin's base | `git diff 24b16b1 -- '*.go' ':(exclude)*_test.go' \| grep -E '^-(func ([A-Z]\|\(...\) [A-Z])\|type [A-Z])'` | no match | ✓ PASS | +| Decision note precedes code; wristband purity | `git merge-base --is-ancestor 756a97c 659b42a`; `go list -deps ./modules/wristband` | ancestor; no foreign module | ✓ PASS | ### Probe Execution -No `scripts/*/tests/probe-*.sh` exists or is declared by the plans; the phase gate script stands in for probes and was run above. Step 7c: no probes to run. +No `scripts/*/tests/probe-*.sh` exists, and no plan declares one. The phase gate script stands in for probes, and I ran it above. Step 7c: no probes to run. ### Requirements Coverage -The phase IDs are local (CONTEXT and plan success criteria); `.planning/REQUIREMENTS.md` maps nothing to Phase 19.4 (ROADMAP "Requirements: TBD"), so there are no orphaned REQUIREMENTS.md IDs. +`.planning/REQUIREMENTS.md` maps nothing to Phase 19.4 (ROADMAP "Requirements: TBD"), so there are no orphaned IDs. The phase-local IDs: | Requirement | Source Plan | Description | Status | Evidence | | --- | --- | --- | --- | --- | | SC-1 | 01, 04 | Decision note before code, names the JOSE dependency | ✓ SATISFIED | truth 1 | | SC-2 | 01, 02, 04 | Discovery, JWKS, userinfo, id_token on RS256/ES256; rotation publishes both | ✓ SATISFIED | truth 2 | -| SC-3 | 01, 02, 03, 04 | Resume, switch account, consent list/revoke, subjects, scheduled prune | ✓ SATISFIED | truth 3 | +| SC-3 | 01-04 | Resume, switch account, consent list/revoke, subjects, scheduled prune | ✓ SATISFIED | truth 3 | | SC-4 | 03, 04 | Import of issuer, keys, secrets; security review | ✓ SATISFIED | truth 4 | | SC-5 | 01, 04 | README and docs; unit tests in the last plan | ✓ SATISFIED | truth 5 | -| D-01 | 01, 02 | Protocol in wristband, storage/UI in the new plugin | ✓ SATISFIED | truth 6 | -| D-02 | 02, 04 | Plugin stores serve MCP and OIDC | ✓ SATISFIED | `classes/mcp_store.go`; `TestMCPStore`, `TestMCPStoreSmoke` | -| D-03 | 02, 03, 04 | Server-rendered branded pages, CSRF, redirect mode | ✓ SATISFIED (visual → human 1) | truth 10 | +| D-01, D-06 | 01, 02 | One engine, two flavours; protocol in wristband, storage/UI in the plugin | ✓ SATISFIED | truth 6 | +| D-02 | 02, 04 | Plugin stores serve MCP and OIDC | ✓ SATISFIED | `TestMCPStore`, `TestMCPStoreSmoke` | +| D-03 | 02-04 | Server-rendered branded pages, CSRF, redirect mode | ✓ SATISFIED | truth 10 (human UAT row 1) | | D-04 | 01, 02 | Login handoff, cookie user, switch account | ✓ SATISFIED | truth 10 | | D-05 | 02, 04 | Requires sm-user-plugin; additive changes only | ✓ SATISFIED | truth 11 | -| D-06 | 01, 02 | Two Servers, one engine | ✓ SATISFIED | truth 6 | -| D-07 | 01, 04 | MCP cannot request openid | ✓ SATISFIED | truth 8 | -| D-08 | 01, 02, 03, 04 | Client kind; each Server accepts only its kind | ✓ SATISFIED (admin SPA → human 2) | truth 8 | +| D-07, D-08 | 01-04 | MCP cannot request openid; each Server accepts only its kind | ✓ SATISFIED | truth 8; admin SPA by human UAT row 2 | | D-09 | 01, 04 | Additive interfaces; fonoteka unchanged | ✓ SATISFIED | truth 7 | | D-10 | 02, 04 | DB key ring, encrypted private PEM, lazy first key | ✓ SATISFIED | truth 9 | -| D-11 | 03, 04 | Import keeps original kids and retired state | ✓ SATISFIED | truth 4 | +| D-11, D-19 | 02-04 | Import keeps kids and retired state; six tables; issuer from config | ✓ SATISFIED | truth 4 | | D-12 | 01, 02, 04 | RS256 default, ES256 configurable, mixed JWKS | ✓ SATISFIED | truth 2 | -| D-13 | 03, 04 | rotate-keys, grace window, opt-in scheduled rotation | ✓ SATISFIED | `console/rotate_keys.go`, `schedule.go`; `TestRotateAndPruneSmoke`, `TestRotateKeysCommand`, `TestScheduleConfig` | -| D-14 | 01, 03, 04 | Parity bar (amended: harness fail-closed, recording deferred) | ✓ SATISFIED (as amended) | truth 15; recording deferred | -| D-15 | 01, 02, 03, 04 | Public default, pairwise opt-in | ✓ SATISFIED | truth 12 | +| D-13 | 03, 04 | rotate-keys, grace window, opt-in scheduled rotation | ✓ SATISFIED | `TestRotateAndPruneSmoke`, `TestRotateKeysCommand`, `TestScheduleConfig` | +| D-14 | 01, 03, 04 | Parity bar (amended: harness fail-closed, recording deferred) | ✓ SATISFIED (as amended) | truth 15 | +| D-15 | 01-04 | Public default, pairwise opt-in | ✓ SATISFIED | truth 12 | | D-16 | 01, 02 | Library decision; real RP interop test | ✓ SATISFIED | truths 1, 2 | -| D-17 | 01, 02, 03, 04 | Opt-in refresh per client | ✓ SATISFIED | truth 13 (WR-01, WR-02 warnings) | +| D-17 | 01-04 | Opt-in refresh per client | ✓ SATISFIED | truth 13 (WR-01, WR-02 warnings) | | D-18 | 02, 04 | PHP throttle buckets and keys | ✓ SATISFIED | truth 14 | -| D-19 | 02, 03, 04 | Import of the six tables; hashes copied as-is; issuer from config | ✓ SATISFIED | truth 4; `servers.go:71-75` | -| Folded todo | 01 | Neutral `DefaultOptions().Resource` | ✓ SATISFIED | truth 7; todo moved to `done/` | +| Folded todo | 01 | Neutral `DefaultOptions().Resource` | ✓ SATISFIED | truth 7 | -### Prohibitions (judgment tier, LLM-judge verdicts, non-authoritative) +### Prohibitions (judgment tier) -Each verdict below is unverified-prohibition: human review recommended (human item 3). +The human confirmed all 11 verdicts in UAT row 3 (pass, 22:23:50Z), after the plugin pushes at 20:35-20:36Z. That human resolution is authoritative. The table below records the current evidence. | # | Plan | Prohibition | Verdict | Evidence | | --- | --- | --- | --- | --- | -| 1 | 01 | wristband imports no ORM, framework module or application; no non-test file, README or docs page names a consuming application | held | `go list -deps` clean; name grep empty; gate docs stage "127 files checked" | -| 2 | 01 | MCP wire bytes unchanged; old tests pass with only explicit-resource edits | held | pre-existing test diffs only swap application literals for neutral ones; `TestPhase08Coverage` and 37 fonoteka OAuth tests pass; MCP encoders untouched (`git diff 95051d2` removes no encoder line) | -| 3 | 01 | Private keys, codes, tokens and secrets never logged or in error strings | held | `TestOIDCLogsCarryNoCredentials`, `TestImportKeyDirErrorsNameNoKey`; security review V7 sweep | -| 4 | 02 | No sm-user-plugin exported signature changes; DeactivateUsers unchanged | held | full removed-line diff since c077029 (truth 11) | -| 5 | 02 | Signing-key model never in cabana; private PEM never leaves the key ring | held | `TestAdminSurfaceHidesCredentials`; no `SigningKey` in `controllers/` or `admin.go` | -| 6 | 02 | Nothing pushed, tagged or deployed; no app pointer bumped | held | plugin remote has no repository yet; user plugin ahead 7 with no remote branch containing b82d85f; summercms.go ahead 31, no tag contains 659b42a; fonoteka pointers modified but uncommitted (another session) | -| 7 | 03 | No private key, plaintext secret, session token or vars file committed | held | `TestParityFixturesHoldNoSecrets`; no `.vars` tracked; secret-pattern grep over testdata clean | +| 1 | 01 | wristband imports no ORM, framework module or application; no non-test file, README or docs page names a consuming application | held | `go list -deps` clean; gate docs stage "127 files checked, no consuming application named" | +| 2 | 01 | MCP wire bytes unchanged; old tests pass with only explicit-resource edits | held | `TestPhase08Coverage` and 37 fonoteka OAuth tests pass; wristband MCP code unchanged since the previous report | +| 3 | 01 | Private keys, codes, tokens and secrets never logged or in error strings | held | `TestOIDCLogsCarryNoCredentials`, `TestImportKeyDirErrorsNameNoKey` pass | +| 4 | 02 | No sm-user-plugin exported signature changes; DeactivateUsers unchanged | held | The phase commits remove no export. The two signature changes visible against `c077029` come from origin's pre-phase `24b16b1`, which the merge carried. They are not phase edits. | +| 5 | 02 | Signing-key model never in cabana; private PEM never leaves the key ring | held | `TestAdminSurfaceHidesCredentials` passes | +| 6 | 02 | Nothing pushed, tagged or deployed during the plan; no app pointer bumped | held for plan execution | The first push of either plugin repo came at 20:35Z, after plan 04 ended (19:46Z). fonoteka's committed `plugins/golem15/user` pointer is still `cf5c6b9` (the working-tree pointer change is uncommitted). No tag contains a phase commit. | +| 7 | 03 | No private key, plaintext secret, session token or vars file committed | held | `TestParityFixturesHoldNoSecrets` passes | | 8 | 03 | Signing-key model never in a cabana controller, form or list | held | same as 5 | -| 9 | 03 | Nothing pushed, tagged or deployed | held | same as 6 | -| 10 | 04 | No production behaviour change in plan 04 except defect fixes | held | plan 04 commits touch only `_test.go`, testdata, the parity runbook text and the gate | -| 11 | 04 | Nothing pushed or tagged; release waits for the user | held | same as 6 | +| 9 | 03 | Nothing pushed, tagged or deployed | held for plan execution | same as 6 | +| 10 | 04 | No production behaviour change in plan 04 except defect fixes | held | plan 04 and the post-UAT commits (`d9f7bfa`, `36c8c63`) touch only tests, testdata and the gate | +| 11 | 04 | Nothing pushed or tagged; release waits for the user's go-ahead after verification | held | The pushes came after the previous verification. The orchestrator reports the user approved them during UAT, and the human passed row 3 after they happened. No planning file records that approval in writing (see Info below). No tag, no deploy. | ### Anti-Patterns Found | File | Line | Pattern | Severity | Impact | | --- | --- | --- | --- | --- | -| (all phase files in three repos) | n/a | TBD / FIXME / XXX / TODO / placeholder | none found | no debt markers | -| `sm-oauthserver-plugin/classes/oidc_store.go` | 301-312 | WR-01: code replay leaves rotated refresh successors alive | ⚠️ Warning | only with `refresh.enabled: true` (default off, Go-only) | -| `sm-oauthserver-plugin/classes/host.go`, `lang/*` | 271-285 | WR-02: consent page never lists `offline_access` | ⚠️ Warning | only with refresh enabled and an OfflineAccess client | +| phase files in summercms.go, sm-oauthserver-plugin, sm-user-plugin `plugin.go` | n/a | TBD / FIXME / XXX / TODO / HACK / placeholder | none found | no debt markers (re-scanned, including the post-UAT gate and zero-edit changes) | +| `sm-oauthserver-plugin/classes/oidc_store.go` | 301-312 | WR-01: code replay leaves rotated refresh successors alive | ⚠️ Warning | only with `refresh.enabled: true` (default off) | +| `sm-oauthserver-plugin/classes/host.go`, `lang/*` | 271-285 | WR-02: consent page never lists `offline_access` | ⚠️ Warning | only with refresh enabled | | `sm-oauthserver-plugin/console/import_winter.go` | 216-275 | WR-03: `--force` re-import leaves refresh tokens and wipes MCP clients | ⚠️ Warning | the first import into empty tables (the cutover path) is unaffected | -| `sm-oauthserver-plugin/controllers/clients.go` | 330-352 | WR-04: imported clients with legacy redirect URIs cannot be saved in the admin | ⚠️ Warning | authorize keeps working; admin edit blocked | -| `modules/wristband/oidc_subject.go`, plugin `controllers/clients.go` | 160-188 / 366-384 | WR-05: editing a pairwise client's sector silently changes every sub | ⚠️ Warning | pairwise is Go-only and opt-in; admin action | -| `sm-oauthserver-plugin/classes/claims.go` | 181-190 | WR-06: communities read failure aborts the Postgres transaction (500 instead of `[]`) | ⚠️ Warning | only on a transient failure of the optional read (confirmed in code: it runs on the transaction handle with no savepoint) | -| `scripts/check-phase19.4.sh` | 436-438 | IN-09: additive check greps only func/type removals | ℹ️ Info | the verifier's full diff found no removals of any kind | +| `sm-oauthserver-plugin/controllers/clients.go` | 330-352 | WR-04: imported clients with legacy redirect URIs cannot be saved in the admin | ⚠️ Warning | authorize keeps working | +| `modules/wristband/oidc_subject.go`, plugin `controllers/clients.go` | 160-188 / 366-384 | WR-05: editing a pairwise client's sector silently changes every sub | ⚠️ Warning | pairwise is opt-in | +| `sm-oauthserver-plugin/classes/claims.go` | 181-190 | WR-06: a communities read failure aborts the Postgres transaction (500 instead of `[]`) | ⚠️ Warning | only on a transient failure of the optional read | +| `scripts/check-phase19.4.sh`, `zero_edit_test.go` | userplugin stage | The intersection over two bases would miss an export only origin's side had that a merge dropped | ℹ️ Info | No such removal exists today (direct diff against `24b16b1` is clean). Applies only to future merges. | +| (process) | n/a | The user's approval of the plugin pushes is not recorded in a planning file | ℹ️ Info | Evidence is the orchestrator's report plus the UAT row 3 pass after the push timestamps. A one-line note in STATE or the UAT would make it auditable. | -**Do the review warnings break a success criterion?** No. WR-01, WR-02 and WR-05 are in Go-only, default-off or opt-in features (refresh per D-17, pairwise per D-15) that the PHP relying parties do not use. The "switch without noticing" contract and SC-2/SC-3 hold on the default path. WR-03 and WR-04 affect a forced re-import and admin editing of legacy clients, not the SC-4 cutover. A first import into empty tables keeps relying parties working (`TestImportWinterCutoverSmoke`). WR-06 needs a transient failure of an optional query. All six stay open in `19.4-REVIEW-DISPOSITION.md`. They should be fixed before an application enables refresh or pairwise subjects, but they are not goal failures. WR-01 is the most security-relevant: an attacker holding an intercepted code who refreshed once keeps a 30-day lineage. Fix it before anyone turns `refresh.enabled` on. +The six review warnings are still `open` in `19.4-REVIEW-DISPOSITION.md`, and no pending todo tracks them. None breaks a success criterion. WR-01, WR-02 and WR-05 sit in opt-in, Go-only features (refresh per D-17, pairwise per D-15) that the PHP relying parties do not use. WR-03 and WR-04 affect a forced re-import and admin editing of legacy clients, not the first-import cutover that `TestImportWinterCutoverSmoke` proves. WR-06 needs a transient failure. WR-01 is the most security-relevant: fix it before any application turns `refresh.enabled` on. Capturing WR-01..06 as todos is recommended so they are not lost when the phase closes. -Pre-existing and out of scope (`deferred-items.md`): fonoteka `TestPhase09SecurityRoutes` allowlist misses cabana routes from other phases, and two tide test files are not gofmt-clean. Neither involves this phase's code. +Pre-existing and out of scope (`deferred-items.md`): fonoteka `TestPhase09SecurityRoutes` (allowlist misses cabana routes from other phases) and two tide test files that are not gofmt-clean. -### Human Verification Required +### Human Verification -### 1. Consent and error pages against PHP +All earlier human items were settled by recorded UAT (Step 8b). `verification.uat-evidence` reports one covered file changed after the UAT, `scripts/check-phase19.4.sh` (d9f7bfa). Outside the repo, `zero_edit_test.go` and `PHP_METHOD_MAP.md` changed (36c8c63). None of these bears on what the rows exercised: -**Test:** With the plugin running locally, set a site name, a logo and an accent colour in row 1 of `golem15_oauthserver_settings`, open an authorize URL for a test client and the inactive-client error page, and compare both side by side with PHP's `consent.htm` / `error.htm` in a browser. -**Expected:** Same structure and copy, branding applied, error heading in the fixed danger colour. -**Why human:** Tests prove branding reaches the HTML safely; none judges visual likeness. +- **Row 1 (consent and error pages vs PHP): verified by human UAT.** The changed files are gate and test code. No template, host, branding or lang file changed after the UAT. +- **Row 2 (Clients and Branding admin screens): verified by human UAT.** No controller, YAML, partial or `admin/` file changed after the UAT. +- **Row 3 (11 judgment-tier prohibitions): verified by human UAT.** The user plugin merge and both pushes happened before the UAT pass, so the human judged the current state. The later change only widens the gate's baseline to the second pre-phase commit. I re-derived the D-05 evidence directly from the phase commits (unchanged) and against `24b16b1`, and it holds. -### 2. Clients and Branding admin screens in the SPA - -**Test:** Open both screens in the embedded admin SPA: create a client, check the one-time secret reveal, the scopes taglist, the authorized-users table, the regenerate danger button, and the Settings link to Branding with a logo upload. -**Expected:** Both render usably; the secret appears once. -**Why human:** The admin smokes drive the JSON API only. - -### 3. Judgment-tier prohibitions - -**Test:** Review the 11 LLM-judge verdicts in the Prohibitions table. -**Expected:** Confirm each held. -**Why human:** Verifier verdicts on judgment-tier prohibitions are non-authoritative. - -Not listed on purpose: the PHP parity recording. The user deferred it to the WavePath cutover (D-14 amendment), and it does not hold this verdict. +No human verification is outstanding. ### Gaps Summary -No gaps. Every ROADMAP success criterion, every CONTEXT decision D-01..D-19 and the folded todo is implemented, wired and pinned by named tests. Those tests pass in this verifier's own run of `scripts/check-phase19.4.sh --all` on a Docker host (all stages PASS; parity NOT RUN as amended). The status is `human_needed` only because of two visual checks (D-03/D-08) and the review of judgment-tier prohibitions. The six open review warnings are real defects in opt-in or admin-edge paths. None breaks a success criterion, and WR-01 should be fixed before refresh is enabled anywhere. +No gaps. Every ROADMAP success criterion, every CONTEXT decision D-01..D-19 and the folded todo is implemented, wired and pinned by named tests. Those tests passed in a fresh `scripts/check-phase19.4.sh --all` run on a Docker host today (all stages PASS, parity NOT RUN as amended). The changes since the previous report (user plugin merge, additivity-check fix, pushes) leave every truth intact. The merge kept origin's change without loss, the phase's own commits stay additive, and the gate still fails closed on a single base or an unknown ref. The three human items from the previous report passed in UAT on unchanged code, so the status moves from `human_needed` to `passed`. PHP parity recording stays deferred to the WavePath cutover (todo, severity major). WR-01..06 are open warnings in opt-in or admin-edge paths, and WR-01 must be fixed before refresh is enabled anywhere. --- -_Verified: 2026-10-19T21:10:00Z_ +_Verified: 2026-10-19T22:46:04Z_ _Verifier: Claude (msd-verifier)_