diff --git a/.planning/phases/19.4-oidc-provider-on-wristband/19.4-03-PLAN.md b/.planning/phases/19.4-oidc-provider-on-wristband/19.4-03-PLAN.md index 85ac2bdd..49da944c 100644 --- a/.planning/phases/19.4-oidc-provider-on-wristband/19.4-03-PLAN.md +++ b/.planning/phases/19.4-oidc-provider-on-wristband/19.4-03-PLAN.md @@ -70,8 +70,8 @@ must_haves: - "D-13: oauthserver:rotate-keys promotes a new key (algorithm from key_algorithm) and retires the previous one, which stays in the JWKS for rotation_grace_hours (default 24); --prune also removes keys past the grace; the output lines match PHP and never contain key material; a scheduled rotation runs only when rotation_interval is set (off by default)." - "SC-3: oauthserver:prune runs daily through the framework scheduler and deletes codes, access tokens (expired or revoked), auth requests, refresh tokens and MCP codes older than one day past expiry, and keys past the grace window, printing one count per kind." - "D-03, D-08: administrators manage OIDC clients in cabana with stock field types: create generates an oas_ client id and an oas_cs_ secret that is shown exactly once, regenerate-secret is a danger action that invalidates the old secret, the secret hash never appears in a list, form or JSON, redirect URIs are edited one per line, pairwise clients need a resolvable sector, and the client form lists who authorized the client with each user's sub; branding is a singleton form linked from Settings with a logo upload." - - statement: "D-14: tide fixtures recorded from the PHP plugin (discovery, JWKS, authorize errors and redirects, login handoff, resume, decide, deny, silent consent, token success and errors, userinfo success and 401, authorizations list, revoke and 404) replay against the Go plugin with no difference outside a documented deviations list; the PHP id_token's header members and claim set equal the Go id_token's. The only evidence is TestPHPParityReplay and TestIDTokenShapeParity passing with PHASE194_REQUIRE_PARITY=1 over committed recordings; a skip, the gate's NOT RUN parity line or the skip-parity reply is not evidence, so without recordings this truth stays unproven and the phase verdict is human_needed (or gaps_found), never passed." - verification: backstop + - statement: "D-14 (amended 2026-10-19 by the user): the parity harness ships and fails closed: ten spec flows (discovery, JWKS, authorize errors and redirects, login handoff, resume, decide, deny, silent consent, token success and errors, userinfo success and 401, authorizations list, revoke and 404), capture rules, a deviations list whose entries cite a D-number, TestPHPParityReplay and TestIDTokenShapeParity, and a runbook. Without recordings the tests skip with \"no PHP recordings\" and fail with PHASE194_REQUIRE_PARITY=1. Recording the PHP plugin and passing the replay is deferred to the WavePath cutover phase as a preliminary task (`.planning/todos/pending/2026-10-19-record-php-oidc-parity-against-wavepath.md` (g15office govfaqir)); with recordings committed, the replay must pass with only decision-backed deviations. A skip-parity reply does not hold the phase verdict at human_needed." + verification: automated - "SC-4: a security review walks ASVS L1 across wristband's OIDC flavour, the plugin and the sm-user-plugin additions, verifies every T-19.4 mitigation in code and in a test, fixes any gap, and records the result in 19.4-SECURITY-REVIEW.md." artifacts: - path: "../sm-oauthserver-plugin/console/import_winter.go" @@ -136,7 +136,7 @@ This plan's slice (plan 3 of 4, wave 3): the cutover and operations half. An ope - Scheduled rotation (CONTEXT discretion): `rotation_interval` empty means off; the README suggests 2160h (90 days) when switched on; an unparseable or non-positive value fails boot naming the key. - Parity: the session JWT on both sides comes from a variable (`session_token`): recorded from a human's WavePath login, minted by the Go harness on replay; JWKS `n`, `e`, `x`, `y` and `kid` values, id_token and access token strings, codes and request ids are compared by shape or captured as variables; HTML bodies of rendered pages are not compared (D-14), their status and headers are. - D-11 is a one-way door already decided by the user in CONTEXT; it is rated here and not re-gated. -- Parity skip (D-14): a "skip-parity" reply at Task 5 is an accepted way to finish this plan, never a way to prove D-14. The D-14 truth is a backstop truth whose only evidence is the replay passing with PHASE194_REQUIRE_PARITY=1; after a skip the phase verdict stays human_needed (or gaps_found) until the recording is made. +- Parity skip (D-14, amended 2026-10-19 by the user): a "skip-parity" reply at Task 5 is an accepted way to finish this plan and the phase. The PHP recording and a green replay with PHASE194_REQUIRE_PARITY=1 move to the WavePath cutover phase as a preliminary task (`.planning/todos/pending/2026-10-19-record-php-oidc-parity-against-wavepath.md` (g15office govfaqir)); WavePath's OIDC provider does not move to the Go plugin until then. In this phase the D-14 truth is the fail-closed harness, so a skip does not hold the phase verdict at human_needed. - Context budget (estimate 260k, 7 tasks; size accepted with the four-plan decision): the Task 5 checkpoint splits execution in two, since the executor resumes in a fresh continuation agent. Tasks 1-4 run in the first context; Tasks 6-7 run in the continuation agent. Task 7 (security review) reads everything it needs from disk (the three plans' threat models, the plan 01 and 02 SUMMARYs, the code), so the executor may also give it a fresh agent of its own. ## Artifacts this phase produces (this plan) @@ -302,7 +302,7 @@ Tests commands_smoke_test.go (Postgres, skip under -short): `TestRotateAndPruneS 5. Write base_url, client_id, client_secret and session_token into ~/.config/summercms/parity-oauthserver.vars (chmod 600). Do not paste the secret or the token into the chat. Claude checks the vars file mode, that discovery answers 200 at base_url, and that the export file holds lines for the parity client. - Reply "ready" with the vars file path and the parity user's id, name and email, or "skip-parity" if WavePath cannot be started now (Task 6 then runs its explicit skip check, commits nothing new and records D-14 parity as NOT RUN with a human verification item; D-14 stays unproven and the phase verdict stays human_needed until the recording is made, never passed). + Reply "ready" with the vars file path and the parity user's id, name and email, or "skip-parity" if WavePath cannot be started now (Task 6 then runs its explicit skip check, commits nothing new and records D-14 parity as NOT RUN with a human verification item; the recording is deferred to the WavePath cutover phase per `.planning/todos/pending/2026-10-19-record-php-oidc-parity-against-wavepath.md` (g15office govfaqir), and the phase verdict is not held at human_needed). @@ -316,10 +316,10 @@ Tests commands_smoke_test.go (Postgres, skip under -short): `TestRotateAndPruneS - With "ready": all ten flow files exist and every flow file contains a recorded response (`grep -L 'response:' ../sm-oauthserver-plugin/testdata/parity/flows/*.yaml` prints nothing), and every entry in deviations.yaml carries a `reason:` that cites a D-number (`grep -c 'reason:' deviations.yaml` equals `grep -c 'reason:.*D-[0-9]' deviations.yaml`). - - With "skip-parity": `grep -l 'response:' ../sm-oauthserver-plugin/testdata/parity/flows/*.yaml` prints nothing, and 19.4-03-SUMMARY.md contains `D-14 parity: NOT RUN (skip-parity)` and a `## Human verification pending` section naming TestPHPParityReplay (checked when the SUMMARY is written). + - With "skip-parity": `grep -l 'response:' ../sm-oauthserver-plugin/testdata/parity/flows/*.yaml` prints nothing, and 19.4-03-SUMMARY.md contains `D-14 parity: NOT RUN (skip-parity)` and a `## Deferred to the WavePath cutover` section naming TestPHPParityReplay and the todo file 2026-10-19-record-php-oidc-parity-against-wavepath.md (checked when the SUMMARY is written). - Either reply: `F=$(git -C ../sm-oauthserver-plugin ls-files testdata) && printf '%s\n' "$F" | grep -c '\.vars$'` prints 0. - With "ready": the relying-party-visible contract of the PHP plugin is recorded and the Go plugin reproduces it, with every difference explained by a decision. With "skip-parity": nothing is recorded, the skip is checked explicitly, and D-14 is carried to verification as an open human item that keeps the verdict at human_needed. + With "ready": the relying-party-visible contract of the PHP plugin is recorded and the Go plugin reproduces it, with every difference explained by a decision. With "skip-parity": nothing is recorded, the skip is checked explicitly, the harness is proven fail-closed, and the PHP recording is carried to the WavePath cutover phase through the captured todo; the phase verdict is not held at human_needed. @@ -370,16 +370,16 @@ Tests commands_smoke_test.go (Postgres, skip under -short): `TestRotateAndPruneS - Task 1: TestImportWinterCutoverSmoke (old token, old id_token, old secret, original kids). - Task 2: TestClientsAdminSmoke and TestBrandingAdminSmoke. - Task 3: TestRotateAndPruneSmoke and TestScheduleSmoke. -- Task 4 to 6: the parity suite prepared, the PHP plugin recorded after the human starts it, the replay and id_token shape tests green, no secrets under testdata. After a skip-parity reply only the explicit skip branch passes; D-14 stays unproven and goes to verification as a human item. +- Task 4 to 6: the parity suite prepared, the PHP plugin recorded after the human starts it, the replay and id_token shape tests green, no secrets under testdata. After a skip-parity reply only the explicit skip branch passes; the PHP recording is deferred to the WavePath cutover phase (captured todo), not a human item of this phase. - Task 7: the security review document covers every threat; suites green after fixes. - SC-3: the daily prune is scheduled and removes expired codes, tokens, requests and keys. - SC-4: an existing issuer, its keys (original kids) and client secrets import so relying parties keep working; the phase has a security review with no open threat. -- D-14: the Go plugin replays the PHP recordings with only decision-backed deviations. Unmet after skip-parity: the phase verdict is then human_needed (or gaps_found), not passed. +- D-14 (amended 2026-10-19 by the user): the parity harness ships fail-closed; with recordings, the Go plugin replays them with only decision-backed deviations. After skip-parity the recording is deferred to the WavePath cutover phase (`.planning/todos/pending/2026-10-19-record-php-oidc-parity-against-wavepath.md` (g15office govfaqir)) and the phase can pass. -Create `.planning/phases/19.4-oidc-provider-on-wristband/19.4-03-SUMMARY.md` when done, with the commits per repository, the human's Task 5 reply (without secrets), the parity result or `D-14 parity: NOT RUN (skip-parity)` with the `## Human verification pending` section from Task 6, the review's fixed and accepted items, which Docker-backed tests ran, and any deviation from this plan's names. +Create `.planning/phases/19.4-oidc-provider-on-wristband/19.4-03-SUMMARY.md` when done, with the commits per repository, the human's Task 5 reply (without secrets), the parity result or `D-14 parity: NOT RUN (skip-parity)` with the `## Deferred to the WavePath cutover` section from Task 6, the review's fixed and accepted items, which Docker-backed tests ran, and any deviation from this plan's names. diff --git a/.planning/phases/19.4-oidc-provider-on-wristband/19.4-04-PLAN.md b/.planning/phases/19.4-oidc-provider-on-wristband/19.4-04-PLAN.md index ff413520..e320c184 100644 --- a/.planning/phases/19.4-oidc-provider-on-wristband/19.4-04-PLAN.md +++ b/.planning/phases/19.4-oidc-provider-on-wristband/19.4-04-PLAN.md @@ -55,7 +55,7 @@ must_haves: - "D-14: every one of the PHP plugin's test methods (25 files under plugins/golem15/oauthserver/tests) appears in testdata/PHP_METHOD_MAP.md with the Go test that ports it (top-level name or name/subtest), or not-applicable with a reason; TestPHPMethodMap fails on a missing, unknown or extra method and on a Go test name that does not exist in either repository." - "Go-only behaviour has its own tests: ES256 and mixed key rings, pairwise subjects, kind isolation in both flavours, opt-in refresh, PHP query encoding and repeated parameters, PEM parsing, the database key ring race, decryption with app.previous_keys, MCP stores, the consent redirect mode, the Winter import with key_dir errors, rotation, prune and schedule." - "D-07, D-09: wristband's existing MCP tests, fonoteka's OAuth tests and the sm-user-plugin suite pass unchanged; an MCP server refuses openid and never emits an id_token." - - "scripts/check-phase19.4.sh fails closed: a required test that fails, skips or does not run fails its stage; --self-test proves the detectors on planted output; --all runs every stage and prints NOT RUN for the docker stage without a daemon and for the parity stage without recordings, failing instead when PHASE194_REQUIRE_DOCKER=1 or PHASE194_REQUIRE_PARITY=1; the parity NOT RUN line states that D-14 parity is unproven, and a gate that exits 0 with parity NOT RUN is never evidence for D-14 (the phase verdict stays human_needed)." + - "scripts/check-phase19.4.sh fails closed: a required test that fails, skips or does not run fails its stage; --self-test proves the detectors on planted output; --all runs every stage and prints NOT RUN for the docker stage without a daemon and for the parity stage without recordings, failing instead when PHASE194_REQUIRE_DOCKER=1 or PHASE194_REQUIRE_PARITY=1; the parity NOT RUN line states that D-14 parity is unproven, and points to the WavePath cutover todo that carries the PHP recording (D-14 amended 2026-10-19: parity NOT RUN does not hold the phase verdict at human_needed)." - "SC-1, SC-5: the gate's docs stage checks the decision note's dependency names, TestDocsTree, summer docs:build --check, and that no wristband source, docs page or plugin file names a consuming application; its userplugin stage proves sm-user-plugin removed no exported function or type since the phase began (D-05)." artifacts: - path: "scripts/check-phase19.4.sh" @@ -99,7 +99,7 @@ This plan's slice (plan 4 of 4, wave 4, the project's "unit tests are the last p - One Go test function per PHP test file, with one subtest per PHP method (subtest names are the PHP method names without the `test_` prefix), so the map stays one line per PHP method and the gate requires about thirty top-level names instead of 166. - Protocol-level PHP cases (PKCE, redirect matching, token issuance, code lifecycle, replay, consent CSRF) run in wristband over memory fakes; cases that need the database, the pages, the admin, the user plugin or config run in the plugin on Postgres. The map says where. - `test_private_key_file_is_not_world_readable` becomes "the private PEM is encrypted at rest and never serialised"; `test_sub_is_public_not_pairwise` becomes "public by default, pairwise only on opt-in"; `test_shared_plugins_are_unedited` becomes the gate's additive check on sm-user-plugin; `test_the_gdpr_scheduled_deletion_path_cascades` maps to the Go user plugin's deletion path if one exists, else not-applicable with that reason; `test_command_exits_zero_and_prints_four_counts_on_an_empty_database` asserts PHP's four lines first and the two Go lines after. -- The parity stage of the gate counts as NOT RUN (not failed) when plan 03 recorded no PHP fixtures, unless PHASE194_REQUIRE_PARITY=1, so this plan can finish after a skip-parity reply. NOT RUN only lets the gate exit 0; it does not prove D-14. Plan 03's D-14 truth is a backstop truth whose only evidence is the replay passing with PHASE194_REQUIRE_PARITY=1, so with parity NOT RUN the phase verdict is human_needed (or gaps_found), never passed. The open item stays visible in the gate output and in this plan's SUMMARY as a human verification item. +- The parity stage of the gate counts as NOT RUN (not failed) when plan 03 recorded no PHP fixtures, unless PHASE194_REQUIRE_PARITY=1, so this plan can finish after a skip-parity reply. NOT RUN only lets the gate exit 0; it does not prove D-14. By the user's amendment of D-14 (2026-10-19), the PHP recording is deferred to the WavePath cutover phase (`.planning/todos/pending/2026-10-19-record-php-oidc-parity-against-wavepath.md` (g15office govfaqir)), so parity NOT RUN does not hold the phase verdict at human_needed. The deferral stays visible in the gate output and in this plan's SUMMARY. ## Artifacts this phase produces (this plan) @@ -256,7 +256,7 @@ Repos: summercms.go (wristband tests, gate, VALIDATION.md), ../sm-oauthserver-pl - SC-5: unit tests for all new code delivered in the phase's last plan. - D-14: the PHP suite mapped case by case with at most three not-applicable rows, each explained. - D-07, D-09: MCP behaviour, fonoteka and sm-user-plugin unchanged and green. -- The phase gate is green with `--all`; docker and parity either ran or are reported NOT RUN. Parity NOT RUN leaves D-14 unproven: the phase verdict is then human_needed, not passed. +- The phase gate is green with `--all`; docker and parity either ran or are reported NOT RUN. Parity NOT RUN is the deferred WavePath-cutover item (D-14 amended 2026-10-19) and does not block a passed verdict. diff --git a/.planning/phases/19.4-oidc-provider-on-wristband/19.4-CONTEXT.md b/.planning/phases/19.4-oidc-provider-on-wristband/19.4-CONTEXT.md index 8e53ba76..dbc6b240 100644 --- a/.planning/phases/19.4-oidc-provider-on-wristband/19.4-CONTEXT.md +++ b/.planning/phases/19.4-oidc-provider-on-wristband/19.4-CONTEXT.md @@ -48,6 +48,7 @@ Out of scope: ### Parity bar - **D-14:** Parity means the **relying-party-visible contract**, recorded from PHP with `tide` and replayed against Go: the discovery document, JWKS shape, token and userinfo JSON, error redirects and error bodies, the `id_token` header (`alg`, `kid`) and claim set (including `nonce`, `at_hash`, `auth_time`/`azp` if PHP emits them), the authorize/resume/decide redirects, and the authorizations list/revoke JSON. The PHP test suite (`plugins/golem15/oauthserver/tests/`) is mapped case by case. Consent and error HTML must work the same and honour branding, but need not be byte-identical. + - *Amended 2026-10-19 by the user:* the PHP recording was skipped at 19.4-03 Task 5 (`skip-parity`). Phase 19.4 passes with the harness shipped and fail-closed; recording WavePath and a green replay become a preliminary task of the WavePath cutover phase (`.planning/todos/pending/2026-10-19-record-php-oidc-parity-against-wavepath.md` (g15office govfaqir)). - **D-15:** Subjects: **public by default**. Imported PHP subs are random UUIDv4 values that never rotate, one per user, allocated lazily on first authorize and hard-deleted with the user (PHP D-98-01..04). A client can opt into **`subject_type=pairwise`** (sector identifier derived from its redirect host, or a configured sector identifier URI). Discovery advertises `public` and `pairwise`. This goes beyond PHP (D-98-02 chose public only) because the roadmap asks for it. - **D-16:** The library question is **left to research**. The decision note (SC-1) compares extending the hand-written wristband (plus a JOSE/JWK library for RS256/ES256 and JWKS) against putting zitadel/oidc v3 behind it. Whichever is chosen must keep D-06 (one engine for MCP and OIDC) and D-09 (additive interfaces, fonoteka unchanged). An interop test with a real RP library (the equivalent of PHP's `JumbojettInteropTest`) is required either way, and the RP library may be test-only. - **D-17:** Refresh tokens for OIDC are **opt-in per client**. An OIDC client allowed `offline_access` gets wristband's existing rotating refresh tokens with lineage revocation. Other clients get only the code grant, as in PHP (D-98-14). Imported PHP clients default to no `offline_access`. diff --git a/.planning/phases/19.4-oidc-provider-on-wristband/19.4-VALIDATION.md b/.planning/phases/19.4-oidc-provider-on-wristband/19.4-VALIDATION.md index dee5d409..282d1ec1 100644 --- a/.planning/phases/19.4-oidc-provider-on-wristband/19.4-VALIDATION.md +++ b/.planning/phases/19.4-oidc-provider-on-wristband/19.4-VALIDATION.md @@ -56,7 +56,7 @@ Filled in by the planner per task; seed rows from RESEARCH.md "Phase Requirement | 19.4-03-03 | 03 | 3 | SC-3, D-13 | T-19.4-30, T-19.4-33 | grace window, daily prune, opt-in rotation | Postgres smoke | `go -C ../sm-oauthserver-plugin test -count=1 -run '^(TestRotateAndPruneSmoke\|TestScheduleSmoke)$' ./...` | ❌ | ⬜ pending | | 19.4-03-04 | 03 | 3 | D-14 | T-19.4-31 | parity suite prepared, replay skips without recordings | setup | `go -C ../sm-oauthserver-plugin test -count=1 ./...` | ❌ | ⬜ pending | | 19.4-03-05 | 03 | 3 | D-14 | T-19.4-31 | human starts WavePath; vars file mode 0600 outside the repos | checkpoint | — (human action) | — | ⬜ pending | -| 19.4-03-06 | 03 | 3 | D-14 | T-19.4-31 | Go replays PHP recordings; no secrets under testdata | parity | `PHASE194_REQUIRE_PARITY=1 go -C ../sm-oauthserver-plugin test -count=1 -run '^(TestPHPParityReplay\|TestIDTokenShapeParity)$' ./...` | ❌ | ⬜ pending | +| 19.4-03-06 | 03 | 3 | D-14 | T-19.4-31 | Go replays PHP recordings; no secrets under testdata | parity | `PHASE194_REQUIRE_PARITY=1 go -C ../sm-oauthserver-plugin test -count=1 -run '^(TestPHPParityReplay\|TestIDTokenShapeParity)$' ./...` | ❌ | ⏭ deferred to the WavePath cutover (D-14 amended 2026-10-19; todo 2026-10-19-record-php-oidc-parity-against-wavepath) | | 19.4-03-07 | 03 | 3 | SC-4 | all T-19.4 | ASVS L1 review, every threat verified, fixed or accepted | review | `test -s .planning/phases/19.4-oidc-provider-on-wristband/19.4-SECURITY-REVIEW.md` | ❌ | ⬜ pending | | 19.4-04-01 | 04 | 4 | SC-5 | T-19.4-35 | gate fails closed on skip, fail, missing name | gate | `bash scripts/check-phase19.4.sh --self-test` | ❌ W0 | ⬜ pending | | 19.4-04-02 | 04 | 4 | SC-5, D-07, D-08, D-12, D-15, D-17 | T-19.4-01..14 | wristband PHP protocol cases and Go-only cases | unit | `bash scripts/check-phase19.4.sh go` | ❌ | ⬜ pending |