From 5a1672f0282028f1c67e65f5b91521bb4bfb1b70 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 15 Jul 2026 10:03:36 +0100 Subject: [PATCH 01/19] Start AUTH-07 authorization kernel chunk --- .agent-loop/LOOP_STATE.md | 18 +++++++++------ .agent-loop/REVIEW_LOG.md | 9 ++++++++ .agent-loop/WORK_QUEUE.md | 10 +++++---- .../CHUNK_MAP.md | 10 +++++---- .../STATUS.md | 22 ++++++++++++------- 5 files changed, 46 insertions(+), 23 deletions(-) diff --git a/.agent-loop/LOOP_STATE.md b/.agent-loop/LOOP_STATE.md index 1ebf0df06..e0e49baee 100644 --- a/.agent-loop/LOOP_STATE.md +++ b/.agent-loop/LOOP_STATE.md @@ -5,16 +5,19 @@ - This authored file is reviewed planning/history context, not canonical live post-merge state. Canonical state is the signed schema-v2 output on `automation/loop-memory`. -- Active initiative: none recorded here +- Active initiative: `WS-AUTH-001` - Workstream Authorization Service - Active planning chunk: none -- Active implementation chunk: none +- Active implementation chunk: `WS-AUTH-001-07` - Authorization Kernel And + Permission Registry +- Current branch: `codex/ws-auth-001-07-authorization-kernel` +- Start basis: the user explicitly started AUTH-07 after PR #124 merged AUTH-06 + as `f599551`; signed merge state required a separate explicit start. - PR #119 merged `WS-AUTH-001-05B` as `ad71c7e`. - PR #120 merged `WS-ART-001-OBJECT-STORAGE-AMENDMENT` as `4408256`. - PR #122 merged the first automated post-merge memory implementation as `fc89fb6`; its schema-v1 cross-initiative next pointer is superseded by the schema-v2 initiative-local clean cut. -- Current gate: no product chunk is selected by this authored file. A human - must explicitly start one candidate after reading current signed state. +- Current gate: L1 discovery and plan review before runtime implementation. - Scope checkpoint: AWS S3 is the only v0.1 production provider; MinIO is local/CI S3 protocol proof; LocalStorage is focused development/test; R2 and Flow Node are deferred. Product modules receive narrow artifact capabilities, @@ -25,9 +28,10 @@ each owning WS-ART feature chunk activates only its own canonical actions. - Next artifact candidate: `WS-ART-001-02A1` remains inactive until the user gives a separate explicit start signal. -- Parallel authorization work: `WS-AUTH-001-05B` merged through PR #119 as - `ad71c7e`. `WS-AUTH-001-06` remains inactive pending a separate explicit - user start. +- Authorization checkpoint: `WS-AUTH-001-06` merged through PR #124 as + `f599551`. The user separately started `WS-AUTH-001-07`; AUTH-08 remains + inactive until AUTH-07 merges, automated memory completes, and the user gives + another explicit start. - Parallel coverage work: `WS-QUAL-001-01B2` remains paused. Its last official whole-app result is `6466/8159` statements (`79.249908%`); no replacement evidence exists. diff --git a/.agent-loop/REVIEW_LOG.md b/.agent-loop/REVIEW_LOG.md index 0490c6e39..36dffb7ef 100644 --- a/.agent-loop/REVIEW_LOG.md +++ b/.agent-loop/REVIEW_LOG.md @@ -1,5 +1,14 @@ # Review Log +## 2026-07-15 - WS-AUTH-001-07 Started + +PR #124 merged `WS-AUTH-001-06` as `f599551`; Backend, Agent Gates, +CodeRabbit, and signed automated merge memory passed. The user explicitly +started `WS-AUTH-001-07` on branch +`codex/ws-auth-001-07-authorization-kernel`. This L1 authorization chunk is in +read-only discovery and required plan review; runtime implementation has not +started. + ## 2026-07-15 - WS-ENG-001-02 Internal Review Passed Reviewed implementation SHA `8670005` passed senior engineering, diff --git a/.agent-loop/WORK_QUEUE.md b/.agent-loop/WORK_QUEUE.md index 95a2735ce..68fa7a7ae 100644 --- a/.agent-loop/WORK_QUEUE.md +++ b/.agent-loop/WORK_QUEUE.md @@ -2,14 +2,14 @@ ## In Progress -None. This authored queue does not select a global next chunk; live post-merge -state is read from signed `automation/loop-memory` output. +| Chunk | Title | Risk | Status | +|---|---|---:|---| +| `WS-AUTH-001-07` | Authorization Kernel And Permission Registry | L1 | Explicitly started by the user after AUTH-06 merge; L1 discovery and plan review in progress | ## Planned Next | Chunk | Title | Risk | Status | |---|---|---:|---| -| `WS-AUTH-001-06` | Canonical Actor Profile And Identity Link | L1 | Inactive until a separate explicit user start | | `WS-QUAL-001-01B2` | Baseline Evidence And CI Ratchet | L1 | Paused for AUTH priority; no valid replacement baseline yet | | `WS-QUAL-001-02` | Project Service Coverage | L1 | Inactive until 01B2 merge/memory plus explicit user start | | `WS-POL-002-04` | Locked Runtime Execution And Routing Hardening | L1 | Inactive pending relevant authorization proof and a separate explicit user start | @@ -65,6 +65,7 @@ state is read from signed `automation/loop-memory` output. | `WS-AUTH-001-CAT` | Action And Resource Catalogue Reconciliation | L1 | Merged through PR #117 as `4c5d4fc` on 2026-07-14 | | `WS-AUTH-001-CAT-MEMORY` | Catalogue Post-Merge Memory | L1 | Merged through PR #118 as `eba7e2b` on 2026-07-14 | | `WS-AUTH-001-05B` | Authority Idempotency And Invalidation Foundation | L1 | Merged through PR #119 as `ad71c7e` on 2026-07-14 | +| `WS-AUTH-001-06` | Canonical Actor Profile And Identity Link | L1 | Merged through PR #124 as `f599551` on 2026-07-15 | | `WS-ART-001-OBJECT-STORAGE-AMENDMENT` | AWS-First Object Storage Planning Amendment | L1 | Merged through PR #120 as `4408256` on 2026-07-14 | | `WS-ENG-001-02` | Automated Post-Merge Memory | L1 | Merged through PR #122 as `fc89fb6`; schema-v1 output superseded by WS-ENG-001-03 | @@ -72,7 +73,8 @@ state is read from signed `automation/loop-memory` output. AUTH-05A merged through PR #115 as `8e1cde6`, and CAT plus its post-merge memory merged through PRs #117 and #118. AUTH-05B merged through PR #119 as `ad71c7e`. -Do not start AUTH-06 or POL-002-04 automatically. +AUTH-06 merged through PR #124 as `f599551`, and the user explicitly started +AUTH-07. Do not start AUTH-08 or POL-002-04 automatically. Coverage R10 merged through PR #108. Do not start 01B2, chunk 02, or another coverage implementation chunk from this worktree. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/CHUNK_MAP.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/CHUNK_MAP.md index 59ae9bac6..f291189de 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/CHUNK_MAP.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/CHUNK_MAP.md @@ -22,8 +22,8 @@ stopped. | `WS-AUTH-001-05A` | Shared Audit Ownership And Append-Only Authority Evidence | L1 | Merged through PR #115 as `8e1cde6` | | `WS-AUTH-001-CAT` | Action And Resource Catalogue Reconciliation | L1 | Merged through PR #117 as `4c5d4fc` | | `WS-AUTH-001-05B` | Authority Idempotency And Invalidation Foundation | L1 | Merged through PR #119 as `ad71c7e` | -| `WS-AUTH-001-06` | Canonical Actor Profile And Identity Link | L1 | Proposed | -| `WS-AUTH-001-07` | Authorization Kernel And Permission Registry | L1 | Proposed | +| `WS-AUTH-001-06` | Canonical Actor Profile And Identity Link | L1 | Merged through PR #124 as `f599551` | +| `WS-AUTH-001-07` | Authorization Kernel And Permission Registry | L1 | Explicitly started; discovery and L1 plan review in progress | | `WS-AUTH-001-08` | Bootstrap And Administrative Role Grants | L1 | Proposed | | `WS-AUTH-001-09` | Actor State, Identity Revocation, And Service Actors | L1 | Proposed | | `WS-AUTH-001-10` | Project Qualification And Contributor Role Grants | L1 | Proposed | @@ -116,5 +116,7 @@ human approval merged PR #115 as `8e1cde6` on 2026-07-14, followed by merged post-merge memory. `WS-AUTH-001-CAT` then merged through PR #117 as `4c5d4fc` after Backend, Agent Gates, CodeRabbit, and explicit human approval passed. The CAT post-merge memory merged through PR #118 as `eba7e2b`; AUTH-05B then merged -through PR #119 as `ad71c7e`. Do not start AUTH-06 or POL-002-04 without a -separate explicit user start. +through PR #119 as `ad71c7e`. AUTH-06 merged through PR #124 as `f599551`, its +signed automated memory completed, and the user explicitly started AUTH-07. Do +not start AUTH-08 or POL-002-04 without a separate explicit user start after +their prerequisites complete. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md index 8e69f7caf..caf0a154b 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md @@ -62,19 +62,24 @@ CodeRabbit; explicit human approval merged PR #117 as `4c5d4fc` on 2026-07-14. Its post-merge memory merged through PR #118 as `eba7e2b`. AUTH-05B's repaired L1 plan, implementation, repair, focused evidence, and required review tracks passed before explicit human approval merged PR #119 as `ad71c7e`. +AUTH-06 then established canonical actor profiles and identity links. Its +runtime, migration, compatibility, privacy, and API evidence passed required +internal and external checks; explicit human approval merged PR #124 as +`f599551` on 2026-07-15. Signed automated memory stopped with AUTH-07 requiring +a separate start. The user explicitly started AUTH-07 on 2026-07-15; discovery +and required L1 plan review are now active. ## Active planning chunk -None. `WS-AUTH-001-06` remains an inactive candidate requiring a separate -explicit user start. +None. ## Active implementation chunk -None. +`WS-AUTH-001-07` - Authorization Kernel And Permission Registry. ## Current implementation branch -None recorded by this authored status file. +`codex/ws-auth-001-07-authorization-kernel` ## Chunk status @@ -91,8 +96,8 @@ None recorded by this authored status file. | `WS-AUTH-001-05A` | Merged | `codex/ws-auth-001-05-authority-evidence` | #115 | Merged as `8e1cde6`; reviewed code `ea16fd8`; final branch head `d023952`. | | `WS-AUTH-001-CAT` | Merged | `codex/ws-auth-001-action-catalogue-reconciliation` | #117 | Merged as `4c5d4fc`; final branch head `5b4ec96`. | | `WS-AUTH-001-05B` | Merged | `codex/ws-auth-001-05b-idempotency-invalidation` | #119 | Merged as `ad71c7e`; reviewed runtime SHA `e083890`. | -| `WS-AUTH-001-06` | Proposed | - | - | Canonical actor profile and identity link. | -| `WS-AUTH-001-07` | Proposed | - | - | Authorization kernel and permissions. | +| `WS-AUTH-001-06` | Merged | `codex/ws-auth-001-06-canonical-actor-profile` | #124 | Merged as `f599551`; final PR head `4a2193f`. | +| `WS-AUTH-001-07` | In discovery and plan review | `codex/ws-auth-001-07-authorization-kernel` | - | Explicitly started after signed AUTH-06 merge memory. | | `WS-AUTH-001-08` | Proposed | - | - | Bootstrap and administrative grants. | | `WS-AUTH-001-09` | Proposed | - | - | Actor/link states and service actors. | | `WS-AUTH-001-10` | Proposed | - | - | Project contributor grants. | @@ -121,8 +126,9 @@ permission identifiers remain approved, including `operations.checker.retry`; the three recovery identifiers receive persisted parity only in their owning later chunks. `WS-AUTH-001-CAT` retains only safe registry/conformance rules. This is a scope decision, not an AUTH-05B runtime -blocker. PR #118 and AUTH-05B PR #119 are merged. AUTH-06 and POL-002-04 remain -inactive until an explicit human start. +blocker. PR #118, AUTH-05B PR #119, and AUTH-06 PR #124 are merged. AUTH-07 has +an explicit user start and is at its L1 plan-review gate. POL-002-04 remains +inactive until an explicit human start and its authorization prerequisite. AUTH-04B review evidence and its PR trust bundle are recorded at `reviews/WS-AUTH-001-04B-internal-review-evidence.md` and From 581ecd72e9d9354d20873e615c2fc29e2eafe4d2 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 15 Jul 2026 10:13:03 +0100 Subject: [PATCH 02/19] Split AUTH-07 at catalogue and kernel boundary --- .agent-loop/LOOP_STATE.md | 14 +- .agent-loop/REVIEW_LOG.md | 13 ++ .agent-loop/WORK_QUEUE.md | 6 +- .../CHUNK_MAP.md | 23 +- .../DECISIONS.md | 44 +++- .../PLAN.md | 5 +- .../RISKS.md | 1 + .../STATUS.md | 17 +- .../WS-AUTH-001-07-authorization-kernel.md | 215 +++--------------- ...-07A-closed-permission-action-catalogue.md | 162 +++++++++++++ ...01-07B-deny-default-kernel-self-cutover.md | 162 +++++++++++++ ...TH-001-07-preimplementation-plan-review.md | 38 ++++ docs/operations_authorization_service.md | 19 ++ docs/spec_authorization_service.md | 40 ++-- 14 files changed, 526 insertions(+), 233 deletions(-) create mode 100644 .agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md create mode 100644 .agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07B-deny-default-kernel-self-cutover.md create mode 100644 .agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md diff --git a/.agent-loop/LOOP_STATE.md b/.agent-loop/LOOP_STATE.md index e0e49baee..68e8e9d93 100644 --- a/.agent-loop/LOOP_STATE.md +++ b/.agent-loop/LOOP_STATE.md @@ -7,8 +7,8 @@ `automation/loop-memory`. - Active initiative: `WS-AUTH-001` - Workstream Authorization Service - Active planning chunk: none -- Active implementation chunk: `WS-AUTH-001-07` - Authorization Kernel And - Permission Registry +- Active implementation chunk: `WS-AUTH-001-07A` - Closed Permission And Action + Catalogue - Current branch: `codex/ws-auth-001-07-authorization-kernel` - Start basis: the user explicitly started AUTH-07 after PR #124 merged AUTH-06 as `f599551`; signed merge state required a separate explicit start. @@ -17,7 +17,8 @@ - PR #122 merged the first automated post-merge memory implementation as `fc89fb6`; its schema-v1 cross-initiative next pointer is superseded by the schema-v2 initiative-local clean cut. -- Current gate: L1 discovery and plan review before runtime implementation. +- Current gate: repaired 07A contract in required L1 plan review; no runtime + implementation has started. - Scope checkpoint: AWS S3 is the only v0.1 production provider; MinIO is local/CI S3 protocol proof; LocalStorage is focused development/test; R2 and Flow Node are deferred. Product modules receive narrow artifact capabilities, @@ -29,9 +30,10 @@ - Next artifact candidate: `WS-ART-001-02A1` remains inactive until the user gives a separate explicit start signal. - Authorization checkpoint: `WS-AUTH-001-06` merged through PR #124 as - `f599551`. The user separately started `WS-AUTH-001-07`; AUTH-08 remains - inactive until AUTH-07 merges, automated memory completes, and the user gives - another explicit start. + `f599551`. The user separately started parent `WS-AUTH-001-07`; required L1 + review split it into 07A/07B before runtime implementation. AUTH-07B and + AUTH-08 remain inactive until their predecessor merges, automated memory + completes, and the user gives another explicit start. - Parallel coverage work: `WS-QUAL-001-01B2` remains paused. Its last official whole-app result is `6466/8159` statements (`79.249908%`); no replacement evidence exists. diff --git a/.agent-loop/REVIEW_LOG.md b/.agent-loop/REVIEW_LOG.md index 36dffb7ef..548ab4c4b 100644 --- a/.agent-loop/REVIEW_LOG.md +++ b/.agent-loop/REVIEW_LOG.md @@ -9,6 +9,19 @@ started `WS-AUTH-001-07` on branch read-only discovery and required plan review; runtime implementation has not started. +## 2026-07-15 - WS-AUTH-001-07 Combined Plan Rejected + +Architecture/reuse and security/auth/product/docs reviewers failed the combined +contract; QA/test and CI integrity passed only with blocking conditions. The +contract required grant-backed admin reads before AUTH-08, project capabilities +before AUTH-10, omitted the audit ORM and actor-route ownership files, and did +not define exact active actions, denial precedence, transaction ownership, or +coverage-compatible verification. No runtime code was written. + +The bounded repair splits parent AUTH-07 into 07A closed catalogue/action-aware +audit parity and 07B minimal kernel/actor self-action cutover. The repaired 07A +contract must pass fresh L1 plan review before implementation. + ## 2026-07-15 - WS-ENG-001-02 Internal Review Passed Reviewed implementation SHA `8670005` passed senior engineering, diff --git a/.agent-loop/WORK_QUEUE.md b/.agent-loop/WORK_QUEUE.md index 68fa7a7ae..7ae62e49e 100644 --- a/.agent-loop/WORK_QUEUE.md +++ b/.agent-loop/WORK_QUEUE.md @@ -4,13 +4,14 @@ | Chunk | Title | Risk | Status | |---|---|---:|---| -| `WS-AUTH-001-07` | Authorization Kernel And Permission Registry | L1 | Explicitly started by the user after AUTH-06 merge; L1 discovery and plan review in progress | +| `WS-AUTH-001-07A` | Closed Permission And Action Catalogue | L1 | Parent AUTH-07 split before runtime implementation; repaired contract in required L1 plan review | ## Planned Next | Chunk | Title | Risk | Status | |---|---|---:|---| | `WS-QUAL-001-01B2` | Baseline Evidence And CI Ratchet | L1 | Paused for AUTH priority; no valid replacement baseline yet | +| `WS-AUTH-001-07B` | Deny-By-Default Kernel And Self-Action Cutover | L1 | Inactive until 07A merge/memory and explicit user start | | `WS-QUAL-001-02` | Project Service Coverage | L1 | Inactive until 01B2 merge/memory plus explicit user start | | `WS-POL-002-04` | Locked Runtime Execution And Routing Hardening | L1 | Inactive pending relevant authorization proof and a separate explicit user start | | `WS-ART-001-02A1` | External Service Adapter Foundation | L1 | Inactive until a separate explicit user start | @@ -74,7 +75,8 @@ AUTH-05A merged through PR #115 as `8e1cde6`, and CAT plus its post-merge memory merged through PRs #117 and #118. AUTH-05B merged through PR #119 as `ad71c7e`. AUTH-06 merged through PR #124 as `f599551`, and the user explicitly started -AUTH-07. Do not start AUTH-08 or POL-002-04 automatically. +AUTH-07. Required L1 review split it into 07A/07B before runtime implementation. +Do not start 07B, AUTH-08, or POL-002-04 automatically. Coverage R10 merged through PR #108. Do not start 01B2, chunk 02, or another coverage implementation chunk from this worktree. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/CHUNK_MAP.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/CHUNK_MAP.md index f291189de..83b8cada0 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/CHUNK_MAP.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/CHUNK_MAP.md @@ -23,7 +23,9 @@ stopped. | `WS-AUTH-001-CAT` | Action And Resource Catalogue Reconciliation | L1 | Merged through PR #117 as `4c5d4fc` | | `WS-AUTH-001-05B` | Authority Idempotency And Invalidation Foundation | L1 | Merged through PR #119 as `ad71c7e` | | `WS-AUTH-001-06` | Canonical Actor Profile And Identity Link | L1 | Merged through PR #124 as `f599551` | -| `WS-AUTH-001-07` | Authorization Kernel And Permission Registry | L1 | Explicitly started; discovery and L1 plan review in progress | +| `WS-AUTH-001-07` | Authorization Kernel And Permission Registry | L1 | Split before implementation after required L1 plan review | +| `WS-AUTH-001-07A` | Closed Permission And Action Catalogue | L1 | Repaired contract in required L1 plan review | +| `WS-AUTH-001-07B` | Deny-By-Default Kernel And Self-Action Cutover | L1 | Inactive until 07A merge/memory and explicit user start | | `WS-AUTH-001-08` | Bootstrap And Administrative Role Grants | L1 | Proposed | | `WS-AUTH-001-09` | Actor State, Identity Revocation, And Service Actors | L1 | Proposed | | `WS-AUTH-001-10` | Project Qualification And Contributor Role Grants | L1 | Proposed | @@ -47,7 +49,8 @@ WS-AUTH-001-PLAN -> WS-AUTH-001-CAT -> WS-AUTH-001-05B -> WS-AUTH-001-06 --> WS-AUTH-001-07 +-> WS-AUTH-001-07A +-> WS-AUTH-001-07B -> WS-AUTH-001-08 -> WS-AUTH-001-09 -> WS-AUTH-001-10 @@ -75,13 +78,16 @@ WS-AUTH-001-PLAN - Chunk 06 establishes canonical actor resolution while preserving only the enumerated non-authoritative legacy workflow-eligibility consumers required for intermediate-release operability. -- Chunk 07 provides the single authorization engine before grant APIs. +- Parent chunk 07 was split before runtime implementation. Chunk 07A owns the + closed permission/action catalogue and action-aware audit parity; chunk 07B + owns the minimal deny-by-default kernel and actor self-action cutover. - Chunks 08-10 establish local grant truth before product cutover. - Chunks 11-15 migrate bounded complete product/system surfaces. - Artifact upload, read, retention, release/delete, replication, integrity, and reconciliation remain mechanically owned by the artifact subsystem but must - receive centralized AUTH decisions. Chunk 07 owns the permission registry, - chunk 08 owns Operator grant definitions, chunk 09 owns fixed artifact service + receive centralized AUTH decisions. Chunk 07A owns the permission/action + registry, chunk 07B owns the central kernel, chunk 08 owns Operator grant + definitions, chunk 09 owns fixed artifact service principals, and each WS-ART feature chunk owns the canonical resource facts, guards, surface declarations, and behavior tests for the exact artifact actions it activates. AUTH-12, AUTH-14, and AUTH-15 do not pre-activate or @@ -117,6 +123,7 @@ post-merge memory. `WS-AUTH-001-CAT` then merged through PR #117 as `4c5d4fc` after Backend, Agent Gates, CodeRabbit, and explicit human approval passed. The CAT post-merge memory merged through PR #118 as `eba7e2b`; AUTH-05B then merged through PR #119 as `ad71c7e`. AUTH-06 merged through PR #124 as `f599551`, its -signed automated memory completed, and the user explicitly started AUTH-07. Do -not start AUTH-08 or POL-002-04 without a separate explicit user start after -their prerequisites complete. +signed automated memory completed, and the user explicitly started AUTH-07. +Required L1 review rejected the combined contract before runtime edits and +required 07A/07B. Do not start 07B, AUTH-08, or POL-002-04 without a separate +explicit user start after their prerequisites complete. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DECISIONS.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DECISIONS.md index 9e974cbf3..b07442a76 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DECISIONS.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DECISIONS.md @@ -209,8 +209,9 @@ not imply one another. The authorization decision and operation receipt must share bounded request/ correlation evidence, resource identity, operation, and service principal. -Receipts prove that storage work occurred; they do not create authority. AUTH-07 -registers exact artifact permissions, AUTH-08 defines applicable Operator +Receipts prove that storage work occurred; they do not create authority. AUTH-07A +registers exact artifact permissions and planned actions, AUTH-07B introduces +the central kernel, AUTH-08 defines applicable Operator grants, and AUTH-09 provisions fixed service principals. Each owning WS-ART feature chunk supplies the canonical resource composer, guards, surface declaration, and behavior tests that activate its exact actions. AUTH-12, @@ -222,7 +223,8 @@ no artifact permission or route attachment. Status: accepted by the user on 2026-07-14 after repository mapping and internal design review. -AUTH-07 introduces a closed typed action registry. Each active `ActionId` binds +AUTH-07A introduces a closed typed action registry, and AUTH-07B activates the +first bounded actions. Each active `ActionId` binds one already approved `PermissionId` to one canonical authorization target, candidate authority sources, mandatory guards, principal class, and transaction-revalidation rule. Multiple closed actions may map to one retained @@ -239,8 +241,9 @@ Each protected route or asynchronous command declares one primary registered action as its authorization entry point. Domain invariants remain owned by the feature service. Human bearer tokens are never executable worker authority, and collection filtering occurs before counts or cursors. Catalogue adoption is -staged: AUTH-07 owns types and its own current definitions, every route-owning -AUTH-07 through AUTH-15 chunk owns declarations during its feature cutover, and +staged: AUTH-07A owns identifiers and planned metadata, AUTH-07B owns the first +self-route definitions, every later route-owning chunk through AUTH-15 owns +declarations during its feature cutover, and AUTH-16 owns the aggregate generated route/command completeness manifest and final no-bypass proof. @@ -251,11 +254,11 @@ composer. An owning cutover chunk activates an action only with its adopted domain contract, canonical resource composer, route or command declaration, allow/deny behavior proof, and generated manifest-delta proof. -`ActionId` is security-significant evidence. AUTH-07 adds it to -`AuthorizationDecision`, bounded logs/metrics, and allowed/denied authority -events with exact typed/PostgreSQL registry parity. Historical events may remain -null; every AUTH-07-or-later action-based decision event requires a registered -identifier. Planned IDs can be registered before activation because they encode +`ActionId` is security-significant evidence. AUTH-07A adds typed/PostgreSQL +audit parity; AUTH-07B adds it to `AuthorizationDecision`, bounded logs/metrics, +and allowed/denied authority events. Historical events may remain null; every +AUTH-07B-or-later action-based decision event requires a registered identifier. +Planned IDs can be registered before activation because they encode only identifier, approved permission, owner, and availability, not foreign resource design. @@ -267,7 +270,7 @@ were unchanged by that catalogue review. The later approved artifact-storage contract adds 21 exact identifiers, making the current closed total 73. AUTH-05A currently enforces a 49-identifier typed/PostgreSQL audit base; the three already approved Operator recovery identifiers and 21 artifact -identifiers remain planned and non-executable until AUTH-07 adds typed/SQL audit +identifiers remain planned and non-executable until AUTH-07A adds typed/SQL audit parity and the paired owning feature activates each action. Other permission additions or renames still require an approved specification/ADR change, typed and PostgreSQL registry migration, audit-history treatment, cutover ownership, and rollback proof. @@ -275,6 +278,23 @@ WS-REV, WS-CON, and the artifact-storage specification continue to own their resources and state transitions. Migration custody is reconciled with merged main: AUTH-05A owns `0018`, AUTH-05B -solely owns `0019`, AUTH-06 uses `0020`, AUTH-07 action evidence uses `0021`, +solely owns `0019`, AUTH-06 uses `0020`, AUTH-07A action evidence uses `0021`, AUTH-08 uses `0022`, AUTH-10 uses `0023`, AUTH-12 uses `0024`, AUTH-13 uses `0025`, and AUTH-14 uses `0026`. + +## D16: Split AUTH-07 at the catalogue and executable-kernel boundary + +Status: accepted as a required L1 preimplementation repair on 2026-07-15. + +Required architecture, security/auth, and QA/CI plan review found that the +combined AUTH-07 contract placed grant-backed administrative APIs before +AUTH-08, project capability composition before AUTH-10, and mixed the audit +migration with executable kernel/API behavior. No runtime code had started. + +AUTH-07A now owns only the exact 73-PermissionId catalogue, 28 four-field +planned ActionId definitions, and action-aware audit migration `0021`. AUTH-07B +later owns the minimal deny-by-default kernel and activates only +`actor.profile.read_self` and `actor.profile.update_self`. Permission and +admin-role definition APIs move to AUTH-08; project-scoped authorization +context moves to AUTH-10. Each child retains its own merge and explicit-start +gate. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/PLAN.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/PLAN.md index 625a27bb2..2a6ed8083 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/PLAN.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/PLAN.md @@ -133,8 +133,9 @@ proving the same token role alone no longer authorizes. 5. Evolve shared audit evidence in 05A, then add canonical idempotency/invalidation in 05B. 6. Migrate to canonical profile/link semantics and first-human resolution. -7. Implement the minimal registered permission and AuthorizationService kernel - before protected authority-management APIs. +7. Register the closed permission/action catalogue with audit parity in 07A, + then implement the minimal AuthorizationService kernel and canonical actor + self-action cutover in 07B before protected authority-management APIs. 8. Implement bootstrap, `AuthorityControl`, and immutable admin-role grants. 9. Implement actor/link state administration and controlled service actors. 10. Implement qualification snapshots and exact-project contributor grants. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/RISKS.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/RISKS.md index 7e6fe7216..1131f6e80 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/RISKS.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/RISKS.md @@ -34,6 +34,7 @@ | A18 | Authority evidence is mutable or denial events arrive late | Security decisions cannot be reconstructed | Insert/read-only repository API, database append-only enforcement, per-mutation allowed/denied event proof, operational retention controls | Update/delete rejection plus atomic allowed/denied event tests in owning chunks | | A19 | Authority invalidation releases a needs-revision task as ordinary ready work | Prior findings, context, supersession, or replay obligations are bypassed | Keep the task in needs_revision with a durable unassigned obligation and controlled replacement assignment | Replacement-contributor revision-context, supersession, and high/medium replay tests | | A20 | Rate-limit replicas split counters, leak identity keys, lose increments, or retain pseudonymous rows indefinitely | Abuse controls bypassed or privacy/availability incident | Canonical Base64 HMAC secret, exact framed digest, one committed PostgreSQL statement, coordinated quiesced rotation, bounded opportunistic pruning, and operator idle cleanup | Known-answer/privacy tests, synchronized independent-session concurrency, commit-failure proof, rotation runbook, and retention tests | +| A21 | Kernel APIs ship before their grant/resource authority exists | Fabricated authority, deny-only public surfaces, or hidden dual policy | Split AUTH-07 into 07A catalogue/audit and 07B self-only kernel; defer admin definitions to 08 and project context to 10 | Child-contract plan review, exact active-action tables, and no-grant/no-project surface scans | ## Required reviewers diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md index caf0a154b..7d20d380d 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md @@ -68,6 +68,11 @@ internal and external checks; explicit human approval merged PR #124 as `f599551` on 2026-07-15. Signed automated memory stopped with AUTH-07 requiring a separate start. The user explicitly started AUTH-07 on 2026-07-15; discovery and required L1 plan review are now active. +That review rejected the combined AUTH-07 contract before runtime edits because +grant-backed/project APIs preceded their authority sources and the audit/API +ownership files were incomplete. Parent AUTH-07 is now split into 07A catalogue +and audit parity, followed by 07B kernel and actor self-action cutover. The +repaired 07A contract is under required plan review. ## Active planning chunk @@ -75,7 +80,7 @@ None. ## Active implementation chunk -`WS-AUTH-001-07` - Authorization Kernel And Permission Registry. +`WS-AUTH-001-07A` - Closed Permission And Action Catalogue. ## Current implementation branch @@ -97,7 +102,9 @@ None. | `WS-AUTH-001-CAT` | Merged | `codex/ws-auth-001-action-catalogue-reconciliation` | #117 | Merged as `4c5d4fc`; final branch head `5b4ec96`. | | `WS-AUTH-001-05B` | Merged | `codex/ws-auth-001-05b-idempotency-invalidation` | #119 | Merged as `ad71c7e`; reviewed runtime SHA `e083890`. | | `WS-AUTH-001-06` | Merged | `codex/ws-auth-001-06-canonical-actor-profile` | #124 | Merged as `f599551`; final PR head `4a2193f`. | -| `WS-AUTH-001-07` | In discovery and plan review | `codex/ws-auth-001-07-authorization-kernel` | - | Explicitly started after signed AUTH-06 merge memory. | +| `WS-AUTH-001-07` | Split | `codex/ws-auth-001-07-authorization-kernel` | - | Required L1 review rejected the combined contract before runtime edits. | +| `WS-AUTH-001-07A` | In plan review | `codex/ws-auth-001-07-authorization-kernel` | - | Closed catalogue and action-aware audit parity only. | +| `WS-AUTH-001-07B` | Proposed | - | - | Inactive until 07A merge/memory and explicit user start. | | `WS-AUTH-001-08` | Proposed | - | - | Bootstrap and administrative grants. | | `WS-AUTH-001-09` | Proposed | - | - | Actor/link states and service actors. | | `WS-AUTH-001-10` | Proposed | - | - | Project contributor grants. | @@ -127,8 +134,10 @@ permission identifiers remain approved, including parity only in their owning later chunks. `WS-AUTH-001-CAT` retains only safe registry/conformance rules. This is a scope decision, not an AUTH-05B runtime blocker. PR #118, AUTH-05B PR #119, and AUTH-06 PR #124 are merged. AUTH-07 has -an explicit user start and is at its L1 plan-review gate. POL-002-04 remains -inactive until an explicit human start and its authorization prerequisite. +an explicit user start. Required review split it before runtime implementation; +the repaired 07A contract is at its L1 plan-review gate. AUTH-07B and +POL-002-04 remain inactive until separate explicit human starts and their +authorization prerequisites. AUTH-04B review evidence and its PR trust bundle are recorded at `reviews/WS-AUTH-001-04B-internal-review-evidence.md` and diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07-authorization-kernel.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07-authorization-kernel.md index f4d14a538..78a01f1d7 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07-authorization-kernel.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07-authorization-kernel.md @@ -1,197 +1,44 @@ -# Chunk Contract: WS-AUTH-001-07 - Authorization Kernel And Permission Registry +# Parent Chunk: WS-AUTH-001-07 - Authorization Kernel And Permission Registry -## Parent initiative +## Status -`WS-AUTH-001` - Workstream Authorization Service +Split before runtime implementation on 2026-07-15 after required L1 plan +review. No application or migration code was written under the combined +contract. -## Goal +## Why the split was required -Implement the deny-by-default AuthorizationService kernel, registered -permissions, canonical ResourceContext, stable AuthorizationDecision, actor -state/global guards, and reusable FastAPI/application dependencies before any -grant-management API. +The combined contract crossed two independently reviewable persistence and +runtime boundaries: -## Why this chunk exists +- the closed permission/action catalogue and action-aware audit migration; and +- the deny-by-default evaluation kernel plus its first real API cutover. -Every later protected grant and product command must call one service rather -than introducing temporary direct grant queries. +It also proposed admin-definition and project-capability APIs before AUTH-08 +and AUTH-10 create their authoritative grant sources. Implementing those APIs +in AUTH-07 would require fabricated grants, token-role authority, or deny-only +public surfaces. All three outcomes violate the adopted authorization design. -## Approved plan reference +## Child chunks -- INTENT: `.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/INTENT.md` -- PLAN: `.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/PLAN.md` -- CHUNK_MAP: `.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/CHUNK_MAP.md` +| Chunk | Title | Boundary | +|---|---|---| +| `WS-AUTH-001-07A` | Closed Permission And Action Catalogue | Register exact approved permissions and planned actions, add action-aware audit parity, and make no action executable. | +| `WS-AUTH-001-07B` | Deny-By-Default Kernel And Self-Action Cutover | Implement the minimal kernel and activate only canonical actor self-read/self-update actions. | -## Risk class +## Deferred to owning chunks -L1 +- Permission and admin-role definition APIs move to AUTH-08, after bootstrap + and administrative grants exist. +- Project-scoped authorization context moves to AUTH-10, after exact-project + grants and canonical project capability composition exist. +- Feature resource loaders, grant matrices, concealment rules, and + revoke-versus-command races remain with their owning cutover chunks. -## SLA +## Ordering -P1 +`WS-AUTH-001-07A -> WS-AUTH-001-07B -> WS-AUTH-001-08` -## Allowed files - -```text -backend/app/modules/authorization/** -backend/app/modules/audit/** -backend/app/modules/actors/repository.py -backend/app/modules/actors/service.py -backend/app/api/deps/auth.py -backend/app/api/router.py -backend/app/main.py -backend/app/schemas/auth.py -backend/alembic/versions/0021_authorization_action_evidence.py -backend/tests/test_auth.py -backend/tests/test_audit.py -backend/tests/test_alembic.py -backend/tests/test_actors.py -backend/tests/test_app.py -backend/scripts/api_contract_e2e.py -docs/operations_authorization_service.md -docs/spec_authorization_service.md -.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/** -.agent-loop/LOOP_STATE.md -.agent-loop/WORK_QUEUE.md -.agent-loop/REVIEW_LOG.md -``` - -## Not allowed - -```text -dynamic/user-authored permissions or policy language -authorization decision cache across requests -direct role queries in routers -project/task/checker surface cutover -review/compensation permissions beyond registered future definitions -``` - -## Acceptance criteria - -- Permission identifiers are a closed registered enum/value set. -- The closed registry includes every exact artifact permission in - `docs/spec_authorization_service.md`: Operator reads/retry/recovery/audit, - guide-source ingest, contributor upload-session create/read/item/seal/cancel - actions, binding, - verification, pending-work scan, put-attempt resolution, guide-source read, checker-input - materialization, and checker-output write. Broad `operations.*` permissions - are not aliases for artifact authority. -- A closed typed action registry gives each active `ActionId` one approved - `PermissionId`, canonical target resource type, target-resolution rule, - allowed principal class, authority-candidate sources, mandatory registered - guards, and transaction-revalidation requirement. Multiple closed actions may - map to one retained broad permission with distinct targets/guards. Unknown - action, permission, resource, or guard identifiers deny during - registration/startup validation. -- Create and collection actions authorize against an existing parent or - `system` target; request-supplied project, owner, parent, and state values are - never canonical resource facts. -- Resource loading remains feature-owned. AUTH defines the bounded - `ResourceContext` protocol as closed typed per-resource variants and defines - the composition-root registration contract without importing feature - repositories or duplicating feature queries. Guards declare required facts; - missing, extra, or mistyped facts fail closed. -- Request context contains verified identity plus current local actor/grant state - and correlation/request IDs. -- Authorization resolves actor/link state, grant candidates, canonical project, - ownership, global guards, and resource state in a defined order. -- Unknown permissions deny. -- System scope is not superuser authority. -- Suspended/deactivated/revoked actors deny before permission expansion. -- Stable structured error envelope and concealment rules are defined. -- Table-driven tests cover every current permission/role/scope candidate. -- Sensitive mutations can revalidate inside the caller transaction/UoW. -- The bootstrap system operation and default-human self permissions are the - only usable candidates before grant tables ship. -- Direct role/grant authorization outside this service is explicitly banned. -- Reusable FastAPI and application-command dependencies accept one primary - registered action declaration. Domain invariants remain separate, and service - commands use fixed Workstream principals rather than serialized human tokens. -- Catalogue completeness is staged: this chunk validates current usable - definitions but does not require loaders or route declarations owned by - inactive feature-cutover or later domain initiatives. -- Reserved planned action metadata contains only stable `ActionId`, approved - `PermissionId`, owner, and availability; it never authorizes or predefines - another domain's target, facts, guards, or composer. Active definitions - require the owning domain contract, canonical composer, surface declaration, - and behavior tests. -- AUTH-07 registers every artifact-related planned `ActionId` in the canonical - table in `docs/spec_authorization_service.md` with its exact approved - `PermissionId` and owner. The 73-identifier count applies to PermissionIds, - not ActionIds. Registration is reserved metadata only: AUTH does not invent - artifact resource facts, guards, composers, or executable authority. -- `operations.artifact_storage_admission.read` is an operations-status action - mapped to the already approved `operations.status.read` PermissionId and the - deployment artifact-storage namespace. It is not an alias for any - `artifact.*` permission and grants no read/write access to artifact content. -- Artifact permissions are registered with typed/PostgreSQL audit parity here, - but remain reserved and non-executable until their owning WS-ART activation - row in `docs/spec_authorization_service.md` supplies the feature resource - composer, guards, surface declaration, and behavior tests. AUTH-12, AUTH-14, - and AUTH-15 are not alternate artifact activation paths. -- `AuthorizationDecision`, bounded logs/metrics, and every action-based allowed - or denied authority event carry the stable `ActionId`. Migration `0021` - preserves historical nulls, establishes exact typed/PostgreSQL action-registry - parity, and requires a registered identifier for AUTH-07-or-later - action-based decision evidence. Upgrade/downgrade/re-upgrade, direct-SQL, - unknown-ID, and preserved-history tests pass. -- The three authorization APIs introduced here have exact declarations: - permission and admin-role definition reads use `admin_role.read` against - `system` and expose only the authorized administrative metadata projection; - self authorization-context read uses `actor.profile.read_self` against the - current actor and returns active, currently authorized capabilities only. - Planned metadata never appears as an actor capability. -- Generated OpenAPI/command manifest-delta tests prove every protected surface - introduced by this chunk has exactly one active `ActionId` declaration. -- External denial precedence and concealment are an exact tested matrix. A - sensitive action declares its transaction linearization contract, including - ordered actor/link/grant/resource reload/locks or an explicitly approved - serializable retry strategy; each later mutation chunk proves its own - revoke-versus-command race. -- `GET /api/v1/authorization/permissions` and - `GET /api/v1/authorization/admin-role-definitions` return registered, - non-dynamic definitions with authorization/privacy tests. -- `GET /api/v1/actors/me/authorization-context` is introduced here, after the - closed registry exists, with project-scoped capability allowlist, privacy, - unknown-project, and inactive-actor tests. - -## Verification commands - -```bash -(cd backend && .venv/bin/python -m ruff check app tests) -(cd backend && WORKSTREAM_DATABASE_URL= .venv/bin/alembic upgrade head) -(cd backend && WORKSTREAM_DATABASE_URL= .venv/bin/alembic downgrade -1) -(cd backend && WORKSTREAM_DATABASE_URL= .venv/bin/alembic upgrade head) -(cd backend && WORKSTREAM_DATABASE_URL= .venv/bin/python -m pytest -q \ - tests/test_authorization.py tests/test_audit.py tests/test_alembic.py \ - --cov=app.modules.authorization \ - --cov-report=term-missing --cov-fail-under=90) -(cd backend && WORKSTREAM_DATABASE_URL= .venv/bin/python -m pytest -q) -(cd backend && WORKSTREAM_DATABASE_URL= .venv/bin/python scripts/api_contract_e2e.py) -python3 scripts/check_stale_workstream_wording.py -python3 scripts/check_markdown_links.py -git diff --check -``` - -## Required reviewers - -- senior engineering -- QA/test -- security/auth -- product/ops -- architecture -- CI integrity -- docs -- reuse/dedup -- test delta - -## Human review focus - -Review deny-by-default ordering, permission matrix completeness, canonical -resource scope, artifact permission separation, concealment behavior, and -transaction-local revalidation. - -## Stop conditions - -Stop if the service needs free-form client permissions, cached cross-request -decisions, or a generic policy engine. +Neither child starts the other automatically. Each child requires its own +evidence, internal review, PR, explicit human merge approval, signed merge +memory, and stop. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md new file mode 100644 index 000000000..db754d3b9 --- /dev/null +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md @@ -0,0 +1,162 @@ +# Chunk Contract: WS-AUTH-001-07A - Closed Permission And Action Catalogue + +## Parent initiative + +`WS-AUTH-001` - Workstream Authorization Service + +## Goal + +Create one closed typed catalogue for all 73 approved PermissionIds and the +exact reserved recovery/artifact ActionIds, then add typed and PostgreSQL action +evidence parity without making any action executable. + +## Why this chunk exists + +The deny-by-default kernel cannot safely start until permission and action +identifiers have one validated source, planned actions fail closed, and audit +storage can preserve a stable action identifier. + +## Risk routing + +- Risk class: L1 +- SLA: P1 +- Work type: authorization architecture, audit schema, migration, tests, docs +- Human gate: explicit PR review and merge approval +- Required reviewers: senior engineering, QA/test, security/auth, product/ops, + architecture, CI integrity, docs, reuse/dedup, test delta + +## Allowed files + +```text +backend/app/modules/authorization/** +backend/app/modules/audit/** +backend/app/modules/tasks/models.py +backend/alembic/versions/0021_authorization_action_evidence.py +backend/tests/test_authorization.py +backend/tests/test_audit.py +backend/tests/test_alembic.py +docs/spec_authorization_service.md +docs/operations_authorization_service.md +.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/** +.agent-loop/merge-intents/WS-AUTH-001-07A.json +.agent-loop/LOOP_STATE.md +.agent-loop/WORK_QUEUE.md +.agent-loop/REVIEW_LOG.md +``` + +## Not allowed + +```text +authorization decision evaluation +active route or command declarations +grant tables, grant queries, or role expansion +actor, project, task, checker, review, contribution, or artifact API cutover +feature resource composers, guards, or repositories +dynamic permissions or policy language +public permission/admin-role/context APIs +``` + +## Exact reserved ActionIds + +The catalogue registers the following as `planned`; none can authorize: + +```text +operations.task.start_override +operations.submission_gate.repair +operations.checker.retry +artifact.binding.read +artifact.replica.read +artifact.receipt.read +artifact.verification_job.read +artifact.verification_job.retry +artifact.recovery_attempt.read +artifact.audit.read +operations.artifact_storage_admission.read +artifact.guide_source.ingest +artifact.guide_source.read +artifact.upload_session.create +artifact.upload_session.read +artifact.upload_item.write +artifact.upload_session.seal +artifact.upload_session.cancel +artifact.upload_session.expire +artifact.guide_source.binding.create +artifact.submission.binding.create +artifact.checker_output.binding.create +artifact.verification.execute +artifact.pending_work.scan +artifact.put_attempt.resolve +artifact.pre_submit.checker_input.materialize +artifact.post_submit.checker_input.materialize +artifact.checker_output.write +``` + +Each planned definition stores only `action_id`, its approved `permission_id`, +the owning specification/chunk, and `availability=planned`. Principal class, +resource facts, guards, composers, concealment, and revalidation are activation +blueprints owned by later chunks and are not registered here. + +## Acceptance criteria + +- `PermissionId` is the single closed typed source for exactly the 73 approved + values in `docs/spec_authorization_service.md`. +- Existing audit validation consumes that source rather than maintaining a + second permission literal set. +- A frozen action catalogue contains exactly the 28 planned ActionIds above, + with exact approved PermissionId mapping, owner, and availability. +- Catalogue construction fails on duplicate or unknown actions, unknown + permissions, invalid owners, invalid availability, missing approved entries, + or extra entries. +- Planned actions cannot be resolved as executable and cannot be promoted by + configuration or request input. +- Artifact storage admission remains mapped only to + `operations.status.read`; it grants no `artifact.*` capability. +- Migration `0021` adds nullable `audit_events.action_id`, preserves all + historical rows as null, and constrains every non-null value to the registered + ActionId set. +- `AuthorityAuditEventInput` admits a bounded registered `action_id`; unknown + values fail before rejected input can escape diagnostics. +- Existing non-action authority events remain valid with null `action_id`. +- Downgrade refuses to discard any non-null action evidence. A clean database + with only null action IDs can downgrade and re-upgrade. +- Direct SQL proves unknown ActionIds fail and registered ActionIds persist. +- The canonical specification separates four-field planned registry metadata + from later feature activation blueprints. +- Operations docs cover startup catalogue failure, evidence inspection, and the + guarded rollback rule. +- No workflow, dependency, test skip, coverage exclusion, or global threshold + changes. + +## Verification commands + +```bash +(cd backend && .venv/bin/python -m ruff check app tests alembic/versions/0021_authorization_action_evidence.py) +(cd backend && WORKSTREAM_TEST_DATABASE_URL= \ + .venv/bin/python -m pytest -q tests/test_authorization.py tests/test_audit.py \ + tests/test_alembic.py --cov=app.modules.authorization \ + --cov=app.modules.audit --cov-branch --cov-report=term-missing \ + --cov-fail-under=90) +(cd backend && WORKSTREAM_TEST_DATABASE_URL= \ + .venv/bin/python scripts/run_isolated_tests.py tests/test_authorization.py \ + tests/test_audit.py tests/test_alembic.py) +python3 scripts/check_stale_workstream_wording.py +python3 scripts/check_stale_authorization_docs.py +python3 scripts/check_markdown_links.py +git diff --check +``` + +GitHub Backend remains authoritative for the full suite and repository-wide 78 +percent floor. + +## Human review focus + +Review exact 73-permission parity, exact 28-action planned mapping, inability to +execute planned actions, audit privacy, PostgreSQL constraint parity, historical +null preservation, and guarded downgrade. + +## Stop conditions + +Stop if the catalogue requires feature resource facts, executable grant +candidates, a generic policy engine, or dynamic/user-authored identifiers. + +Stop after merge and signed memory. Do not start AUTH-07B automatically. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07B-deny-default-kernel-self-cutover.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07B-deny-default-kernel-self-cutover.md new file mode 100644 index 000000000..5591873f5 --- /dev/null +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07B-deny-default-kernel-self-cutover.md @@ -0,0 +1,162 @@ +# Chunk Contract: WS-AUTH-001-07B - Deny-By-Default Kernel And Self-Action Cutover + +## Dependency + +AUTH-07A must be merged with signed memory, followed by a separate explicit +human start. + +## Goal + +Implement the minimal request-scoped AuthorizationService and activate it only +for canonical actor self-read and self-update. + +## Risk routing + +- Risk class: L1 +- SLA: P1 +- Work type: authorization runtime, API cutover, audit evidence, tests, docs +- Human gate: explicit PR review and merge approval +- Required reviewers: senior engineering, QA/test, security/auth, product/ops, + architecture, CI integrity, docs, reuse/dedup, test delta + +## Allowed files + +```text +backend/app/modules/authorization/** +backend/app/modules/audit/** +backend/app/modules/actors/repository.py +backend/app/modules/actors/service.py +backend/app/api/deps/authorization.py +backend/app/api/routes/auth.py +backend/app/api/router.py +backend/app/schemas/auth.py +backend/tests/test_authorization.py +backend/tests/test_auth.py +backend/tests/test_actors.py +backend/tests/test_api_controls.py +backend/tests/test_app.py +backend/scripts/api_contract_e2e.py +docs/spec_authorization_service.md +docs/operations_authorization_service.md +.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/** +.agent-loop/merge-intents/WS-AUTH-001-07B.json +.agent-loop/LOOP_STATE.md +.agent-loop/WORK_QUEUE.md +.agent-loop/REVIEW_LOG.md +``` + +## Not allowed + +```text +grant tables, grant queries, or role matrices +permission/admin-role definition APIs +project-scoped authorization-context API +feature resource loaders outside canonical actor self resources +cross-request decision caching +token-role authority or fabricated grants +generic policy language or a second unit-of-work abstraction +project/task/checker/review/contribution/artifact cutover +``` + +## Active action definitions + +| ActionId | PermissionId | Target | Candidate source | Guards | Revalidate | Concealment | Owner | +|---|---|---|---|---|---|---|---| +| `actor.profile.read_self` | `actor.profile.read_self` | current canonical actor profile | active human self | active identity link; actor active or suspended; exact self target | no | none; caller owns target | AUTH-07B | +| `actor.profile.update_self` | `actor.profile.update_self` | current canonical actor profile | active human self | active identity link; actor active; exact self target; caller-owned fields only | yes, actor and link locked in caller transaction | none; caller owns target | AUTH-07B | + +No other action becomes executable. + +## Denial precedence + +| Order | Condition | External status/code | Internal decision | +|---:|---|---|---| +| 1 | Missing or invalid bearer token | 401 existing auth code | no authorization decision | +| 2 | Unsupported subject kind or unprovisioned service | 403 existing actor code | no action evidence before canonical actor exists | +| 3 | Revoked identity link | 403 `identity_link_revoked` | denied with exact ActionId when transaction-local recheck observes revocation | +| 4 | Deactivated actor | 403 `actor_deactivated` | denied with exact ActionId when transaction-local recheck observes deactivation | +| 5 | Suspended actor on update | 403 `actor_suspended` | denied with exact ActionId | +| 6 | Unknown or planned action | 403 `permission_not_granted` at a public boundary | internal `unknown_action` or `action_unavailable` without leaking catalogue metadata | +| 7 | Target/fact mismatch | 403 `resource_guard_denied` | exact internal guard denial | +| 8 | No candidate permission | 403 `permission_not_granted` | exact internal candidate denial | + +Suspended actors may read their own bounded profile but cannot update it. These +self routes do not conceal a separate resource, so they never collapse denial +to 404. Feature-owned concealment matrices begin in their owning cutover chunks. + +## Transaction ownership + +- The kernel validates and writes staged audit evidence but never commits. +- Read dependencies commit only the completed read decision evidence. +- Self-update revalidates and locks actor/link state, applies the bounded + profile mutation, writes allow evidence, and commits once in the route-owned + transaction. +- A denied self-update rolls back staged business changes before committing a + clean denial event in a new transaction. +- Later mutation chunks must define their own lock order and race proof; this + chunk proves the pattern only on self-update. + +## Acceptance criteria + +- `AuthorizationContext` contains canonical actor/link state and request/ + correlation IDs, never token roles or arbitrary client permissions. +- Closed strict `ActorSelfResourceContext` rejects missing, extra, mistyped, or + non-self facts. A minimal `SystemResourceContext` extension type may exist but + grants no system authority and has no active action. +- `AuthorizationDecision` is stable and privacy bounded, carrying ActionId, + PermissionId, allowed/denial code, resource reference, matched authority kind, + revalidation status, request ID, and correlation ID. +- Unknown permissions, unknown actions, and every planned action deny. +- System scope is not superuser authority. +- Default human authority is exactly the two self candidates above. Token role + changes do not alter either decision. +- GET and PATCH `/api/v1/actors/me` each declare exactly one + `x-workstream-action-id` in OpenAPI and call the central kernel. +- PATCH accepts only its existing caller-owned display fields and preserves its + privacy contract. +- Real issuer-token tests resolve through the canonical actor dependency; no + fabricated AuthorizationContext or direct grant rows count as API proof. +- Allowed and denied decision evidence carries the exact active ActionId. +- Deterministic tests prove revoked-link and suspension/deactivation rechecks, + no cross-request cache, and a synchronized revoke-versus-update outcome + without sleeps. +- OpenAPI inventory and API E2E proofs are updated without weakening their + closed assertions. +- Permission/admin-role definition APIs remain deferred to AUTH-08. +- Project capability context remains deferred to AUTH-10 and no hidden project + existence is exposed here. +- Materially changed authorization/dependency/route behavior remains at or + above 90 percent branch coverage; global CI preserves the 78 percent floor. + +## Verification commands + +```bash +(cd backend && .venv/bin/python -m ruff check app tests scripts/api_contract_e2e.py) +(cd backend && WORKSTREAM_TEST_DATABASE_URL= \ + .venv/bin/python -m pytest -q tests/test_authorization.py tests/test_auth.py \ + tests/test_actors.py tests/test_api_controls.py tests/test_app.py \ + --cov=app.modules.authorization --cov=app.api.deps.authorization \ + --cov-branch --cov-report=term-missing --cov-fail-under=90) +(cd backend && WORKSTREAM_TEST_DATABASE_URL= \ + .venv/bin/python scripts/api_contract_e2e.py) +python3 scripts/check_stale_workstream_wording.py +python3 scripts/check_stale_authorization_docs.py +python3 scripts/check_markdown_links.py +git diff --check +``` + +GitHub Backend remains authoritative for the full suite and repository-wide 78 +percent floor. + +## Human review focus + +Review deny ordering, absence of token-role authority, exact self targeting, +suspended-read/update separation, transaction ownership, decision evidence, +OpenAPI declarations, and immediate state revalidation. + +## Stop conditions + +Stop if the kernel needs grant tables, project capability composition, feature +repositories, fabricated authority, or cross-request caching. + +Stop after merge and signed memory. Do not start AUTH-08 automatically. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md new file mode 100644 index 000000000..d4905dac6 --- /dev/null +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md @@ -0,0 +1,38 @@ +# WS-AUTH-001-07 Preimplementation Plan Review + +## Initial verdict + +`FAIL` before runtime implementation. + +## Review tracks + +| Track | Result | Blocking findings | +|---|---|---| +| senior engineering / architecture / reuse | FAIL | Combined catalogue, migration, kernel, APIs, manifest, and transaction proof were not one bounded L1 change; required ORM and route files were omitted. | +| security/auth / product/ops / docs | FAIL | Grant-backed and project-scoped APIs preceded authority sources; active actions, denial precedence, transaction ownership, response privacy, and planned artifact boundaries were incomplete. | +| QA/test / CI integrity | PASS WITH CONDITIONS | Missing allowed tests, exact action tables, migration versioning, synchronized race proof, isolated-test runner contract, and changed-subsystem 90 percent coverage. | + +No application, migration, dependency, workflow, or test code was changed before +these verdicts. + +## Valid repair + +- Split parent AUTH-07 into AUTH-07A and AUTH-07B. +- AUTH-07A owns exactly 73 PermissionIds, 28 four-field planned ActionIds, and + migration `0021` action-aware audit parity. No action is executable. +- AUTH-07B owns the minimal deny-by-default kernel and activates exactly + `actor.profile.read_self` and `actor.profile.update_self` on the existing + `/api/v1/actors/me` routes. +- Permission/admin-role definition APIs move to AUTH-08. +- Project-scoped authorization context moves to AUTH-10. +- Feature loaders, grant matrices, concealment, and revoke races remain with + their owning cutovers. +- Verification uses `WORKSTREAM_TEST_DATABASE_URL`, the isolated-test runner, + branch coverage at or above 90 percent for materially changed subsystems, and + the unchanged repository-wide 78 percent CI floor. + +## Re-review gate + +Fresh architecture, security/auth, and QA/CI plan review must pass the repaired +AUTH-07A contract before runtime implementation. Prior failed/conditional +results are not implementation approval. diff --git a/docs/operations_authorization_service.md b/docs/operations_authorization_service.md index f2fcc146f..e82d0029a 100644 --- a/docs/operations_authorization_service.md +++ b/docs/operations_authorization_service.md @@ -475,6 +475,25 @@ For each chunk: Do not cut over a resource family until its local actor, grant, permission, resource loader, lifecycle guards, negative tests, and evidence path exist. +### Catalogue And Action-Evidence Staging + +AUTH-07A installs exactly 73 PermissionIds and 28 planned ActionIds. Planned +entries contain only action, permission, owner, and availability; they are not +executable and must not receive deployment configuration, principals, resource +facts, or guards. Startup validation failure is a release blocker, not a reason +to skip or relax catalogue checks. + +Migration `0021` preserves historical audit rows with null `action_id`. Inspect +non-null action evidence only by bounded ActionId, request/correlation IDs, and +resource references; do not export event payloads or actor identity-link data +for routine diagnosis. Downgrade is allowed only while every action ID remains +null. If action-aware evidence exists, stop and recover forward rather than +discarding it. + +AUTH-07B later activates only canonical actor self-read and self-update. Admin +definition reads wait for AUTH-08 grant truth, and project capability context +waits for AUTH-10 exact-project grants and canonical project composition. + ## Rollback Rollback stops rather than bypasses authorization. A deployment may roll back diff --git a/docs/spec_authorization_service.md b/docs/spec_authorization_service.md index 922f94637..29fcf6929 100644 --- a/docs/spec_authorization_service.md +++ b/docs/spec_authorization_service.md @@ -199,8 +199,9 @@ audit.export Artifact permissions are deliberately resource- and operation-specific. `artifact.*.read` permissions do not authorize retry or recovery, human Operator permissions do not authorize internal execution, and internal service -permissions do not authorize Operator APIs. AUTH-07 owns this closed registry, -AUTH-08 owns the Operator grant definitions, AUTH-09 owns the service +permissions do not authorize Operator APIs. AUTH-07A owns this closed registry, +AUTH-07B introduces the central kernel, AUTH-08 owns the Operator grant +definitions, AUTH-09 owns the service principals, and WS-ART consumes the resulting decisions without registering permissions or inferring authority. Artifact actions activate only through the paired feature model below; AUTH-12, AUTH-14, and AUTH-15 do not activate or @@ -211,7 +212,7 @@ closed registry layer and are not included in that permission count. AUTH-05A's PostgreSQL audit registry accepts 49. The three approved Operator recovery identifiers `operations.task.start_override`, `operations.submission_gate.repair`, and `operations.checker.retry`, plus the -21 artifact identifiers above, are reserved planned metadata. AUTH-07 adds +21 artifact identifiers above, are reserved planned metadata. AUTH-07A adds their matching typed/SQL audit parity without making them executable. An artifact action becomes active only when the owning WS-ART chunk supplies its canonical resource composer, guards, surface declaration, behavior tests, and @@ -230,18 +231,19 @@ The paired artifact activation matrix is closed: | `WS-ART-001-06A` | `artifact.post_submit.checker_input.materialize` mapped to `artifact.checker_input.materialize` | | `WS-ART-001-06B` | `artifact.checker_output.write` and `artifact.checker_output.binding.create` mapped to `artifact.binding.create` using the checker-run resource | -Every row requires AUTH-07's registry first. A row with an Operator principal +Every row requires AUTH-07A's registry and AUTH-07B's kernel first. A row with an Operator principal also requires its AUTH-08 grant definition; a row with a fixed service principal also requires its AUTH-09 service-actor assignment. Feature code receives centralized decisions; it never queries grants or constructs permission identifiers dynamically. -The following table is the single source of truth for reserved artifact-related -`ActionId` metadata. AUTH-07 registers each row as `planned`; the owning WS-ART -chunk may activate it only with its canonical resource composer, guards, -surface declaration, and behavior tests. A mapping is not a permission alias: -authorization still evaluates the listed registered `PermissionId` against the -listed canonical resource and principal class. +The following table is the single source of truth for the owning WS-ART activation blueprint for +artifact-related `ActionId` values. AUTH-07A registers only each row's stable +`ActionId`, approved `PermissionId`, owning WS-ART chunk, and `planned` +availability. Its principal-class and canonical-resource columns are not AUTH +registry fields and are not executable authority; the owning WS-ART chunk must +adopt them with its canonical resource composer, guards, surface declaration, +and behavior tests before activation. A mapping is not a permission alias. | ActionId | PermissionId | Principal class | Canonical resource | Owning WS-ART chunk | |---|---|---|---|---| @@ -337,11 +339,12 @@ never serialize a human bearer token as executable authority. Collection actions authorize and filter against their canonical parent scope before counts, cursors, facets, or distinct values are computed. -Registration and completeness are staged with the approved chunk map. Chunk 07 -introduces the types and registry. Reserved action metadata contains only the +Registration and completeness are staged with the approved chunk map. Chunk +07A introduces the identifiers and planned registry; 07B introduces the kernel +and first active self actions. Reserved action metadata contains only the stable `ActionId`, approved `PermissionId`, owning specification/chunk, and `planned` availability; it is not executable and does not predefine a -foreign-domain target, facts, or guards. Every route-owning chunk from 07 +foreign-domain target, facts, or guards. Every route-owning chunk from 07B through 15 may promote an action to active only when its owning domain contract, feature-owned resource composition, surface declaration, and behavior tests exist. Each such chunk generates a manifest-delta proof for every surface it @@ -375,9 +378,10 @@ List filtering occurs before counts and pagination cursors. `AuthorizationDecision` carries the stable `ActionId` in addition to permission, resource, scope, matched authority, and denial information. The action identifier is included in bounded logs/metrics and every action-based allowed or denied -authority event emitted by AUTH-07 or a later chunk. AUTH-07 adds nullable +authority event emitted by AUTH-07B or a later chunk. AUTH-07A adds nullable historical storage and exact typed/SQL registry parity; legacy rows remain null, -while new action-based decision events must contain a registered identifier. A +while new AUTH-07B-or-later action-based decision events must contain a +registered identifier. A new action identifier requires the same approved typed/PostgreSQL registry and migration treatment as a permission. @@ -481,6 +485,12 @@ POST /api/v1/projects/{project_id}/role-grants/{grant_id}/revoke Exact request/response/error contracts are introduced by their owning chunks. No route may accept role or scope from request JSON as canonical authority. +AUTH-07B cuts only existing `GET|PATCH /api/v1/actors/me` behavior over to the +kernel. Permission and admin-role definition reads begin in AUTH-08 after +bootstrap and administrative-grant truth exists. Project-scoped +`GET /api/v1/actors/me/authorization-context` begins in AUTH-10 after +exact-project grant and canonical project capability composition exists. + ## Migration And Compatibility The implementation order is fixed by the WS-AUTH-001 chunk map: From b1b47b01334b3d7276cf818ae53ad90b004ffc33 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 15 Jul 2026 10:20:27 +0100 Subject: [PATCH 03/19] Repair AUTH-07A catalogue contract --- .agent-loop/REVIEW_LOG.md | 11 ++ .../DECISIONS.md | 5 +- .../STATUS.md | 11 +- ...-07A-closed-permission-action-catalogue.md | 114 ++++++++++-------- .../WS-AUTH-001-13-task-assignment-cutover.md | 11 +- ...-AUTH-001-14-submission-checker-cutover.md | 12 +- ...TH-001-07-preimplementation-plan-review.md | 2 +- docs/operations_authorization_service.md | 2 +- docs/spec_authorization_service.md | 5 + 9 files changed, 106 insertions(+), 67 deletions(-) diff --git a/.agent-loop/REVIEW_LOG.md b/.agent-loop/REVIEW_LOG.md index 548ab4c4b..04000f2b8 100644 --- a/.agent-loop/REVIEW_LOG.md +++ b/.agent-loop/REVIEW_LOG.md @@ -22,6 +22,17 @@ The bounded repair splits parent AUTH-07 into 07A closed catalogue/action-aware audit parity and 07B minimal kernel/actor self-action cutover. The repaired 07A contract must pass fresh L1 plan review before implementation. +## 2026-07-15 - WS-AUTH-001-07A First Repaired Plan Failed + +Exact-SHA review of `581ecd7` confirmed the split and deferrals but found four +remaining blockers: migration `0021` omitted the two AUTH-07B self ActionIds, +the planned catalogue lacked one exact mapping/owner table, AUTH-13/14 still +claimed later permission-registry migrations, and combined coverage could hide +a sub-90 materially changed subsystem. QA additionally corrected the isolated +database runner invocation, while security required an exclusive audit-table +lock before downgrade evidence checks. No runtime code was written. The second +repair closes those exact findings and requires another fresh exact-SHA review. + ## 2026-07-15 - WS-ENG-001-02 Internal Review Passed Reviewed implementation SHA `8670005` passed senior engineering, diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DECISIONS.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DECISIONS.md index b07442a76..8edaafd22 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DECISIONS.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DECISIONS.md @@ -291,8 +291,9 @@ combined AUTH-07 contract placed grant-backed administrative APIs before AUTH-08, project capability composition before AUTH-10, and mixed the audit migration with executable kernel/API behavior. No runtime code had started. -AUTH-07A now owns only the exact 73-PermissionId catalogue, 28 four-field -planned ActionId definitions, and action-aware audit migration `0021`. AUTH-07B +AUTH-07A now owns only the exact 73-PermissionId catalogue, 30 four-field +planned ActionId definitions, including both later self actions, and +action-aware audit migration `0021`. AUTH-07B later owns the minimal deny-by-default kernel and activates only `actor.profile.read_self` and `actor.profile.update_self`. Permission and admin-role definition APIs move to AUTH-08; project-scoped authorization diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md index 7d20d380d..8abed2d76 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md @@ -127,12 +127,13 @@ inferred kinds before the owning canonical actor migration. The proposed external catalogue cannot be adopted as a normative handoff: it conflicts with `/api/v1`, AUTH-05A's merged 49-identifier persisted audit base, -current project and artifact models, and staged domain ownership. All 52 -permission identifiers remain approved, including +current project and artifact models, and staged domain ownership. All 73 +permission identifiers are approved, including `operations.task.start_override`, `operations.submission_gate.repair`, and -`operations.checker.retry`; the three recovery identifiers receive persisted -parity only in their owning later chunks. `WS-AUTH-001-CAT` retains only safe -registry/conformance rules. This is a scope decision, not an AUTH-05B runtime +`operations.checker.retry`; AUTH-07A gives those recovery identifiers exact +typed/PostgreSQL parity while AUTH-13/14 retain action activation and feature +behavior ownership. `WS-AUTH-001-CAT` retains only safe registry/conformance +rules. This is a scope decision, not an AUTH-05B runtime blocker. PR #118, AUTH-05B PR #119, and AUTH-06 PR #124 are merged. AUTH-07 has an explicit user start. Required review split it before runtime implementation; the repaired 07A contract is at its L1 plan-review gate. AUTH-07B and diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md index db754d3b9..f65c1bb90 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md @@ -7,7 +7,7 @@ ## Goal Create one closed typed catalogue for all 73 approved PermissionIds and the -exact reserved recovery/artifact ActionIds, then add typed and PostgreSQL action +exact reserved self/recovery/artifact ActionIds, then add typed and PostgreSQL action evidence parity without making any action executable. ## Why this chunk exists @@ -56,45 +56,48 @@ dynamic permissions or policy language public permission/admin-role/context APIs ``` -## Exact reserved ActionIds - -The catalogue registers the following as `planned`; none can authorize: - -```text -operations.task.start_override -operations.submission_gate.repair -operations.checker.retry -artifact.binding.read -artifact.replica.read -artifact.receipt.read -artifact.verification_job.read -artifact.verification_job.retry -artifact.recovery_attempt.read -artifact.audit.read -operations.artifact_storage_admission.read -artifact.guide_source.ingest -artifact.guide_source.read -artifact.upload_session.create -artifact.upload_session.read -artifact.upload_item.write -artifact.upload_session.seal -artifact.upload_session.cancel -artifact.upload_session.expire -artifact.guide_source.binding.create -artifact.submission.binding.create -artifact.checker_output.binding.create -artifact.verification.execute -artifact.pending_work.scan -artifact.put_attempt.resolve -artifact.pre_submit.checker_input.materialize -artifact.post_submit.checker_input.materialize -artifact.checker_output.write -``` - -Each planned definition stores only `action_id`, its approved `permission_id`, -the owning specification/chunk, and `availability=planned`. Principal class, -resource facts, guards, composers, concealment, and revalidation are activation -blueprints owned by later chunks and are not registered here. +## Exact planned action catalogue + +The owner is a full canonical chunk ID. The closed owner set is exactly the +distinct values in this table; abbreviations, free-form domains, and aliases +are invalid. Every row has `availability=planned` and cannot authorize. + +| ActionId | PermissionId | Owner | Availability | +|---|---|---|---| +| `actor.profile.read_self` | `actor.profile.read_self` | `WS-AUTH-001-07B` | `planned` | +| `actor.profile.update_self` | `actor.profile.update_self` | `WS-AUTH-001-07B` | `planned` | +| `operations.task.start_override` | `operations.task.start_override` | `WS-AUTH-001-13` | `planned` | +| `operations.submission_gate.repair` | `operations.submission_gate.repair` | `WS-AUTH-001-14` | `planned` | +| `operations.checker.retry` | `operations.checker.retry` | `WS-AUTH-001-14` | `planned` | +| `artifact.binding.read` | `artifact.binding.read` | `WS-ART-001-02D` | `planned` | +| `artifact.replica.read` | `artifact.replica.read` | `WS-ART-001-02D` | `planned` | +| `artifact.receipt.read` | `artifact.receipt.read` | `WS-ART-001-02D` | `planned` | +| `artifact.verification_job.read` | `artifact.verification_job.read` | `WS-ART-001-02D` | `planned` | +| `artifact.verification_job.retry` | `artifact.verification_job.retry` | `WS-ART-001-02D` | `planned` | +| `artifact.recovery_attempt.read` | `artifact.recovery_attempt.read` | `WS-ART-001-02D` | `planned` | +| `artifact.audit.read` | `artifact.audit.read` | `WS-ART-001-02D` | `planned` | +| `operations.artifact_storage_admission.read` | `operations.status.read` | `WS-ART-001-02D` | `planned` | +| `artifact.guide_source.ingest` | `artifact.guide_source.ingest` | `WS-ART-001-03` | `planned` | +| `artifact.guide_source.read` | `artifact.guide_source.read` | `WS-ART-001-03` | `planned` | +| `artifact.upload_session.create` | `artifact.upload_session.create` | `WS-ART-001-04A` | `planned` | +| `artifact.upload_session.read` | `artifact.upload_session.read` | `WS-ART-001-04A` | `planned` | +| `artifact.upload_item.write` | `artifact.upload_item.write` | `WS-ART-001-04A` | `planned` | +| `artifact.upload_session.seal` | `artifact.upload_session.seal` | `WS-ART-001-04A` | `planned` | +| `artifact.upload_session.cancel` | `artifact.upload_session.cancel` | `WS-ART-001-04A` | `planned` | +| `artifact.upload_session.expire` | `artifact.upload_session.expire` | `WS-ART-001-04A` | `planned` | +| `artifact.guide_source.binding.create` | `artifact.binding.create` | `WS-ART-001-03` | `planned` | +| `artifact.submission.binding.create` | `artifact.binding.create` | `WS-ART-001-05` | `planned` | +| `artifact.checker_output.binding.create` | `artifact.binding.create` | `WS-ART-001-06B` | `planned` | +| `artifact.verification.execute` | `artifact.verification.execute` | `WS-ART-001-02D` | `planned` | +| `artifact.pending_work.scan` | `artifact.pending_work.scan` | `WS-ART-001-02D` | `planned` | +| `artifact.put_attempt.resolve` | `artifact.put_attempt.resolve` | `WS-ART-001-02D` | `planned` | +| `artifact.pre_submit.checker_input.materialize` | `artifact.checker_input.materialize` | `WS-ART-001-04B` | `planned` | +| `artifact.post_submit.checker_input.materialize` | `artifact.checker_input.materialize` | `WS-ART-001-06A` | `planned` | +| `artifact.checker_output.write` | `artifact.checker_output.write` | `WS-ART-001-06B` | `planned` | + +Each definition stores only these four fields. Principal class, resource facts, +guards, composers, concealment, and revalidation are activation blueprints +owned by later chunks and are not registered here. ## Acceptance criteria @@ -102,7 +105,7 @@ blueprints owned by later chunks and are not registered here. values in `docs/spec_authorization_service.md`. - Existing audit validation consumes that source rather than maintaining a second permission literal set. -- A frozen action catalogue contains exactly the 28 planned ActionIds above, +- A frozen action catalogue contains exactly the 30 planned ActionIds above, with exact approved PermissionId mapping, owner, and availability. - Catalogue construction fails on duplicate or unknown actions, unknown permissions, invalid owners, invalid availability, missing approved entries, @@ -114,11 +117,21 @@ blueprints owned by later chunks and are not registered here. - Migration `0021` adds nullable `audit_events.action_id`, preserves all historical rows as null, and constrains every non-null value to the registered ActionId set. +- A non-null `action_id` is valid only for `SensitiveAuthorizationAllowed` or + `SensitiveAuthorizationDenied`; unrelated authority/lifecycle events must + remain null. +- Migration `0021` replaces the 49-value permission constraint with exact + 73-value typed/PostgreSQL parity without changing historical values. - `AuthorityAuditEventInput` admits a bounded registered `action_id`; unknown values fail before rejected input can escape diagnostics. - Existing non-action authority events remain valid with null `action_id`. - Downgrade refuses to discard any non-null action evidence. A clean database - with only null action IDs can downgrade and re-upgrade. + with only null action IDs can downgrade, restores the exact prior 49-value + permission constraint, drops the action column/constraint, and re-upgrades. +- Downgrade takes `LOCK audit_events IN ACCESS EXCLUSIVE MODE` before checking + for non-null action evidence or changing constraints. Deterministic + independent-session proof shows a concurrent insert cannot pass between the + check and destructive DDL. - Direct SQL proves unknown ActionIds fail and registered ActionIds persist. - The canonical specification separates four-field planned registry metadata from later feature activation blueprints. @@ -131,14 +144,17 @@ blueprints owned by later chunks and are not registered here. ```bash (cd backend && .venv/bin/python -m ruff check app tests alembic/versions/0021_authorization_action_evidence.py) -(cd backend && WORKSTREAM_TEST_DATABASE_URL= \ +(cd backend && tmp_dir=$(mktemp -d) && trap 'rm -rf "$tmp_dir"' EXIT && \ + .venv/bin/coverage erase && \ + WORKSTREAM_TEST_ADMIN_DATABASE_URL= \ + .venv/bin/python scripts/run_isolated_tests.py \ + --metadata-json "$tmp_dir/07a.json" --timeout-seconds 1800 -- \ .venv/bin/python -m pytest -q tests/test_authorization.py tests/test_audit.py \ tests/test_alembic.py --cov=app.modules.authorization \ - --cov=app.modules.audit --cov-branch --cov-report=term-missing \ - --cov-fail-under=90) -(cd backend && WORKSTREAM_TEST_DATABASE_URL= \ - .venv/bin/python scripts/run_isolated_tests.py tests/test_authorization.py \ - tests/test_audit.py tests/test_alembic.py) + --cov=app.modules.audit --cov-branch --cov-report= --cov-fail-under=0 && \ + .venv/bin/coverage report --include='app/modules/authorization/*' \ + --fail-under=90 && \ + .venv/bin/coverage report --include='app/modules/audit/*' --fail-under=90) python3 scripts/check_stale_workstream_wording.py python3 scripts/check_stale_authorization_docs.py python3 scripts/check_markdown_links.py @@ -150,7 +166,7 @@ percent floor. ## Human review focus -Review exact 73-permission parity, exact 28-action planned mapping, inability to +Review exact 73-permission parity, exact 30-action planned mapping, inability to execute planned actions, audit privacy, PostgreSQL constraint parity, historical null preservation, and guarded downgrade. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-13-task-assignment-cutover.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-13-task-assignment-cutover.md index 1c5cd4007..4d72062cf 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-13-task-assignment-cutover.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-13-task-assignment-cutover.md @@ -86,10 +86,13 @@ token role or legacy active-worker-profile fallback reasoned start override is separately authorized as `operations.task.start_override` for Operator recovery; matched permission, scope, reason, and audit event distinguish the two paths. -- Before `operations.task.start_override` becomes active, migration `0025` and - the typed audit schema add that already approved identifier to the 49-item - AUTH-05A audit base. Upgrade/downgrade/re-upgrade and direct-SQL parity tests - preserve all prior audit rows and reject unknown identifiers. +- AUTH-07A migration `0021` already gives + `operations.task.start_override` PermissionId/ActionId typed and PostgreSQL + parity as planned metadata. This chunk promotes the action only with its task + resource composer, Operator candidate, guards, surface declaration, reason, + evidence, and behavior tests. Migration `0025` owns task/assignment and + Contributor-field schema changes only; it does not change the permission or + action registry. - Operator `operations.status.read` exposes a read-only cross-project task-queue operational projection with bounded fields; it does not grant task mutation. Audit Authority `audit.read` exposes only covered task evidence. Both paths diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-14-submission-checker-cutover.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-14-submission-checker-cutover.md index 756a7951e..bf3e1d3da 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-14-submission-checker-cutover.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-14-submission-checker-cutover.md @@ -87,11 +87,13 @@ legacy active-worker-profile or workflow-eligibility compatibility fallback uses distinct `operations.submission_gate.repair` and `operations.checker.retry` permissions. Each path requires a reason and records matched grant/permission without granting general project authority. -- Before `operations.submission_gate.repair` or `operations.checker.retry` - becomes active, migration `0026` and the typed audit schema add both already - approved identifiers to the 50-item post-AUTH-13 audit base. Upgrade, - downgrade, re-upgrade, and direct-SQL parity tests preserve earlier audit rows - and establish exact 52-identifier typed/PostgreSQL parity. +- AUTH-07A migration `0021` already gives + `operations.submission_gate.repair` and `operations.checker.retry` + PermissionId/ActionId typed and PostgreSQL parity as planned metadata. This + chunk promotes each action only with its feature resource composer, Operator + candidate, guards, surface declaration, reason, evidence, and behavior tests. + Migration `0026` owns submission/checker Contributor-field schema changes + only; it does not change the permission or action registry. - Contributor reads preserve ownership, hidden-result redaction, and concealed not-found behavior. - Audit reads expose only permission-appropriate bounded fields before counts. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md index d4905dac6..8d55735a6 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md @@ -18,7 +18,7 @@ these verdicts. ## Valid repair - Split parent AUTH-07 into AUTH-07A and AUTH-07B. -- AUTH-07A owns exactly 73 PermissionIds, 28 four-field planned ActionIds, and +- AUTH-07A owns exactly 73 PermissionIds, 30 four-field planned ActionIds, and migration `0021` action-aware audit parity. No action is executable. - AUTH-07B owns the minimal deny-by-default kernel and activates exactly `actor.profile.read_self` and `actor.profile.update_self` on the existing diff --git a/docs/operations_authorization_service.md b/docs/operations_authorization_service.md index e82d0029a..4374cfcbc 100644 --- a/docs/operations_authorization_service.md +++ b/docs/operations_authorization_service.md @@ -477,7 +477,7 @@ resource loader, lifecycle guards, negative tests, and evidence path exist. ### Catalogue And Action-Evidence Staging -AUTH-07A installs exactly 73 PermissionIds and 28 planned ActionIds. Planned +AUTH-07A installs exactly 73 PermissionIds and 30 planned ActionIds. Planned entries contain only action, permission, owner, and availability; they are not executable and must not receive deployment configuration, principals, resource facts, or guards. Startup validation failure is a release blocker, not a reason diff --git a/docs/spec_authorization_service.md b/docs/spec_authorization_service.md index 29fcf6929..de5927adb 100644 --- a/docs/spec_authorization_service.md +++ b/docs/spec_authorization_service.md @@ -219,6 +219,11 @@ canonical resource composer, guards, surface declaration, behavior tests, and transaction-local revalidation where required. Both halves are mandatory; registry presence alone never grants authority. +AUTH-07A also reserves `actor.profile.read_self` and +`actor.profile.update_self` as four-field planned action metadata. AUTH-07B +later supplies their complete active definitions and self-route behavior proof +without changing migration `0021`. + The paired artifact activation matrix is closed: | Owning WS-ART chunk | Actions activated by that chunk | From 8690ef51a337b2478a8108d8c70d1669fcece7bc Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 15 Jul 2026 10:29:03 +0100 Subject: [PATCH 04/19] Close AUTH-07A audit mapping invariants --- .agent-loop/REVIEW_LOG.md | 12 ++++++ ...-07A-closed-permission-action-catalogue.md | 39 +++++++++++++------ ...TH-001-07-preimplementation-plan-review.md | 25 ++++++++++-- 3 files changed, 61 insertions(+), 15 deletions(-) diff --git a/.agent-loop/REVIEW_LOG.md b/.agent-loop/REVIEW_LOG.md index 04000f2b8..910fb0ee9 100644 --- a/.agent-loop/REVIEW_LOG.md +++ b/.agent-loop/REVIEW_LOG.md @@ -1,5 +1,17 @@ # Review Log +## 2026-07-15 - WS-AUTH-001-07A Second Repaired Plan Failed + +Exact-SHA senior engineering, architecture/reuse, QA/test, and CI-integrity +review passed `b1b47b0`, while security/auth, product/ops, and docs review found +one remaining audit-integrity blocker. The contract independently bounded +ActionIds and PermissionIds but did not enforce each action's exact permission +mapping, allowed newly admitted permissions without action evidence, did not +bar planned actions from allowed-decision evidence, and checked only non-null +actions before downgrade. The repair closes all four cases in typed and +PostgreSQL acceptance criteria and keeps runtime code unmodified pending fresh +exact-SHA review. + ## 2026-07-15 - WS-AUTH-001-07 Started PR #124 merged `WS-AUTH-001-06` as `f599551`; Backend, Agent Gates, diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md index f65c1bb90..b6d701cd3 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md @@ -117,22 +117,38 @@ owned by later chunks and are not registered here. - Migration `0021` adds nullable `audit_events.action_id`, preserves all historical rows as null, and constrains every non-null value to the registered ActionId set. +- Typed validation and PostgreSQL enforce every non-null ActionId's exact + PermissionId mapping from the 30-row catalogue; an action cannot be persisted + with another registered permission. +- Every PermissionId outside migration `0018`'s historical 49-value set requires + a non-null ActionId whose catalogue row maps to that PermissionId. The three + planned actions mapped to an existing PermissionId may still carry their + registered action while the historical permission remains valid without one + for pre-`0021` event shapes. - A non-null `action_id` is valid only for `SensitiveAuthorizationAllowed` or `SensitiveAuthorizationDenied`; unrelated authority/lifecycle events must remain null. - Migration `0021` replaces the 49-value permission constraint with exact 73-value typed/PostgreSQL parity without changing historical values. - `AuthorityAuditEventInput` admits a bounded registered `action_id`; unknown - values fail before rejected input can escape diagnostics. + values and action/permission mismatches fail before rejected input can escape + diagnostics. +- Because all 30 actions remain `planned`, `AuthorityAuditEventInput` rejects + `SensitiveAuthorizationAllowed` when any of them is present. A planned action + may be persisted only as bounded `SensitiveAuthorizationDenied` evidence; + activation chunks own later allowed evidence. - Existing non-action authority events remain valid with null `action_id`. -- Downgrade refuses to discard any non-null action evidence. A clean database - with only null action IDs can downgrade, restores the exact prior 49-value - permission constraint, drops the action column/constraint, and re-upgrades. +- Downgrade refuses when either any non-null action evidence or any PermissionId + outside the historical 49-value set exists. A clean database satisfying both + predicates can downgrade, restores the exact prior 49-value permission + constraint, drops the action column/constraint, and re-upgrades. - Downgrade takes `LOCK audit_events IN ACCESS EXCLUSIVE MODE` before checking - for non-null action evidence or changing constraints. Deterministic - independent-session proof shows a concurrent insert cannot pass between the - check and destructive DDL. -- Direct SQL proves unknown ActionIds fail and registered ActionIds persist. + both refusal predicates or changing constraints. Deterministic independent- + session proof shows a concurrent insert cannot pass between the checks and + destructive DDL. +- Direct SQL proves unknown ActionIds and mismatched action/permission pairs + fail, every new permission without its mapped action fails, and each registered + planned ActionId persists only as denied evidence with its mapped permission. - The canonical specification separates four-field planned registry metadata from later feature activation blueprints. - Operations docs cover startup catalogue failure, evidence inspection, and the @@ -166,9 +182,10 @@ percent floor. ## Human review focus -Review exact 73-permission parity, exact 30-action planned mapping, inability to -execute planned actions, audit privacy, PostgreSQL constraint parity, historical -null preservation, and guarded downgrade. +Review exact 73-permission parity, exact 30-action planned mapping, typed/SQL +mapping enforcement, inability to record planned actions as allowed, audit +privacy, PostgreSQL constraint parity, historical null preservation, and guarded +downgrade. ## Stop conditions diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md index 8d55735a6..7cb8d52af 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md @@ -27,12 +27,29 @@ these verdicts. - Project-scoped authorization context moves to AUTH-10. - Feature loaders, grant matrices, concealment, and revoke races remain with their owning cutovers. -- Verification uses `WORKSTREAM_TEST_DATABASE_URL`, the isolated-test runner, +- Verification uses `WORKSTREAM_TEST_ADMIN_DATABASE_URL`, the isolated-test runner, branch coverage at or above 90 percent for materially changed subsystems, and the unchanged repository-wide 78 percent CI floor. +## Second repaired review + +Exact-SHA senior engineering, architecture/reuse, QA/test, and CI-integrity +review passed `b1b47b0`. Security/auth, product/ops, and docs review found one +remaining audit-integrity blocker before runtime implementation: + +- the contract admitted registered actions and permissions independently rather + than enforcing the exact action-to-permission mapping; +- a newly admitted permission could be stored without an action, so the guarded + downgrade predicate was incomplete; and +- planned actions were not explicitly barred from allowed-decision evidence. + +The next candidate closes those findings in both typed and PostgreSQL +acceptance criteria, requires denial-only evidence for planned actions, and +checks both action and post-`0018` permission evidence under the exclusive +downgrade lock. No runtime code was written. + ## Re-review gate -Fresh architecture, security/auth, and QA/CI plan review must pass the repaired -AUTH-07A contract before runtime implementation. Prior failed/conditional -results are not implementation approval. +Fresh architecture, security/auth, product/ops, docs, and QA/CI plan review must +pass the repaired AUTH-07A contract before runtime implementation. Prior +failed/conditional results are not implementation approval. From beb85ac9d7d6ad0f7f12630cf3e8fdc4df8ac3f7 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 15 Jul 2026 10:32:34 +0100 Subject: [PATCH 05/19] Clarify AUTH-07A availability enforcement --- .agent-loop/REVIEW_LOG.md | 11 +++++++++++ ...UTH-001-07A-closed-permission-action-catalogue.md | 10 ++++++++-- .../WS-AUTH-001-07-preimplementation-plan-review.md | 12 ++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/.agent-loop/REVIEW_LOG.md b/.agent-loop/REVIEW_LOG.md index 910fb0ee9..a252bd656 100644 --- a/.agent-loop/REVIEW_LOG.md +++ b/.agent-loop/REVIEW_LOG.md @@ -1,5 +1,16 @@ # Review Log +## 2026-07-15 - WS-AUTH-001-07A Third Repaired Plan Failed + +Exact-SHA architecture/reuse and CI review of `8690ef5` passed all prior +mapping, downgrade, scope, and verification findings but rejected one +temporal-schema ambiguity. If migration `0021` froze all planned actions to +denial-only PostgreSQL evidence, AUTH-07B could not activate its two self +actions without an unowned migration. The repair keeps availability enforcement +in typed catalogue validation while PostgreSQL permanently enforces registered +identifiers, decision-event-only use, and exact action-to-permission mapping. +Runtime code remains unmodified pending fresh exact-SHA review. + ## 2026-07-15 - WS-AUTH-001-07A Second Repaired Plan Failed Exact-SHA senior engineering, architecture/reuse, QA/test, and CI-integrity diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md index b6d701cd3..ab3938558 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md @@ -137,6 +137,10 @@ owned by later chunks and are not registered here. `SensitiveAuthorizationAllowed` when any of them is present. A planned action may be persisted only as bounded `SensitiveAuthorizationDenied` evidence; activation chunks own later allowed evidence. +- PostgreSQL is deliberately availability-neutral: it enforces registration, + decision-event-only use, and the exact action-to-permission mapping, but does + not freeze `planned` versus `active`. Later owner chunks activate catalogue + rows in typed code without altering migration `0021`. - Existing non-action authority events remain valid with null `action_id`. - Downgrade refuses when either any non-null action evidence or any PermissionId outside the historical 49-value set exists. A clean database satisfying both @@ -147,8 +151,10 @@ owned by later chunks and are not registered here. session proof shows a concurrent insert cannot pass between the checks and destructive DDL. - Direct SQL proves unknown ActionIds and mismatched action/permission pairs - fail, every new permission without its mapped action fails, and each registered - planned ActionId persists only as denied evidence with its mapped permission. + fail and every new permission without its mapped action fails. Direct-SQL + positive fixtures use denied evidence for every planned ActionId; denial-only + availability is tested at `AuthorityAuditEventInput`, not as a PostgreSQL + constraint. - The canonical specification separates four-field planned registry metadata from later feature activation blueprints. - Operations docs cover startup catalogue failure, evidence inspection, and the diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md index 7cb8d52af..5507a7c5c 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md @@ -48,6 +48,18 @@ acceptance criteria, requires denial-only evidence for planned actions, and checks both action and post-`0018` permission evidence under the exclusive downgrade lock. No runtime code was written. +## Third repaired review + +Exact-SHA architecture/reuse and CI review of `8690ef5` confirmed the mapping +and downgrade repair but rejected one temporal-schema ambiguity. The contract +could be read as freezing planned actions to denied events in PostgreSQL, which +would prevent AUTH-07B from activating its two self actions without another +migration. The repair makes availability a typed catalogue invariant only. +PostgreSQL remains stable across activation and enforces registration, +decision-event-only use, and exact action-to-permission mapping. Direct-SQL +planned-action fixtures use denied evidence without asserting a database-level +availability rule. No runtime code was written. + ## Re-review gate Fresh architecture, security/auth, product/ops, docs, and QA/CI plan review must From c2055ce5b51835a140c967b1b3d1e5898da7f76b Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 15 Jul 2026 10:35:07 +0100 Subject: [PATCH 06/19] Approve AUTH-07A implementation contract --- .agent-loop/LOOP_STATE.md | 5 +++-- .agent-loop/REVIEW_LOG.md | 11 ++++++++++ .agent-loop/WORK_QUEUE.md | 2 +- .../CHUNK_MAP.md | 2 +- .../STATUS.md | 8 ++++--- ...TH-001-07-preimplementation-plan-review.md | 21 ++++++++++++++++--- 6 files changed, 39 insertions(+), 10 deletions(-) diff --git a/.agent-loop/LOOP_STATE.md b/.agent-loop/LOOP_STATE.md index 68e8e9d93..2c696192d 100644 --- a/.agent-loop/LOOP_STATE.md +++ b/.agent-loop/LOOP_STATE.md @@ -17,8 +17,9 @@ - PR #122 merged the first automated post-merge memory implementation as `fc89fb6`; its schema-v1 cross-initiative next pointer is superseded by the schema-v2 initiative-local clean cut. -- Current gate: repaired 07A contract in required L1 plan review; no runtime - implementation has started. +- Current gate: AUTH-07A's repaired L1 contract passed every required + preimplementation review at `beb85ac`; bounded runtime implementation may + start. - Scope checkpoint: AWS S3 is the only v0.1 production provider; MinIO is local/CI S3 protocol proof; LocalStorage is focused development/test; R2 and Flow Node are deferred. Product modules receive narrow artifact capabilities, diff --git a/.agent-loop/REVIEW_LOG.md b/.agent-loop/REVIEW_LOG.md index a252bd656..c052520c0 100644 --- a/.agent-loop/REVIEW_LOG.md +++ b/.agent-loop/REVIEW_LOG.md @@ -1,5 +1,16 @@ # Review Log +## 2026-07-15 - WS-AUTH-001-07A Repaired Plan Passed + +Exact-SHA `beb85ac` passed senior engineering, architecture/reuse, +security/auth, product/ops, docs, QA/test, test-delta, and CI-integrity review. +The final contract keeps planned/active availability in typed validation while +PostgreSQL enforces registered identifiers, decision-event use, exact +action-to-permission mapping, post-`0018` permission pairing, and guarded +downgrade custody. Seventy-one agent-gate tests and all static documentation +checks passed. Bounded AUTH-07A implementation may begin; AUTH-07B remains +inactive. + ## 2026-07-15 - WS-AUTH-001-07A Third Repaired Plan Failed Exact-SHA architecture/reuse and CI review of `8690ef5` passed all prior diff --git a/.agent-loop/WORK_QUEUE.md b/.agent-loop/WORK_QUEUE.md index 7ae62e49e..268ed1f60 100644 --- a/.agent-loop/WORK_QUEUE.md +++ b/.agent-loop/WORK_QUEUE.md @@ -4,7 +4,7 @@ | Chunk | Title | Risk | Status | |---|---|---:|---| -| `WS-AUTH-001-07A` | Closed Permission And Action Catalogue | L1 | Parent AUTH-07 split before runtime implementation; repaired contract in required L1 plan review | +| `WS-AUTH-001-07A` | Closed Permission And Action Catalogue | L1 | Repaired contract passed required L1 plan review at `beb85ac`; bounded implementation active | ## Planned Next diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/CHUNK_MAP.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/CHUNK_MAP.md index 83b8cada0..b60128cb8 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/CHUNK_MAP.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/CHUNK_MAP.md @@ -24,7 +24,7 @@ stopped. | `WS-AUTH-001-05B` | Authority Idempotency And Invalidation Foundation | L1 | Merged through PR #119 as `ad71c7e` | | `WS-AUTH-001-06` | Canonical Actor Profile And Identity Link | L1 | Merged through PR #124 as `f599551` | | `WS-AUTH-001-07` | Authorization Kernel And Permission Registry | L1 | Split before implementation after required L1 plan review | -| `WS-AUTH-001-07A` | Closed Permission And Action Catalogue | L1 | Repaired contract in required L1 plan review | +| `WS-AUTH-001-07A` | Closed Permission And Action Catalogue | L1 | Repaired L1 contract approved at `beb85ac`; implementation active | | `WS-AUTH-001-07B` | Deny-By-Default Kernel And Self-Action Cutover | L1 | Inactive until 07A merge/memory and explicit user start | | `WS-AUTH-001-08` | Bootstrap And Administrative Role Grants | L1 | Proposed | | `WS-AUTH-001-09` | Actor State, Identity Revocation, And Service Actors | L1 | Proposed | diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md index 8abed2d76..4cbf450e4 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md @@ -67,7 +67,8 @@ runtime, migration, compatibility, privacy, and API evidence passed required internal and external checks; explicit human approval merged PR #124 as `f599551` on 2026-07-15. Signed automated memory stopped with AUTH-07 requiring a separate start. The user explicitly started AUTH-07 on 2026-07-15; discovery -and required L1 plan review are now active. +and required L1 plan review are complete. The repaired AUTH-07A contract passed +all required tracks at `beb85ac`; bounded implementation is active. That review rejected the combined AUTH-07 contract before runtime edits because grant-backed/project APIs preceded their authority sources and the audit/API ownership files were incomplete. Parent AUTH-07 is now split into 07A catalogue @@ -103,7 +104,7 @@ None. | `WS-AUTH-001-05B` | Merged | `codex/ws-auth-001-05b-idempotency-invalidation` | #119 | Merged as `ad71c7e`; reviewed runtime SHA `e083890`. | | `WS-AUTH-001-06` | Merged | `codex/ws-auth-001-06-canonical-actor-profile` | #124 | Merged as `f599551`; final PR head `4a2193f`. | | `WS-AUTH-001-07` | Split | `codex/ws-auth-001-07-authorization-kernel` | - | Required L1 review rejected the combined contract before runtime edits. | -| `WS-AUTH-001-07A` | In plan review | `codex/ws-auth-001-07-authorization-kernel` | - | Closed catalogue and action-aware audit parity only. | +| `WS-AUTH-001-07A` | Implementing | `codex/ws-auth-001-07-authorization-kernel` | - | Repaired contract approved at `beb85ac`; closed catalogue and action-aware audit parity only. | | `WS-AUTH-001-07B` | Proposed | - | - | Inactive until 07A merge/memory and explicit user start. | | `WS-AUTH-001-08` | Proposed | - | - | Bootstrap and administrative grants. | | `WS-AUTH-001-09` | Proposed | - | - | Actor/link states and service actors. | @@ -136,7 +137,8 @@ behavior ownership. `WS-AUTH-001-CAT` retains only safe registry/conformance rules. This is a scope decision, not an AUTH-05B runtime blocker. PR #118, AUTH-05B PR #119, and AUTH-06 PR #124 are merged. AUTH-07 has an explicit user start. Required review split it before runtime implementation; -the repaired 07A contract is at its L1 plan-review gate. AUTH-07B and +the repaired 07A contract passed all required tracks at `beb85ac` and bounded +implementation is active. AUTH-07B and POL-002-04 remain inactive until separate explicit human starts and their authorization prerequisites. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md index 5507a7c5c..73ff023e4 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07-preimplementation-plan-review.md @@ -62,6 +62,21 @@ availability rule. No runtime code was written. ## Re-review gate -Fresh architecture, security/auth, product/ops, docs, and QA/CI plan review must -pass the repaired AUTH-07A contract before runtime implementation. Prior -failed/conditional results are not implementation approval. +`PASS` at exact planning SHA `beb85ac9d7d6ad0f7f12630cf3e8fdc4df8ac3f7`. + +Senior engineering, architecture/reuse, security/auth, product/ops, docs, +QA/test, test-delta, and CI-integrity review found no remaining blockers. The +reviewed contract keeps availability in typed validation, keeps PostgreSQL +availability-neutral, closes exact action-to-permission mapping and downgrade +custody, and preserves the 07A/07B/08/10 ownership boundaries. + +Deterministic planning evidence at the reviewed SHA: + +- 71 agent-gate tests passed with third-party pytest plugin autoload disabled; +- Markdown links passed; +- stale Workstream wording passed; +- stale authorization documentation passed; and +- `git diff --check` passed. + +Bounded AUTH-07A runtime implementation may begin. Prior failed results remain +historical evidence and do not approve code outside this contract. From 8e9e970280b5200c05f856de14e74cb123e15c68 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 15 Jul 2026 11:11:54 +0100 Subject: [PATCH 07/19] Implement AUTH-07A closed action catalogue --- .../0021_authorization_action_evidence.py | 271 +++++++++++++ backend/app/modules/audit/schemas.py | 49 ++- .../app/modules/authorization/catalogue.py | 362 ++++++++++++++++++ backend/app/modules/tasks/models.py | 1 + backend/tests/test_alembic.py | 345 ++++++++++++++++- backend/tests/test_audit.py | 72 ++++ backend/tests/test_authorization.py | 128 +++++++ docs/operations_authorization_service.md | 13 +- docs/spec_authorization_service.md | 6 + 9 files changed, 1225 insertions(+), 22 deletions(-) create mode 100644 backend/alembic/versions/0021_authorization_action_evidence.py create mode 100644 backend/app/modules/authorization/catalogue.py diff --git a/backend/alembic/versions/0021_authorization_action_evidence.py b/backend/alembic/versions/0021_authorization_action_evidence.py new file mode 100644 index 000000000..1babe2703 --- /dev/null +++ b/backend/alembic/versions/0021_authorization_action_evidence.py @@ -0,0 +1,271 @@ +"""add closed permission and action audit parity + +Revision ID: 0021_auth_action_evidence +Revises: 0020_canonical_actor_profile +Create Date: 2026-07-15 +""" + +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + +revision = "0021_auth_action_evidence" +down_revision = "0020_canonical_actor_profile" +branch_labels = None +depends_on = None + +HISTORICAL_PERMISSIONS = """actor.profile.read_self actor.profile.update_self actor.profile.read_any +actor.profile.suspend actor.profile.reactivate actor.profile.deactivate actor.identity_link.read +actor.identity_link.revoke actor.identity_link.reactivate actor.service.provision admin_role.read +admin_role.grant admin_role.revoke project.create project.read project.update project.archive +project.guide.manage project.effective_policy.manage project.task.manage project.review_policy.manage +project.role_grant.read project.role_grant.manage task.queue.read task.claim submission.create +submission.read_own submission.read_for_review review.queue.read review.queue.inspect review.claim +review.release review.decline_preference review.decision review.lease.force_release review.chain.read +contribution.read_self contribution.read_project compensation.policy.manage +compensation.adapter_binding.manage compensation.award.read compensation.delivery.reconcile +operations.status.read operations.timer.run operations.reconcile.run operations.outbox.retry +operations.projection.rebuild audit.read audit.export""".split() + +NEW_PERMISSIONS = """operations.task.start_override operations.submission_gate.repair +operations.checker.retry artifact.binding.read artifact.replica.read artifact.receipt.read +artifact.verification_job.read artifact.verification_job.retry artifact.recovery_attempt.read +artifact.audit.read artifact.guide_source.ingest artifact.upload_session.create +artifact.upload_session.read artifact.upload_item.write artifact.upload_session.seal +artifact.upload_session.cancel artifact.upload_session.expire artifact.binding.create +artifact.verification.execute artifact.pending_work.scan artifact.put_attempt.resolve +artifact.guide_source.read artifact.checker_input.materialize artifact.checker_output.write""".split() + +PERMISSIONS = HISTORICAL_PERMISSIONS + NEW_PERMISSIONS + +ACTION_PERMISSION_PAIRS = ( + ("actor.profile.read_self", "actor.profile.read_self"), + ("actor.profile.update_self", "actor.profile.update_self"), + ("operations.task.start_override", "operations.task.start_override"), + ("operations.submission_gate.repair", "operations.submission_gate.repair"), + ("operations.checker.retry", "operations.checker.retry"), + ("artifact.binding.read", "artifact.binding.read"), + ("artifact.replica.read", "artifact.replica.read"), + ("artifact.receipt.read", "artifact.receipt.read"), + ("artifact.verification_job.read", "artifact.verification_job.read"), + ("artifact.verification_job.retry", "artifact.verification_job.retry"), + ("artifact.recovery_attempt.read", "artifact.recovery_attempt.read"), + ("artifact.audit.read", "artifact.audit.read"), + ("operations.artifact_storage_admission.read", "operations.status.read"), + ("artifact.guide_source.ingest", "artifact.guide_source.ingest"), + ("artifact.guide_source.read", "artifact.guide_source.read"), + ("artifact.upload_session.create", "artifact.upload_session.create"), + ("artifact.upload_session.read", "artifact.upload_session.read"), + ("artifact.upload_item.write", "artifact.upload_item.write"), + ("artifact.upload_session.seal", "artifact.upload_session.seal"), + ("artifact.upload_session.cancel", "artifact.upload_session.cancel"), + ("artifact.upload_session.expire", "artifact.upload_session.expire"), + ("artifact.guide_source.binding.create", "artifact.binding.create"), + ("artifact.submission.binding.create", "artifact.binding.create"), + ("artifact.checker_output.binding.create", "artifact.binding.create"), + ("artifact.verification.execute", "artifact.verification.execute"), + ("artifact.pending_work.scan", "artifact.pending_work.scan"), + ("artifact.put_attempt.resolve", "artifact.put_attempt.resolve"), + ( + "artifact.pre_submit.checker_input.materialize", + "artifact.checker_input.materialize", + ), + ( + "artifact.post_submit.checker_input.materialize", + "artifact.checker_input.materialize", + ), + ("artifact.checker_output.write", "artifact.checker_output.write"), +) + +DENIAL_CODES = """required_scope_missing unsupported_subject_kind service_actor_not_provisioned +identity_link_revoked actor_suspended actor_deactivated permission_not_granted scope_not_authorized +self_grant_forbidden self_role_revoke_forbidden resource_guard_denied actor_not_found grant_not_found +resource_not_found actor_already_suspended actor_not_suspended actor_deactivated_terminal +last_access_administrator admin_role_grant_exists project_role_grant_exists identity_link_conflict +resource_project_mismatch idempotency_mismatch invalid_role_scope invalid_project_role +qualification_snapshot_invalid""".split() + +REASONS = { + "ActorProfileProvisioned": ("automatic_first_access",), + "ServiceActorProvisioned": ("manual_service_provisioning",), + "ActorIdentityLinked": ("identity_lifecycle_change",), + "ActorIdentityLinkRevoked": ("identity_lifecycle_change",), + "ActorIdentityLinkReactivated": ("identity_lifecycle_change",), + "ActorProfileSuspended": ("security_response", "administrative_correction"), + "ActorProfileReactivated": ("administrative_correction",), + "ActorProfileDeactivated": ("security_response", "administrative_correction"), + "InitialAccessAdministratorBootstrapped": ("initial_access_bootstrap",), + "AdminRoleGrantIssued": ("authority_assignment",), + "AdminRoleGrantRevoked": ("authority_revocation",), + "AdminRoleGrantIssueDenied": ("authorization_policy_denial",), + "LastAccessAdministratorOperationDenied": ("authorization_policy_denial",), + "ProjectRoleQualificationSnapshotCaptured": ("qualification_evidence_captured",), + "ProjectRoleGrantIssued": ("authority_assignment",), + "ProjectRoleGrantReplaced": ("authority_replacement",), + "ProjectRoleGrantRevoked": ("authority_revocation",), + "SensitiveAuthorizationAllowed": ("authorization_evaluation",), + "SensitiveAuthorizationDenied": ("authorization_evaluation",), + "AuthorityInvalidationRequested": ("authority_state_changed",), +} + + +def _tokens(values: list[str] | tuple[str, ...]) -> str: + return ", ".join(f"'{value}'" for value in values) + + +def _pair_tokens() -> str: + return ", ".join( + f"('{action}', '{permission}')" for action, permission in ACTION_PERMISSION_PAIRS + ) + + +def _create_registry_constraint(permissions: list[str]) -> None: + reason_rules = " or ".join( + f"(event_type = '{event}' and reason in ({_tokens(reasons)}))" + for event, reasons in REASONS.items() + ) + op.create_check_constraint( + "authority_registries", + "audit_events", + f""" + event_domain <> 'authority' or ( + reason is not null and ({reason_rules}) + and (permission_id is null or permission_id in ({_tokens(permissions)})) + and (denial_code is null or denial_code in ({_tokens(DENIAL_CODES)})) + ) + """, + ) + + +def _create_privacy_constraint(permissions: list[str]) -> None: + entity_tokens = _tokens( + ( + "actor_profile", + "actor_identity_link", + "admin_role_grant", + "qualification_snapshot", + "project_role_grant", + "authorization_decision", + "authority_invalidation", + ) + ) + resource_tokens = _tokens( + """actor_profile actor_identity_link admin_role_grant project project_role_grant task + submission review contribution compensation_award compensation_delivery operations + audit_event""".split() + ) + uuid_target_tokens = _tokens( + ( + "actor_profile", + "actor_identity_link", + "admin_role_grant", + "qualification_snapshot", + "project_role_grant", + ) + ) + uuid_pattern = r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + permission_tokens = _tokens(permissions) + op.create_check_constraint( + "authority_privacy_bounds", + "audit_events", + f""" + event_domain <> 'authority' or ( + id ~ '{uuid_pattern}' and entity_type in ({entity_tokens}) and entity_id ~ '{uuid_pattern}' + and ( + (actor_ref_kind in ('legacy_actor', 'actor_profile') and actor_id ~ '{uuid_pattern}') + or (actor_ref_kind = 'system_principal' and actor_id = 'workstream:system:bootstrap') + ) + and ( + target_actor_ref is null + or (target_actor_ref_kind = 'actor_profile' and target_actor_ref ~ '{uuid_pattern}') + ) + and (matched_grant_id is null or matched_grant_id ~ '{uuid_pattern}') + and (project_id is null or project_id ~ '{uuid_pattern}') + and (resource_type is null or resource_type in ({resource_tokens})) + and (resource_id is null or resource_id ~ '{uuid_pattern}') + and ( + target_ref_kind is null + or (target_ref_kind in ({uuid_target_tokens}) and target_ref_id ~ '{uuid_pattern}') + or (target_ref_kind = 'permission_registry' and target_ref_id in ({permission_tokens})) + ) + and ( + invalidation_target_kind is null + or ( + invalidation_target_kind in ({uuid_target_tokens}) + and invalidation_target_ref ~ '{uuid_pattern}' + ) + or ( + invalidation_target_kind = 'permission_registry' + and invalidation_target_ref in ({permission_tokens}) + ) + ) + and ( + entity_type not in ('authorization_decision', 'authority_invalidation') + or entity_id = id + ) + and ( + resource_type <> 'project' or resource_id is null + or (project_id is not null and resource_id = project_id) + ) + ) + """, + ) + + +def upgrade() -> None: + """Install action-aware audit constraints without activating any action.""" + op.add_column("audit_events", sa.Column("action_id", sa.String(160), nullable=True)) + op.drop_constraint("authority_registries", "audit_events", type_="check") + op.drop_constraint("authority_privacy_bounds", "audit_events", type_="check") + _create_registry_constraint(PERMISSIONS) + _create_privacy_constraint(PERMISSIONS) + + pair_tokens = _pair_tokens() + op.create_check_constraint( + "authorization_action_evidence", + "audit_events", + f""" + ( + event_domain = 'legacy_lifecycle' and action_id is null + ) or ( + event_domain = 'authority' + and ( + action_id is null or ( + event_type in ('SensitiveAuthorizationAllowed', 'SensitiveAuthorizationDenied') + and permission_id is not null + and (action_id, permission_id) in ({pair_tokens}) + ) + ) + and ( + permission_id is null + or permission_id not in ({_tokens(NEW_PERMISSIONS)}) + or ( + action_id is not null + and (action_id, permission_id) in ({pair_tokens}) + ) + ) + ) + """, + ) + + +def downgrade() -> None: + """Remove action parity only when no forward evidence would be discarded.""" + bind = op.get_bind() + bind.execute(sa.text("lock table audit_events in access exclusive mode")) + has_forward_evidence = bind.execute( + sa.text( + "select exists(select 1 from audit_events where action_id is not null " + f"or permission_id in ({_tokens(NEW_PERMISSIONS)}))" + ) + ).scalar_one() + if has_forward_evidence: + raise RuntimeError("cannot downgrade non-empty authorization action evidence") + + op.drop_constraint("authorization_action_evidence", "audit_events", type_="check") + op.drop_constraint("authority_registries", "audit_events", type_="check") + op.drop_constraint("authority_privacy_bounds", "audit_events", type_="check") + _create_registry_constraint(HISTORICAL_PERMISSIONS) + _create_privacy_constraint(HISTORICAL_PERMISSIONS) + op.drop_column("audit_events", "action_id") diff --git a/backend/app/modules/audit/schemas.py b/backend/app/modules/audit/schemas.py index 3433016f9..d7f88fb5c 100644 --- a/backend/app/modules/audit/schemas.py +++ b/backend/app/modules/audit/schemas.py @@ -10,6 +10,16 @@ from pydantic import BaseModel, ConfigDict, Field, model_validator +from app.modules.authorization.catalogue import ( + ACTION_BY_ID, + ACTION_IDS, + NEW_PERMISSION_IDS, + PERMISSION_IDS, + ActionAvailability, + ActionId, + PermissionId, +) + _ENTITY_TYPES = frozenset( { "actor_profile", @@ -29,21 +39,6 @@ _UUID_TARGET_KINDS = frozenset( {"actor_profile", "actor_identity_link", "admin_role_grant", "qualification_snapshot", "project_role_grant"} ) -_PERMISSIONS = frozenset( - """actor.profile.read_self actor.profile.update_self actor.profile.read_any - actor.profile.suspend actor.profile.reactivate actor.profile.deactivate - actor.identity_link.read actor.identity_link.revoke actor.identity_link.reactivate - actor.service.provision admin_role.read admin_role.grant admin_role.revoke project.create - project.read project.update project.archive project.guide.manage project.effective_policy.manage - project.task.manage project.review_policy.manage project.role_grant.read - project.role_grant.manage task.queue.read task.claim submission.create submission.read_own - submission.read_for_review review.queue.read review.queue.inspect review.claim review.release - review.decline_preference review.decision review.lease.force_release review.chain.read - contribution.read_self contribution.read_project compensation.policy.manage - compensation.adapter_binding.manage compensation.award.read compensation.delivery.reconcile - operations.status.read operations.timer.run operations.reconcile.run operations.outbox.retry - operations.projection.rebuild audit.read audit.export""".split() -) _DENIAL_CODES = frozenset( """required_scope_missing unsupported_subject_kind service_actor_not_provisioned identity_link_revoked actor_suspended actor_deactivated permission_not_granted @@ -247,7 +242,8 @@ class AuthorityAuditEventInput(BaseModel): target_actor_ref_kind: ActorReferenceKind | None = None target_actor_ref: Annotated[str, Field(max_length=120)] | None = None matched_grant_id: str | None = None - permission_id: str | None = None + permission_id: PermissionId | None = None + action_id: ActionId | None = None project_id: str | None = None resource_type: str | None = None resource_id: str | None = None @@ -298,7 +294,8 @@ def _inspect_privacy_safe_input(cls, value: object) -> dict | None: or kind is None or (kind == "system_principal" and actor_ref != "workstream:system:bootstrap") or (kind != "system_principal" and _uuid(actor_ref) is None) - or data.get("permission_id") is not None and not _registered(data["permission_id"], _PERMISSIONS) + or data.get("permission_id") is not None and not _registered(data["permission_id"], PERMISSION_IDS) + or data.get("action_id") is not None and not _registered(data["action_id"], ACTION_IDS) or data.get("denial_code") is not None and not _registered(data["denial_code"], _DENIAL_CODES) or data.get("resource_type") is not None and not _registered(data["resource_type"], _RESOURCE_TYPES) or data.get("target_ref_kind") is not None @@ -314,7 +311,7 @@ def _inspect_privacy_safe_input(cls, value: object) -> dict | None: ref = data.get(f"{prefix}_ref" if prefix == "invalidation_target" else f"{prefix}_id") invalid |= (ref_kind is None) != (ref is None) invalid |= _registered(ref_kind, _UUID_TARGET_KINDS) and ref is not None and _uuid(ref) is None - invalid |= ref_kind == "permission_registry" and not _registered(ref, _PERMISSIONS) + invalid |= ref_kind == "permission_registry" and not _registered(ref, PERMISSION_IDS) target_kind, target_ref = data.get("target_actor_ref_kind"), data.get("target_actor_ref") invalid |= (target_kind is None) != (target_ref is None) invalid |= target_kind is not None and ( @@ -322,6 +319,10 @@ def _inspect_privacy_safe_input(cls, value: object) -> dict | None: ) if invalid: return None + if data.get("permission_id") is not None: + data["permission_id"] = PermissionId(data["permission_id"]) + if data.get("action_id") is not None: + data["action_id"] = ActionId(data["action_id"]) data["before_facts"] = before_facts data["after_facts"] = after_facts return data @@ -363,9 +364,21 @@ def validate_shape(self) -> Self: if before and after and "scope_id" in before and before.get("scope_id") != after.get("scope_id"): raise ValueError("replacement cannot change project scope") invalidation = self.invalidation_cause_event_id is not None or self.invalidation_target_kind + action = ACTION_BY_ID.get(self.action_id) if self.action_id is not None else None + if action is not None and action.permission_id is not self.permission_id: + raise ValueError("action permission does not match catalogue") + if self.permission_id in NEW_PERMISSION_IDS and action is None: + raise ValueError("new permission requires registered action") + if self.action_id is not None and self.event_type not in { + AuthorityEventType.SENSITIVE_AUTHORIZATION_ALLOWED, + AuthorityEventType.SENSITIVE_AUTHORIZATION_DENIED, + }: + raise ValueError("action requires authorization decision event") if self.event_type == AuthorityEventType.SENSITIVE_AUTHORIZATION_ALLOWED: if self.permission_id is None or self.denial_code is not None or invalidation: raise ValueError("invalid allowed authorization evidence") + if action is not None and action.availability is ActionAvailability.PLANNED: + raise ValueError("planned action cannot produce allowed evidence") elif self.event_type == AuthorityEventType.SENSITIVE_AUTHORIZATION_DENIED: if self.permission_id is None or self.denial_code is None or invalidation or self.idempotency_reference: raise ValueError("invalid denied authorization evidence") diff --git a/backend/app/modules/authorization/catalogue.py b/backend/app/modules/authorization/catalogue.py new file mode 100644 index 000000000..41112d19b --- /dev/null +++ b/backend/app/modules/authorization/catalogue.py @@ -0,0 +1,362 @@ +"""Closed authorization identifiers and staged action metadata.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import StrEnum, unique +from types import MappingProxyType + + +@unique +class PermissionId(StrEnum): + """Closed product permission identifiers.""" + + ACTOR_PROFILE_READ_SELF = "actor.profile.read_self" + ACTOR_PROFILE_UPDATE_SELF = "actor.profile.update_self" + ACTOR_PROFILE_READ_ANY = "actor.profile.read_any" + ACTOR_PROFILE_SUSPEND = "actor.profile.suspend" + ACTOR_PROFILE_REACTIVATE = "actor.profile.reactivate" + ACTOR_PROFILE_DEACTIVATE = "actor.profile.deactivate" + ACTOR_IDENTITY_LINK_READ = "actor.identity_link.read" + ACTOR_IDENTITY_LINK_REVOKE = "actor.identity_link.revoke" + ACTOR_IDENTITY_LINK_REACTIVATE = "actor.identity_link.reactivate" + ACTOR_SERVICE_PROVISION = "actor.service.provision" + ADMIN_ROLE_READ = "admin_role.read" + ADMIN_ROLE_GRANT = "admin_role.grant" + ADMIN_ROLE_REVOKE = "admin_role.revoke" + PROJECT_CREATE = "project.create" + PROJECT_READ = "project.read" + PROJECT_UPDATE = "project.update" + PROJECT_ARCHIVE = "project.archive" + PROJECT_GUIDE_MANAGE = "project.guide.manage" + PROJECT_EFFECTIVE_POLICY_MANAGE = "project.effective_policy.manage" + PROJECT_TASK_MANAGE = "project.task.manage" + PROJECT_REVIEW_POLICY_MANAGE = "project.review_policy.manage" + PROJECT_ROLE_GRANT_READ = "project.role_grant.read" + PROJECT_ROLE_GRANT_MANAGE = "project.role_grant.manage" + TASK_QUEUE_READ = "task.queue.read" + TASK_CLAIM = "task.claim" + SUBMISSION_CREATE = "submission.create" + SUBMISSION_READ_OWN = "submission.read_own" + SUBMISSION_READ_FOR_REVIEW = "submission.read_for_review" + REVIEW_QUEUE_READ = "review.queue.read" + REVIEW_QUEUE_INSPECT = "review.queue.inspect" + REVIEW_CLAIM = "review.claim" + REVIEW_RELEASE = "review.release" + REVIEW_DECLINE_PREFERENCE = "review.decline_preference" + REVIEW_DECISION = "review.decision" + REVIEW_LEASE_FORCE_RELEASE = "review.lease.force_release" + REVIEW_CHAIN_READ = "review.chain.read" + CONTRIBUTION_READ_SELF = "contribution.read_self" + CONTRIBUTION_READ_PROJECT = "contribution.read_project" + COMPENSATION_POLICY_MANAGE = "compensation.policy.manage" + COMPENSATION_ADAPTER_BINDING_MANAGE = "compensation.adapter_binding.manage" + COMPENSATION_AWARD_READ = "compensation.award.read" + COMPENSATION_DELIVERY_RECONCILE = "compensation.delivery.reconcile" + OPERATIONS_STATUS_READ = "operations.status.read" + OPERATIONS_TIMER_RUN = "operations.timer.run" + OPERATIONS_RECONCILE_RUN = "operations.reconcile.run" + OPERATIONS_OUTBOX_RETRY = "operations.outbox.retry" + OPERATIONS_PROJECTION_REBUILD = "operations.projection.rebuild" + OPERATIONS_TASK_START_OVERRIDE = "operations.task.start_override" + OPERATIONS_SUBMISSION_GATE_REPAIR = "operations.submission_gate.repair" + OPERATIONS_CHECKER_RETRY = "operations.checker.retry" + ARTIFACT_BINDING_READ = "artifact.binding.read" + ARTIFACT_REPLICA_READ = "artifact.replica.read" + ARTIFACT_RECEIPT_READ = "artifact.receipt.read" + ARTIFACT_VERIFICATION_JOB_READ = "artifact.verification_job.read" + ARTIFACT_VERIFICATION_JOB_RETRY = "artifact.verification_job.retry" + ARTIFACT_RECOVERY_ATTEMPT_READ = "artifact.recovery_attempt.read" + ARTIFACT_AUDIT_READ = "artifact.audit.read" + ARTIFACT_GUIDE_SOURCE_INGEST = "artifact.guide_source.ingest" + ARTIFACT_UPLOAD_SESSION_CREATE = "artifact.upload_session.create" + ARTIFACT_UPLOAD_SESSION_READ = "artifact.upload_session.read" + ARTIFACT_UPLOAD_ITEM_WRITE = "artifact.upload_item.write" + ARTIFACT_UPLOAD_SESSION_SEAL = "artifact.upload_session.seal" + ARTIFACT_UPLOAD_SESSION_CANCEL = "artifact.upload_session.cancel" + ARTIFACT_UPLOAD_SESSION_EXPIRE = "artifact.upload_session.expire" + ARTIFACT_BINDING_CREATE = "artifact.binding.create" + ARTIFACT_VERIFICATION_EXECUTE = "artifact.verification.execute" + ARTIFACT_PENDING_WORK_SCAN = "artifact.pending_work.scan" + ARTIFACT_PUT_ATTEMPT_RESOLVE = "artifact.put_attempt.resolve" + ARTIFACT_GUIDE_SOURCE_READ = "artifact.guide_source.read" + ARTIFACT_CHECKER_INPUT_MATERIALIZE = "artifact.checker_input.materialize" + ARTIFACT_CHECKER_OUTPUT_WRITE = "artifact.checker_output.write" + AUDIT_READ = "audit.read" + AUDIT_EXPORT = "audit.export" + + +@unique +class ActionId(StrEnum): + """Closed action identifiers reserved by approved owner chunks.""" + + ACTOR_PROFILE_READ_SELF = "actor.profile.read_self" + ACTOR_PROFILE_UPDATE_SELF = "actor.profile.update_self" + OPERATIONS_TASK_START_OVERRIDE = "operations.task.start_override" + OPERATIONS_SUBMISSION_GATE_REPAIR = "operations.submission_gate.repair" + OPERATIONS_CHECKER_RETRY = "operations.checker.retry" + ARTIFACT_BINDING_READ = "artifact.binding.read" + ARTIFACT_REPLICA_READ = "artifact.replica.read" + ARTIFACT_RECEIPT_READ = "artifact.receipt.read" + ARTIFACT_VERIFICATION_JOB_READ = "artifact.verification_job.read" + ARTIFACT_VERIFICATION_JOB_RETRY = "artifact.verification_job.retry" + ARTIFACT_RECOVERY_ATTEMPT_READ = "artifact.recovery_attempt.read" + ARTIFACT_AUDIT_READ = "artifact.audit.read" + OPERATIONS_ARTIFACT_STORAGE_ADMISSION_READ = "operations.artifact_storage_admission.read" + ARTIFACT_GUIDE_SOURCE_INGEST = "artifact.guide_source.ingest" + ARTIFACT_GUIDE_SOURCE_READ = "artifact.guide_source.read" + ARTIFACT_UPLOAD_SESSION_CREATE = "artifact.upload_session.create" + ARTIFACT_UPLOAD_SESSION_READ = "artifact.upload_session.read" + ARTIFACT_UPLOAD_ITEM_WRITE = "artifact.upload_item.write" + ARTIFACT_UPLOAD_SESSION_SEAL = "artifact.upload_session.seal" + ARTIFACT_UPLOAD_SESSION_CANCEL = "artifact.upload_session.cancel" + ARTIFACT_UPLOAD_SESSION_EXPIRE = "artifact.upload_session.expire" + ARTIFACT_GUIDE_SOURCE_BINDING_CREATE = "artifact.guide_source.binding.create" + ARTIFACT_SUBMISSION_BINDING_CREATE = "artifact.submission.binding.create" + ARTIFACT_CHECKER_OUTPUT_BINDING_CREATE = "artifact.checker_output.binding.create" + ARTIFACT_VERIFICATION_EXECUTE = "artifact.verification.execute" + ARTIFACT_PENDING_WORK_SCAN = "artifact.pending_work.scan" + ARTIFACT_PUT_ATTEMPT_RESOLVE = "artifact.put_attempt.resolve" + ARTIFACT_PRE_SUBMIT_CHECKER_INPUT_MATERIALIZE = "artifact.pre_submit.checker_input.materialize" + ARTIFACT_POST_SUBMIT_CHECKER_INPUT_MATERIALIZE = ( + "artifact.post_submit.checker_input.materialize" + ) + ARTIFACT_CHECKER_OUTPUT_WRITE = "artifact.checker_output.write" + + +@unique +class ActionOwner(StrEnum): + """Closed implementation chunks allowed to activate reserved actions.""" + + AUTH_07B = "WS-AUTH-001-07B" + AUTH_13 = "WS-AUTH-001-13" + AUTH_14 = "WS-AUTH-001-14" + ART_02D = "WS-ART-001-02D" + ART_03 = "WS-ART-001-03" + ART_04A = "WS-ART-001-04A" + ART_04B = "WS-ART-001-04B" + ART_05 = "WS-ART-001-05" + ART_06A = "WS-ART-001-06A" + ART_06B = "WS-ART-001-06B" + + +@unique +class ActionAvailability(StrEnum): + """Whether an owning feature has activated an action.""" + + PLANNED = "planned" + ACTIVE = "active" + + +@dataclass(frozen=True, slots=True) +class ActionDefinition: + """Bounded action metadata; feature guards remain owner-defined.""" + + action_id: ActionId + permission_id: PermissionId + owner: ActionOwner + availability: ActionAvailability + + +def _planned( + action_id: ActionId, + permission_id: PermissionId, + owner: ActionOwner, +) -> ActionDefinition: + return ActionDefinition(action_id, permission_id, owner, ActionAvailability.PLANNED) + + +ACTION_DEFINITIONS = ( + _planned( + ActionId.ACTOR_PROFILE_READ_SELF, PermissionId.ACTOR_PROFILE_READ_SELF, ActionOwner.AUTH_07B + ), + _planned( + ActionId.ACTOR_PROFILE_UPDATE_SELF, + PermissionId.ACTOR_PROFILE_UPDATE_SELF, + ActionOwner.AUTH_07B, + ), + _planned( + ActionId.OPERATIONS_TASK_START_OVERRIDE, + PermissionId.OPERATIONS_TASK_START_OVERRIDE, + ActionOwner.AUTH_13, + ), + _planned( + ActionId.OPERATIONS_SUBMISSION_GATE_REPAIR, + PermissionId.OPERATIONS_SUBMISSION_GATE_REPAIR, + ActionOwner.AUTH_14, + ), + _planned( + ActionId.OPERATIONS_CHECKER_RETRY, + PermissionId.OPERATIONS_CHECKER_RETRY, + ActionOwner.AUTH_14, + ), + _planned( + ActionId.ARTIFACT_BINDING_READ, PermissionId.ARTIFACT_BINDING_READ, ActionOwner.ART_02D + ), + _planned( + ActionId.ARTIFACT_REPLICA_READ, PermissionId.ARTIFACT_REPLICA_READ, ActionOwner.ART_02D + ), + _planned( + ActionId.ARTIFACT_RECEIPT_READ, PermissionId.ARTIFACT_RECEIPT_READ, ActionOwner.ART_02D + ), + _planned( + ActionId.ARTIFACT_VERIFICATION_JOB_READ, + PermissionId.ARTIFACT_VERIFICATION_JOB_READ, + ActionOwner.ART_02D, + ), + _planned( + ActionId.ARTIFACT_VERIFICATION_JOB_RETRY, + PermissionId.ARTIFACT_VERIFICATION_JOB_RETRY, + ActionOwner.ART_02D, + ), + _planned( + ActionId.ARTIFACT_RECOVERY_ATTEMPT_READ, + PermissionId.ARTIFACT_RECOVERY_ATTEMPT_READ, + ActionOwner.ART_02D, + ), + _planned(ActionId.ARTIFACT_AUDIT_READ, PermissionId.ARTIFACT_AUDIT_READ, ActionOwner.ART_02D), + _planned( + ActionId.OPERATIONS_ARTIFACT_STORAGE_ADMISSION_READ, + PermissionId.OPERATIONS_STATUS_READ, + ActionOwner.ART_02D, + ), + _planned( + ActionId.ARTIFACT_GUIDE_SOURCE_INGEST, + PermissionId.ARTIFACT_GUIDE_SOURCE_INGEST, + ActionOwner.ART_03, + ), + _planned( + ActionId.ARTIFACT_GUIDE_SOURCE_READ, + PermissionId.ARTIFACT_GUIDE_SOURCE_READ, + ActionOwner.ART_03, + ), + _planned( + ActionId.ARTIFACT_UPLOAD_SESSION_CREATE, + PermissionId.ARTIFACT_UPLOAD_SESSION_CREATE, + ActionOwner.ART_04A, + ), + _planned( + ActionId.ARTIFACT_UPLOAD_SESSION_READ, + PermissionId.ARTIFACT_UPLOAD_SESSION_READ, + ActionOwner.ART_04A, + ), + _planned( + ActionId.ARTIFACT_UPLOAD_ITEM_WRITE, + PermissionId.ARTIFACT_UPLOAD_ITEM_WRITE, + ActionOwner.ART_04A, + ), + _planned( + ActionId.ARTIFACT_UPLOAD_SESSION_SEAL, + PermissionId.ARTIFACT_UPLOAD_SESSION_SEAL, + ActionOwner.ART_04A, + ), + _planned( + ActionId.ARTIFACT_UPLOAD_SESSION_CANCEL, + PermissionId.ARTIFACT_UPLOAD_SESSION_CANCEL, + ActionOwner.ART_04A, + ), + _planned( + ActionId.ARTIFACT_UPLOAD_SESSION_EXPIRE, + PermissionId.ARTIFACT_UPLOAD_SESSION_EXPIRE, + ActionOwner.ART_04A, + ), + _planned( + ActionId.ARTIFACT_GUIDE_SOURCE_BINDING_CREATE, + PermissionId.ARTIFACT_BINDING_CREATE, + ActionOwner.ART_03, + ), + _planned( + ActionId.ARTIFACT_SUBMISSION_BINDING_CREATE, + PermissionId.ARTIFACT_BINDING_CREATE, + ActionOwner.ART_05, + ), + _planned( + ActionId.ARTIFACT_CHECKER_OUTPUT_BINDING_CREATE, + PermissionId.ARTIFACT_BINDING_CREATE, + ActionOwner.ART_06B, + ), + _planned( + ActionId.ARTIFACT_VERIFICATION_EXECUTE, + PermissionId.ARTIFACT_VERIFICATION_EXECUTE, + ActionOwner.ART_02D, + ), + _planned( + ActionId.ARTIFACT_PENDING_WORK_SCAN, + PermissionId.ARTIFACT_PENDING_WORK_SCAN, + ActionOwner.ART_02D, + ), + _planned( + ActionId.ARTIFACT_PUT_ATTEMPT_RESOLVE, + PermissionId.ARTIFACT_PUT_ATTEMPT_RESOLVE, + ActionOwner.ART_02D, + ), + _planned( + ActionId.ARTIFACT_PRE_SUBMIT_CHECKER_INPUT_MATERIALIZE, + PermissionId.ARTIFACT_CHECKER_INPUT_MATERIALIZE, + ActionOwner.ART_04B, + ), + _planned( + ActionId.ARTIFACT_POST_SUBMIT_CHECKER_INPUT_MATERIALIZE, + PermissionId.ARTIFACT_CHECKER_INPUT_MATERIALIZE, + ActionOwner.ART_06A, + ), + _planned( + ActionId.ARTIFACT_CHECKER_OUTPUT_WRITE, + PermissionId.ARTIFACT_CHECKER_OUTPUT_WRITE, + ActionOwner.ART_06B, + ), +) + +PERMISSION_IDS = frozenset(PermissionId) +ACTION_IDS = frozenset(ActionId) +HISTORICAL_PERMISSION_IDS = frozenset( + permission + for permission in PermissionId + if not permission.value.startswith("artifact.") + and permission + not in { + PermissionId.OPERATIONS_TASK_START_OVERRIDE, + PermissionId.OPERATIONS_SUBMISSION_GATE_REPAIR, + PermissionId.OPERATIONS_CHECKER_RETRY, + } +) +NEW_PERMISSION_IDS = PERMISSION_IDS - HISTORICAL_PERMISSION_IDS + + +def _index_actions( + definitions: tuple[ActionDefinition, ...], +) -> MappingProxyType[ActionId, ActionDefinition]: + if any( + not isinstance(definition, ActionDefinition) + or not isinstance(definition.action_id, ActionId) + or not isinstance(definition.permission_id, PermissionId) + or not isinstance(definition.owner, ActionOwner) + or not isinstance(definition.availability, ActionAvailability) + for definition in definitions + ): + raise RuntimeError("authorization action catalogue contains an invalid row") + indexed = {definition.action_id: definition for definition in definitions} + if len(PERMISSION_IDS) != 73 or len(ACTION_IDS) != 30: + raise RuntimeError("authorization catalogue count mismatch") + if len(indexed) != len(definitions) or set(indexed) != ACTION_IDS: + raise RuntimeError("authorization action catalogue is incomplete") + if len(HISTORICAL_PERMISSION_IDS) != 49 or len(NEW_PERMISSION_IDS) != 24: + raise RuntimeError("authorization permission boundary mismatch") + if any(definition.availability is not ActionAvailability.PLANNED for definition in definitions): + raise RuntimeError("AUTH-07A actions must remain planned") + if set(definitions) != set(ACTION_DEFINITIONS): + raise RuntimeError("authorization action metadata mismatch") + if {definition.owner for definition in definitions} != set(ActionOwner): + raise RuntimeError("authorization action owner catalogue is incomplete") + return MappingProxyType(indexed) + + +ACTION_BY_ID = _index_actions(ACTION_DEFINITIONS) + + +def resolve_executable_action(action_id: ActionId) -> ActionDefinition: + """Return active metadata and fail closed for planned actions.""" + definition = ACTION_BY_ID[action_id] + if definition.availability is not ActionAvailability.ACTIVE: + raise ValueError("authorization action is not active") + return definition diff --git a/backend/app/modules/tasks/models.py b/backend/app/modules/tasks/models.py index 64f137048..988da3ab4 100644 --- a/backend/app/modules/tasks/models.py +++ b/backend/app/modules/tasks/models.py @@ -486,6 +486,7 @@ class AuditEvent(Base): target_actor_ref: Mapped[str | None] = mapped_column(String(100)) matched_grant_id: Mapped[str | None] = mapped_column(String(100)) permission_id: Mapped[str | None] = mapped_column(String(120)) + action_id: Mapped[str | None] = mapped_column(String(160)) project_id: Mapped[str | None] = mapped_column(String(36)) resource_type: Mapped[str | None] = mapped_column(String(80)) resource_id: Mapped[str | None] = mapped_column(String(100)) diff --git a/backend/tests/test_alembic.py b/backend/tests/test_alembic.py index 04f0ca61b..6cc1ea0c4 100644 --- a/backend/tests/test_alembic.py +++ b/backend/tests/test_alembic.py @@ -17,6 +17,7 @@ from sqlalchemy.ext.asyncio import create_async_engine from app.adapters.auth.dev import actor_id_from_external_identity +from app.modules.authorization.catalogue import ACTION_DEFINITIONS from app.modules.actors.legacy_classification import ( CLASSIFICATION_FILE_ENV, LegacyActorClassification, @@ -426,7 +427,7 @@ def test_canonical_actor_downgrade_refuses_nonactive_authority_state( ): command.downgrade(config, "0019_authority_idempotency") assert asyncio.run(_current_revision(isolated_database_env)) == ( - "0020_canonical_actor_profile" + "0021_auth_action_evidence" ) asyncio.run( _reset_canonical_actor_guard_state( @@ -440,6 +441,83 @@ def test_canonical_actor_downgrade_refuses_nonactive_authority_state( command.downgrade(config, "base") +def test_authorization_action_evidence_constraints_and_guarded_downgrade( + isolated_database_env: str, + migration_lock, +) -> None: + """Prove exact action parity, rollback custody, and downgrade locking.""" + config = _alembic_config() + with migration_lock(): + try: + command.downgrade(config, "base") + command.upgrade(config, "head") + schema = asyncio.run(_authorization_action_schema(isolated_database_env)) + asyncio.run(_assert_authorization_action_sql_pairs(isolated_database_env)) + + action_event = asyncio.run( + _insert_authorization_action_event(isolated_database_env) + ) + with pytest.raises( + RuntimeError, + match="^cannot downgrade non-empty authorization action evidence$", + ): + command.downgrade(config, "0020_canonical_actor_profile") + asyncio.run( + _remove_authorization_action_events( + isolated_database_env, [action_event] + ) + ) + + permission_event = asyncio.run( + _insert_authorization_action_event(isolated_database_env) + ) + asyncio.run( + _convert_to_permission_only_forward_evidence( + isolated_database_env, permission_event + ) + ) + with pytest.raises( + RuntimeError, + match="^cannot downgrade non-empty authorization action evidence$", + ): + command.downgrade(config, "0020_canonical_actor_profile") + asyncio.run( + _remove_authorization_action_events( + isolated_database_env, [permission_event] + ) + ) + command.downgrade(config, "0020_canonical_actor_profile") + downgraded = asyncio.run( + _authorization_action_schema(isolated_database_env) + ) + command.upgrade(config, "head") + + lock_observed, raced_event = _action_downgrade_waits_for_insert( + config, isolated_database_env + ) + asyncio.run( + _remove_authorization_action_events( + isolated_database_env, [raced_event] + ) + ) + command.downgrade(config, "0020_canonical_actor_profile") + command.upgrade(config, "head") + finally: + command.downgrade(config, "base") + + assert schema == { + "revision": "0021_auth_action_evidence", + "action_column": True, + "action_constraint": True, + } + assert downgraded == { + "revision": "0020_canonical_actor_profile", + "action_column": False, + "action_constraint": False, + } + assert lock_observed is True + + def test_artifact_foundation_upgrade_preserves_prior_head_and_promotes_nothing( isolated_database_env: str, migration_lock, @@ -3485,3 +3563,268 @@ async def _remove_authority_idempotency_fixture( ) finally: await engine.dispose() + + +_ACTION_EVIDENCE_INSERT = text( + "insert into audit_events " + "(id, entity_type, entity_id, event_type, actor_id, actor_roles, claim_snapshot, " + "auth_source, is_dev_auth, event_payload, event_domain, event_version, actor_ref_kind, " + "request_id, correlation_id, permission_id, action_id, reason, denial_code, after_facts) " + "values (:id, 'authorization_decision', :id, 'SensitiveAuthorizationDenied', " + "'workstream:system:bootstrap', '[]'::json, '{}'::json, 'local_authority', false, " + "'{}'::json, 'authority', 1, 'system_principal', :request_id, :correlation_id, " + ":permission_id, :action_id, 'authorization_evaluation', 'permission_not_granted', " + "'{\"allowed\": false}'::json)" +) + + +def _action_evidence_values(action_id: str, permission_id: str) -> dict[str, str]: + event_id = str(uuid4()) + return { + "id": event_id, + "request_id": str(uuid4()), + "correlation_id": str(uuid4()), + "permission_id": permission_id, + "action_id": action_id, + } + + +async def _authorization_action_schema(database_url: str) -> dict[str, object]: + """Return the migration revision and action-evidence schema markers.""" + engine = create_async_engine(database_url) + try: + async with engine.connect() as connection: + return { + "revision": await connection.scalar( + text("select version_num from alembic_version") + ), + "action_column": bool( + await connection.scalar( + text( + "select exists(select 1 from information_schema.columns " + "where table_schema='public' and table_name='audit_events' " + "and column_name='action_id')" + ) + ) + ), + "action_constraint": bool( + await connection.scalar( + text( + "select exists(select 1 from pg_constraint where " + "conrelid='audit_events'::regclass and " + "conname='ck_audit_events_authorization_action_evidence')" + ) + ) + ), + } + finally: + await engine.dispose() + + +async def _assert_authorization_action_sql_pairs(database_url: str) -> None: + """Prove all exact pairs persist as denied evidence and invalid pairs fail.""" + engine = create_async_engine(database_url) + try: + for definition in ACTION_DEFINITIONS: + async with engine.connect() as connection: + transaction = await connection.begin() + await connection.execute( + _ACTION_EVIDENCE_INSERT, + _action_evidence_values( + definition.action_id.value, definition.permission_id.value + ), + ) + await transaction.rollback() + + invalid = ( + _action_evidence_values("unknown.action", "actor.profile.read_self"), + _action_evidence_values("artifact.binding.read", "artifact.replica.read"), + _action_evidence_values("artifact.binding.read", "actor.profile.read_self"), + ) + for values in invalid: + async with engine.connect() as connection: + transaction = await connection.begin() + with pytest.raises(IntegrityError): + await connection.execute(_ACTION_EVIDENCE_INSERT, values) + await transaction.rollback() + + missing_action = _action_evidence_values( + "artifact.binding.read", "artifact.binding.read" + ) + missing_action["action_id"] = None # type: ignore[assignment] + async with engine.connect() as connection: + transaction = await connection.begin() + with pytest.raises(IntegrityError): + await connection.execute(_ACTION_EVIDENCE_INSERT, missing_action) + await transaction.rollback() + + nondecision = text( + "insert into audit_events " + "(id, entity_type, entity_id, event_type, actor_id, actor_roles, claim_snapshot, " + "auth_source, is_dev_auth, event_payload, event_domain, event_version, " + "actor_ref_kind, request_id, correlation_id, permission_id, action_id, reason, " + "denial_code) values (:id, 'admin_role_grant', :entity_id, " + "'AdminRoleGrantIssueDenied', 'workstream:system:bootstrap', '[]'::json, " + "'{}'::json, 'local_authority', false, '{}'::json, 'authority', 1, " + "'system_principal', :request_id, :correlation_id, 'actor.profile.read_self', " + "'actor.profile.read_self', 'authorization_policy_denial', " + "'permission_not_granted')" + ) + async with engine.connect() as connection: + transaction = await connection.begin() + with pytest.raises(IntegrityError): + await connection.execute( + nondecision, + { + "id": str(uuid4()), + "entity_id": str(uuid4()), + "request_id": str(uuid4()), + "correlation_id": str(uuid4()), + }, + ) + await transaction.rollback() + finally: + await engine.dispose() + + +async def _insert_authorization_action_event(database_url: str) -> str: + """Commit one valid planned-action denial fixture.""" + values = _action_evidence_values("artifact.binding.read", "artifact.binding.read") + engine = create_async_engine(database_url) + try: + async with engine.begin() as connection: + await connection.execute(_ACTION_EVIDENCE_INSERT, values) + return values["id"] + finally: + await engine.dispose() + + +async def _convert_to_permission_only_forward_evidence( + database_url: str, event_id: str +) -> None: + """Simulate a pre-guard forward row to exercise the second rollback predicate.""" + engine = create_async_engine(database_url) + try: + async with engine.begin() as connection: + await connection.execute(text("lock table audit_events in access exclusive mode")) + await connection.execute( + text( + "alter table audit_events disable trigger " + "audit_events_reject_update_delete" + ) + ) + await connection.execute( + text( + "alter table audit_events drop constraint " + "ck_audit_events_authorization_action_evidence" + ) + ) + await connection.execute( + text("update audit_events set action_id=null where id=:id"), + {"id": event_id}, + ) + await connection.execute( + text( + "alter table audit_events add constraint " + "ck_audit_events_authorization_action_evidence " + "check (action_id is null) not valid" + ) + ) + await connection.execute( + text( + "alter table audit_events enable trigger " + "audit_events_reject_update_delete" + ) + ) + finally: + await engine.dispose() + + +async def _remove_authorization_action_events( + database_url: str, event_ids: list[str] +) -> None: + """Owner-only cleanup for immutable action-evidence test fixtures.""" + engine = create_async_engine(database_url) + try: + async with engine.begin() as connection: + await connection.execute(text("lock table audit_events in access exclusive mode")) + await connection.execute( + text( + "alter table audit_events disable trigger " + "audit_events_reject_update_delete" + ) + ) + await connection.execute( + text("delete from audit_events where id = any(:ids)"), + {"ids": event_ids}, + ) + await connection.execute( + text( + "alter table audit_events enable trigger " + "audit_events_reject_update_delete" + ) + ) + finally: + await engine.dispose() + + +def _action_downgrade_waits_for_insert( + config: Config, database_url: str +) -> tuple[bool, str]: + """Prove an insert cannot pass between the downgrade check and destructive DDL.""" + writer_ready = threading.Event() + release_writer = threading.Event() + values = _action_evidence_values("artifact.binding.read", "artifact.binding.read") + + async def hold_uncommitted_insert() -> None: + engine = create_async_engine(database_url) + try: + async with engine.connect() as connection: + transaction = await connection.begin() + await connection.execute(_ACTION_EVIDENCE_INSERT, values) + writer_ready.set() + await asyncio.to_thread(release_writer.wait) + await transaction.commit() + finally: + await engine.dispose() + + async def observe_downgrade_lock() -> bool: + engine = create_async_engine(database_url) + try: + async with engine.connect() as connection: + for _ in range(5000): + waiting = await connection.scalar( + text( + "select exists(select 1 from pg_locks locks " + "join pg_class relation on relation.oid=locks.relation " + "where relation.relname='audit_events' " + "and locks.mode='AccessExclusiveLock' and not locks.granted)" + ) + ) + if waiting: + return True + await asyncio.sleep(0) + return False + finally: + await engine.dispose() + + with ThreadPoolExecutor(max_workers=2) as executor: + writer = executor.submit(asyncio.run, hold_uncommitted_insert()) + if not writer_ready.wait(timeout=5): + release_writer.set() + writer.result(timeout=5) + return False, values["id"] + downgrade = executor.submit( + command.downgrade, config, "0020_canonical_actor_profile" + ) + try: + observed = asyncio.run(observe_downgrade_lock()) + finally: + release_writer.set() + writer.result(timeout=10) + with pytest.raises( + RuntimeError, + match="^cannot downgrade non-empty authorization action evidence$", + ): + downgrade.result(timeout=10) + return observed, values["id"] diff --git a/backend/tests/test_audit.py b/backend/tests/test_audit.py index caa738c29..9604d938f 100644 --- a/backend/tests/test_audit.py +++ b/backend/tests/test_audit.py @@ -23,6 +23,7 @@ AuthorityEventType, ) from app.modules.audit.service import AuditService +from app.modules.authorization.catalogue import ActionId, PermissionId from app.modules.tasks.models import AuditEvent @@ -134,6 +135,76 @@ def _authority_input(event_type: AuthorityEventType, **overrides) -> AuthorityAu return AuthorityAuditEventInput(**values) +def test_action_aware_audit_input_enforces_mapping_and_planned_availability() -> None: + denied = _authority_input( + AuthorityEventType.SENSITIVE_AUTHORIZATION_DENIED, + permission_id="artifact.binding.read", + action_id="artifact.binding.read", + denial_code="permission_not_granted", + ) + assert denied.action_id is ActionId.ARTIFACT_BINDING_READ + assert denied.permission_id is PermissionId.ARTIFACT_BINDING_READ + + with pytest.raises(ValidationError, match="action permission"): + _authority_input( + AuthorityEventType.SENSITIVE_AUTHORIZATION_DENIED, + permission_id="artifact.replica.read", + action_id="artifact.binding.read", + denial_code="permission_not_granted", + ) + with pytest.raises(ValidationError, match="new permission requires"): + _authority_input( + AuthorityEventType.SENSITIVE_AUTHORIZATION_DENIED, + permission_id="artifact.binding.read", + action_id=None, + denial_code="permission_not_granted", + ) + with pytest.raises(ValidationError, match="planned action"): + _authority_input( + AuthorityEventType.SENSITIVE_AUTHORIZATION_ALLOWED, + permission_id="artifact.binding.read", + action_id="artifact.binding.read", + ) + with pytest.raises(TypeError, match="invalid authority audit input"): + _authority_input( + AuthorityEventType.SENSITIVE_AUTHORIZATION_DENIED, + permission_id="artifact.binding.read", + action_id="unknown.action", + denial_code="permission_not_granted", + ) + event_id = uuid4() + with pytest.raises(ValidationError, match="action requires authorization decision"): + AuthorityAuditEventInput( + event_id=event_id, + event_type=AuthorityEventType.ADMIN_ROLE_GRANT_ISSUE_DENIED, + entity_type="admin_role_grant", + entity_id=str(uuid4()), + actor_ref_kind=ActorReferenceKind.SYSTEM_PRINCIPAL, + actor_ref="workstream:system:bootstrap", + request_id=uuid4(), + correlation_id=uuid4(), + permission_id="actor.profile.read_self", + action_id="actor.profile.read_self", + reason="authorization_policy_denial", + denial_code="permission_not_granted", + ) + + +async def test_planned_action_denial_persists_with_bounded_mapping(audit_factory) -> None: + value = _authority_input( + AuthorityEventType.SENSITIVE_AUTHORIZATION_DENIED, + permission_id="artifact.binding.read", + action_id="artifact.binding.read", + denial_code="permission_not_granted", + ) + async with audit_factory() as session: + stored = await AuditService(session).add_authority_event(value) + await session.commit() + + assert stored.action_id == "artifact.binding.read" + assert stored.permission_id == "artifact.binding.read" + + def _authority_event_matrix() -> list[dict]: """Return every closed event with one valid and one fact-invalid shape.""" project_id = str(uuid4()) @@ -555,6 +626,7 @@ def __repr__(self): ({"reason": "secret-bearer-value"}, secret), ({"entity_type": "secret-bearer-value"}, secret), ({"permission_id": [secret]}, secret), + ({"action_id": [secret]}, secret), ): candidate = safe | patch for constructor in constructors: diff --git a/backend/tests/test_authorization.py b/backend/tests/test_authorization.py index d162948ea..9521c6b6f 100644 --- a/backend/tests/test_authorization.py +++ b/backend/tests/test_authorization.py @@ -15,6 +15,21 @@ from app.modules.audit.schemas import ActorReferenceKind, AuthorityAuditEventInput, AuthorityEventType from app.modules.audit.service import AuditService +from app.modules.authorization.catalogue import ( + ACTION_BY_ID, + ACTION_DEFINITIONS, + ACTION_IDS, + HISTORICAL_PERMISSION_IDS, + NEW_PERMISSION_IDS, + PERMISSION_IDS, + ActionAvailability, + ActionDefinition, + ActionId, + ActionOwner, + PermissionId, + _index_actions, + resolve_executable_action, +) from app.modules.authorization.schemas import ( ActorIdentityLinkReactivateRequest, ActorIdentityLinkRevokeRequest, @@ -50,6 +65,119 @@ DIGEST = "sha256:" + "a" * 64 +def test_closed_permission_and_action_catalogue_is_exact_and_non_executable() -> None: + expected = { + "actor.profile.read_self": ("actor.profile.read_self", "WS-AUTH-001-07B"), + "actor.profile.update_self": ("actor.profile.update_self", "WS-AUTH-001-07B"), + "operations.task.start_override": ("operations.task.start_override", "WS-AUTH-001-13"), + "operations.submission_gate.repair": ("operations.submission_gate.repair", "WS-AUTH-001-14"), + "operations.checker.retry": ("operations.checker.retry", "WS-AUTH-001-14"), + "artifact.binding.read": ("artifact.binding.read", "WS-ART-001-02D"), + "artifact.replica.read": ("artifact.replica.read", "WS-ART-001-02D"), + "artifact.receipt.read": ("artifact.receipt.read", "WS-ART-001-02D"), + "artifact.verification_job.read": ("artifact.verification_job.read", "WS-ART-001-02D"), + "artifact.verification_job.retry": ("artifact.verification_job.retry", "WS-ART-001-02D"), + "artifact.recovery_attempt.read": ("artifact.recovery_attempt.read", "WS-ART-001-02D"), + "artifact.audit.read": ("artifact.audit.read", "WS-ART-001-02D"), + "operations.artifact_storage_admission.read": ("operations.status.read", "WS-ART-001-02D"), + "artifact.guide_source.ingest": ("artifact.guide_source.ingest", "WS-ART-001-03"), + "artifact.guide_source.read": ("artifact.guide_source.read", "WS-ART-001-03"), + "artifact.upload_session.create": ("artifact.upload_session.create", "WS-ART-001-04A"), + "artifact.upload_session.read": ("artifact.upload_session.read", "WS-ART-001-04A"), + "artifact.upload_item.write": ("artifact.upload_item.write", "WS-ART-001-04A"), + "artifact.upload_session.seal": ("artifact.upload_session.seal", "WS-ART-001-04A"), + "artifact.upload_session.cancel": ("artifact.upload_session.cancel", "WS-ART-001-04A"), + "artifact.upload_session.expire": ("artifact.upload_session.expire", "WS-ART-001-04A"), + "artifact.guide_source.binding.create": ("artifact.binding.create", "WS-ART-001-03"), + "artifact.submission.binding.create": ("artifact.binding.create", "WS-ART-001-05"), + "artifact.checker_output.binding.create": ("artifact.binding.create", "WS-ART-001-06B"), + "artifact.verification.execute": ("artifact.verification.execute", "WS-ART-001-02D"), + "artifact.pending_work.scan": ("artifact.pending_work.scan", "WS-ART-001-02D"), + "artifact.put_attempt.resolve": ("artifact.put_attempt.resolve", "WS-ART-001-02D"), + "artifact.pre_submit.checker_input.materialize": ( + "artifact.checker_input.materialize", + "WS-ART-001-04B", + ), + "artifact.post_submit.checker_input.materialize": ( + "artifact.checker_input.materialize", + "WS-ART-001-06A", + ), + "artifact.checker_output.write": ("artifact.checker_output.write", "WS-ART-001-06B"), + } + assert len(PERMISSION_IDS) == 73 + assert len(HISTORICAL_PERMISSION_IDS) == 49 + assert len(NEW_PERMISSION_IDS) == 24 + assert len(ACTION_IDS) == len(ACTION_DEFINITIONS) == len(ACTION_BY_ID) == 30 + assert set(ACTION_BY_ID) == ACTION_IDS + assert {definition.owner for definition in ACTION_DEFINITIONS} == set(ActionOwner) + assert all( + definition.availability is ActionAvailability.PLANNED + for definition in ACTION_DEFINITIONS + ) + assert { + definition.action_id.value: ( + definition.permission_id.value, + definition.owner.value, + ) + for definition in ACTION_DEFINITIONS + } == expected + with pytest.raises(ValueError, match="not active"): + resolve_executable_action(ActionId.ACTOR_PROFILE_READ_SELF) + with pytest.raises(TypeError): + ACTION_BY_ID[ActionId.ACTOR_PROFILE_READ_SELF] = ACTION_DEFINITIONS[0] + + +@pytest.mark.parametrize( + "definitions, message", + [ + (ACTION_DEFINITIONS[:-1], "incomplete"), + (ACTION_DEFINITIONS[:-1] + (ACTION_DEFINITIONS[0],), "incomplete"), + ( + ACTION_DEFINITIONS[:-1] + + ( + ActionDefinition( + ActionId.ARTIFACT_CHECKER_OUTPUT_WRITE, + PermissionId.ARTIFACT_CHECKER_OUTPUT_WRITE, + ActionOwner.ART_02D, + ActionAvailability.PLANNED, + ), + ), + "metadata mismatch", + ), + ( + ACTION_DEFINITIONS[:-1] + + ( + ActionDefinition( + ActionId.ARTIFACT_CHECKER_OUTPUT_WRITE, + PermissionId.ARTIFACT_CHECKER_OUTPUT_WRITE, + ActionOwner.ART_06B, + ActionAvailability.ACTIVE, + ), + ), + "must remain planned", + ), + ( + ACTION_DEFINITIONS[:-1] + + ( + ActionDefinition( + ActionId.ARTIFACT_CHECKER_OUTPUT_WRITE, + "unknown.permission", # type: ignore[arg-type] + ActionOwner.ART_06B, + ActionAvailability.PLANNED, + ), + ), + "invalid row", + ), + ], +) +def test_action_catalogue_construction_fails_closed( + definitions: tuple[ActionDefinition, ...], + message: str, +) -> None: + with pytest.raises(RuntimeError, match=message): + _index_actions(definitions) + + @pytest.fixture def authorization_database_env(postgres_database_url: str, migration_lock) -> str: """Ensure authorization tests run at the current isolated schema head.""" diff --git a/docs/operations_authorization_service.md b/docs/operations_authorization_service.md index 4374cfcbc..6cf48f27f 100644 --- a/docs/operations_authorization_service.md +++ b/docs/operations_authorization_service.md @@ -486,9 +486,16 @@ to skip or relax catalogue checks. Migration `0021` preserves historical audit rows with null `action_id`. Inspect non-null action evidence only by bounded ActionId, request/correlation IDs, and resource references; do not export event payloads or actor identity-link data -for routine diagnosis. Downgrade is allowed only while every action ID remains -null. If action-aware evidence exists, stop and recover forward rather than -discarding it. +for routine diagnosis. Every action must carry its catalogue-mapped PermissionId, +and every permission added after migration `0018` must carry one of its mapped +actions. Planned actions can record bounded denial evidence but cannot record an +allowed decision through the typed writer. + +Downgrade is allowed only while every action ID remains null and no permission +outside migration `0018`'s historical 49-value set exists. The migration takes +an exclusive audit-table lock before both checks and keeps it through +destructive DDL. If either kind of forward evidence exists, stop and recover +forward rather than discarding it. AUTH-07B later activates only canonical actor self-read and self-update. Admin definition reads wait for AUTH-08 grant truth, and project capability context diff --git a/docs/spec_authorization_service.md b/docs/spec_authorization_service.md index de5927adb..5da72ac80 100644 --- a/docs/spec_authorization_service.md +++ b/docs/spec_authorization_service.md @@ -224,6 +224,12 @@ AUTH-07A also reserves `actor.profile.read_self` and later supplies their complete active definitions and self-route behavior proof without changing migration `0021`. +Migration `0021` is availability-neutral. PostgreSQL enforces the closed +ActionId set, authorization-decision event shape, exact ActionId-to-PermissionId +mapping, and the requirement that every post-`0018` permission carry a mapped +action. Typed catalogue validation separately rejects allowed evidence until an +owning chunk changes an action from `planned` to `active`. + The paired artifact activation matrix is closed: | Owning WS-ART chunk | Actions activated by that chunk | From 478a819236b9cff1e1d7b61203015691ce0aaf45 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 15 Jul 2026 11:39:30 +0100 Subject: [PATCH 08/19] Strengthen AUTH-07A rollback evidence --- ...-07A-closed-permission-action-catalogue.md | 8 +- .../0021_authorization_action_evidence.py | 6 +- backend/tests/test_alembic.py | 243 ++++++++++++++++-- backend/tests/test_authorization.py | 72 +++++- docs/operations_authorization_service.md | 9 +- 5 files changed, 309 insertions(+), 29 deletions(-) diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md index ab3938558..322a37f8d 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md @@ -143,9 +143,11 @@ owned by later chunks and are not registered here. rows in typed code without altering migration `0021`. - Existing non-action authority events remain valid with null `action_id`. - Downgrade refuses when either any non-null action evidence or any PermissionId - outside the historical 49-value set exists. A clean database satisfying both - predicates can downgrade, restores the exact prior 49-value permission - constraint, drops the action column/constraint, and re-upgrades. + outside the historical 49-value set exists in `permission_id`, a permission- + registry target reference, or a permission-registry invalidation reference. A + clean database satisfying all predicates can downgrade, restores the exact + prior 49-value permission constraint, drops the action column/constraint, and + re-upgrades. - Downgrade takes `LOCK audit_events IN ACCESS EXCLUSIVE MODE` before checking both refusal predicates or changing constraints. Deterministic independent- session proof shows a concurrent insert cannot pass between the checks and diff --git a/backend/alembic/versions/0021_authorization_action_evidence.py b/backend/alembic/versions/0021_authorization_action_evidence.py index 1babe2703..dbe622478 100644 --- a/backend/alembic/versions/0021_authorization_action_evidence.py +++ b/backend/alembic/versions/0021_authorization_action_evidence.py @@ -257,7 +257,11 @@ def downgrade() -> None: has_forward_evidence = bind.execute( sa.text( "select exists(select 1 from audit_events where action_id is not null " - f"or permission_id in ({_tokens(NEW_PERMISSIONS)}))" + f"or permission_id in ({_tokens(NEW_PERMISSIONS)}) " + "or (target_ref_kind = 'permission_registry' " + f"and target_ref_id in ({_tokens(NEW_PERMISSIONS)})) " + "or (invalidation_target_kind = 'permission_registry' " + f"and invalidation_target_ref in ({_tokens(NEW_PERMISSIONS)})))" ) ).scalar_one() if has_forward_evidence: diff --git a/backend/tests/test_alembic.py b/backend/tests/test_alembic.py index 6cc1ea0c4..6deb5eb00 100644 --- a/backend/tests/test_alembic.py +++ b/backend/tests/test_alembic.py @@ -17,7 +17,11 @@ from sqlalchemy.ext.asyncio import create_async_engine from app.adapters.auth.dev import actor_id_from_external_identity -from app.modules.authorization.catalogue import ACTION_DEFINITIONS +from app.modules.authorization.catalogue import ( + ACTION_DEFINITIONS, + HISTORICAL_PERMISSION_IDS, + NEW_PERMISSION_IDS, +) from app.modules.actors.legacy_classification import ( CLASSIFICATION_FILE_ENV, LegacyActorClassification, @@ -447,11 +451,20 @@ def test_authorization_action_evidence_constraints_and_guarded_downgrade( ) -> None: """Prove exact action parity, rollback custody, and downgrade locking.""" config = _alembic_config() + historical_event = str(uuid4()) with migration_lock(): try: command.downgrade(config, "base") + command.upgrade(config, "0020_canonical_actor_profile") + asyncio.run(_insert_authority_audit_fixture(isolated_database_env, historical_event)) + historical_before = asyncio.run( + _authorization_action_row(isolated_database_env, historical_event) + ) command.upgrade(config, "head") schema = asyncio.run(_authorization_action_schema(isolated_database_env)) + historical_upgraded = asyncio.run( + _authorization_action_row(isolated_database_env, historical_event) + ) asyncio.run(_assert_authorization_action_sql_pairs(isolated_database_env)) action_event = asyncio.run( @@ -486,10 +499,56 @@ def test_authorization_action_evidence_constraints_and_guarded_downgrade( isolated_database_env, [permission_event] ) ) + + target_reference_event = asyncio.run( + _insert_forward_permission_reference( + isolated_database_env, + historical_event, + reference_field="target", + ) + ) + with pytest.raises( + RuntimeError, + match="^cannot downgrade non-empty authorization action evidence$", + ): + command.downgrade(config, "0020_canonical_actor_profile") + asyncio.run( + _remove_authorization_action_events( + isolated_database_env, [target_reference_event] + ) + ) + + invalidation_reference_event = asyncio.run( + _insert_forward_permission_reference( + isolated_database_env, + historical_event, + reference_field="invalidation", + ) + ) + with pytest.raises( + RuntimeError, + match="^cannot downgrade non-empty authorization action evidence$", + ): + command.downgrade(config, "0020_canonical_actor_profile") + asyncio.run( + _remove_authorization_action_events( + isolated_database_env, [invalidation_reference_event] + ) + ) + command.downgrade(config, "0020_canonical_actor_profile") downgraded = asyncio.run( _authorization_action_schema(isolated_database_env) ) + historical_downgraded = asyncio.run( + _authorization_action_row(isolated_database_env, historical_event) + ) + asyncio.run(_assert_historical_permission_registry(isolated_database_env)) + asyncio.run( + _remove_authorization_action_events( + isolated_database_env, [historical_event] + ) + ) command.upgrade(config, "head") lock_observed, raced_event = _action_downgrade_waits_for_insert( @@ -515,6 +574,11 @@ def test_authorization_action_evidence_constraints_and_guarded_downgrade( "action_column": False, "action_constraint": False, } + assert historical_before == historical_upgraded == historical_downgraded == { + "event_type": "SensitiveAuthorizationAllowed", + "permission_id": "actor.profile.read_any", + "action_id": None, + } assert lock_observed is True @@ -3578,7 +3642,9 @@ async def _remove_authority_idempotency_fixture( ) -def _action_evidence_values(action_id: str, permission_id: str) -> dict[str, str]: +def _action_evidence_values( + action_id: str | None, permission_id: str +) -> dict[str, str | None]: event_id = str(uuid4()) return { "id": event_id, @@ -3621,6 +3687,41 @@ async def _authorization_action_schema(database_url: str) -> dict[str, object]: await engine.dispose() +async def _authorization_action_row( + database_url: str, event_id: str +) -> dict[str, object]: + """Fetch stable action evidence across both sides of migration 0021.""" + engine = create_async_engine(database_url) + try: + async with engine.connect() as connection: + has_action = await connection.scalar( + text( + "select exists(select 1 from information_schema.columns " + "where table_schema='public' and table_name='audit_events' " + "and column_name='action_id')" + ) + ) + action_column = ", action_id" if has_action else "" + row = ( + ( + await connection.execute( + text( + "select event_type, permission_id" + f"{action_column} from audit_events where id=:id" + ), + {"id": event_id}, + ) + ) + .mappings() + .one() + ) + result = dict(row) + result.setdefault("action_id", None) + return result + finally: + await engine.dispose() + + async def _assert_authorization_action_sql_pairs(database_url: str) -> None: """Prove all exact pairs persist as denied evidence and invalid pairs fail.""" engine = create_async_engine(database_url) @@ -3636,27 +3737,30 @@ async def _assert_authorization_action_sql_pairs(database_url: str) -> None: ) await transaction.rollback() - invalid = ( - _action_evidence_values("unknown.action", "actor.profile.read_self"), - _action_evidence_values("artifact.binding.read", "artifact.replica.read"), - _action_evidence_values("artifact.binding.read", "actor.profile.read_self"), - ) - for values in invalid: + unknown_action = _action_evidence_values("unknown.action", "actor.profile.read_self") + async with engine.connect() as connection: + transaction = await connection.begin() + with pytest.raises(IntegrityError): + await connection.execute(_ACTION_EVIDENCE_INSERT, unknown_action) + await transaction.rollback() + + for definition in ACTION_DEFINITIONS: + wrong_permission = _action_evidence_values( + definition.action_id.value, "actor.profile.read_any" + ) async with engine.connect() as connection: transaction = await connection.begin() with pytest.raises(IntegrityError): - await connection.execute(_ACTION_EVIDENCE_INSERT, values) + await connection.execute(_ACTION_EVIDENCE_INSERT, wrong_permission) await transaction.rollback() - missing_action = _action_evidence_values( - "artifact.binding.read", "artifact.binding.read" - ) - missing_action["action_id"] = None # type: ignore[assignment] - async with engine.connect() as connection: - transaction = await connection.begin() - with pytest.raises(IntegrityError): - await connection.execute(_ACTION_EVIDENCE_INSERT, missing_action) - await transaction.rollback() + for permission in NEW_PERMISSION_IDS: + missing_action = _action_evidence_values(None, permission.value) + async with engine.connect() as connection: + transaction = await connection.begin() + with pytest.raises(IntegrityError): + await connection.execute(_ACTION_EVIDENCE_INSERT, missing_action) + await transaction.rollback() nondecision = text( "insert into audit_events " @@ -3740,6 +3844,109 @@ async def _convert_to_permission_only_forward_evidence( await engine.dispose() +async def _insert_forward_permission_reference( + database_url: str, + cause_event_id: str, + *, + reference_field: str, +) -> str: + """Commit one new permission-registry reference without an action ID.""" + event_id = str(uuid4()) + values = { + "id": event_id, + "request_id": str(uuid4()), + "correlation_id": str(uuid4()), + "permission": "artifact.binding.read", + "cause_id": cause_event_id, + } + if reference_field == "target": + statement = text( + "insert into audit_events " + "(id, entity_type, entity_id, event_type, actor_id, actor_roles, " + "claim_snapshot, auth_source, is_dev_auth, event_payload, event_domain, " + "event_version, actor_ref_kind, request_id, correlation_id, permission_id, " + "target_ref_kind, target_ref_id, reason, after_facts) values " + "(:id, 'authorization_decision', :id, 'SensitiveAuthorizationAllowed', " + "'workstream:system:bootstrap', '[]'::json, '{}'::json, 'local_authority', " + "false, '{}'::json, 'authority', 1, 'system_principal', :request_id, " + ":correlation_id, 'actor.profile.read_any', 'permission_registry', " + ":permission, 'authorization_evaluation', '{\"allowed\": true}'::json)" + ) + elif reference_field == "invalidation": + statement = text( + "insert into audit_events " + "(id, entity_type, entity_id, event_type, actor_id, actor_roles, " + "claim_snapshot, auth_source, is_dev_auth, event_payload, event_domain, " + "event_version, actor_ref_kind, request_id, correlation_id, " + "invalidation_cause_event_id, invalidation_target_kind, " + "invalidation_target_ref, reason, before_facts, after_facts) values " + "(:id, 'authority_invalidation', :id, 'AuthorityInvalidationRequested', " + "'workstream:system:bootstrap', '[]'::json, '{}'::json, 'local_authority', " + "false, '{}'::json, 'authority', 1, 'system_principal', :request_id, " + ":correlation_id, :cause_id, 'permission_registry', :permission, " + "'authority_state_changed', '{\"effective\": true}'::json, " + "'{\"effective\": false}'::json)" + ) + else: + raise ValueError(f"unsupported reference field: {reference_field}") + + engine = create_async_engine(database_url) + try: + async with engine.begin() as connection: + if reference_field == "invalidation": + await connection.execute( + text( + "alter table audit_events disable trigger " + "audit_events_validate_idempotency" + ) + ) + await connection.execute(statement, values) + if reference_field == "invalidation": + await connection.execute( + text( + "alter table audit_events enable trigger " + "audit_events_validate_idempotency" + ) + ) + return event_id + finally: + await engine.dispose() + + +async def _assert_historical_permission_registry(database_url: str) -> None: + """Prove downgrade restores every historical permission and rejects every new one.""" + statement = text( + "insert into audit_events " + "(id, entity_type, entity_id, event_type, actor_id, actor_roles, claim_snapshot, " + "auth_source, is_dev_auth, event_payload, event_domain, event_version, " + "actor_ref_kind, request_id, correlation_id, permission_id, reason, after_facts) " + "values (:id, 'authorization_decision', :id, 'SensitiveAuthorizationAllowed', " + "'workstream:system:bootstrap', '[]'::json, '{}'::json, 'local_authority', false, " + "'{}'::json, 'authority', 1, 'system_principal', :request_id, :correlation_id, " + ":permission_id, 'authorization_evaluation', '{\"allowed\": true}'::json)" + ) + engine = create_async_engine(database_url) + try: + for permission in HISTORICAL_PERMISSION_IDS: + async with engine.connect() as connection: + transaction = await connection.begin() + await connection.execute( + statement, _action_evidence_values(None, permission.value) + ) + await transaction.rollback() + + for permission in NEW_PERMISSION_IDS: + async with engine.connect() as connection: + transaction = await connection.begin() + with pytest.raises(IntegrityError): + await connection.execute( + statement, _action_evidence_values(None, permission.value) + ) + await transaction.rollback() + finally: + await engine.dispose() + + async def _remove_authorization_action_events( database_url: str, event_ids: list[str] ) -> None: diff --git a/backend/tests/test_authorization.py b/backend/tests/test_authorization.py index 9521c6b6f..b396419af 100644 --- a/backend/tests/test_authorization.py +++ b/backend/tests/test_authorization.py @@ -66,6 +66,35 @@ def test_closed_permission_and_action_catalogue_is_exact_and_non_executable() -> None: + historical_permissions = frozenset( + """actor.profile.read_self actor.profile.update_self actor.profile.read_any + actor.profile.suspend actor.profile.reactivate actor.profile.deactivate + actor.identity_link.read actor.identity_link.revoke actor.identity_link.reactivate + actor.service.provision admin_role.read admin_role.grant admin_role.revoke + project.create project.read project.update project.archive project.guide.manage + project.effective_policy.manage project.task.manage project.review_policy.manage + project.role_grant.read project.role_grant.manage task.queue.read task.claim + submission.create submission.read_own submission.read_for_review review.queue.read + review.queue.inspect review.claim review.release review.decline_preference + review.decision review.lease.force_release review.chain.read contribution.read_self + contribution.read_project compensation.policy.manage + compensation.adapter_binding.manage compensation.award.read + compensation.delivery.reconcile operations.status.read operations.timer.run + operations.reconcile.run operations.outbox.retry operations.projection.rebuild + audit.read audit.export""".split() + ) + new_permissions = frozenset( + """operations.task.start_override operations.submission_gate.repair + operations.checker.retry artifact.binding.read artifact.replica.read + artifact.receipt.read artifact.verification_job.read + artifact.verification_job.retry artifact.recovery_attempt.read artifact.audit.read + artifact.guide_source.ingest artifact.upload_session.create + artifact.upload_session.read artifact.upload_item.write artifact.upload_session.seal + artifact.upload_session.cancel artifact.upload_session.expire artifact.binding.create + artifact.verification.execute artifact.pending_work.scan artifact.put_attempt.resolve + artifact.guide_source.read artifact.checker_input.materialize + artifact.checker_output.write""".split() + ) expected = { "actor.profile.read_self": ("actor.profile.read_self", "WS-AUTH-001-07B"), "actor.profile.update_self": ("actor.profile.update_self", "WS-AUTH-001-07B"), @@ -104,9 +133,9 @@ def test_closed_permission_and_action_catalogue_is_exact_and_non_executable() -> ), "artifact.checker_output.write": ("artifact.checker_output.write", "WS-ART-001-06B"), } - assert len(PERMISSION_IDS) == 73 - assert len(HISTORICAL_PERMISSION_IDS) == 49 - assert len(NEW_PERMISSION_IDS) == 24 + assert {item.value for item in HISTORICAL_PERMISSION_IDS} == historical_permissions + assert {item.value for item in NEW_PERMISSION_IDS} == new_permissions + assert {item.value for item in PERMISSION_IDS} == historical_permissions | new_permissions assert len(ACTION_IDS) == len(ACTION_DEFINITIONS) == len(ACTION_BY_ID) == 30 assert set(ACTION_BY_ID) == ACTION_IDS assert {definition.owner for definition in ACTION_DEFINITIONS} == set(ActionOwner) @@ -132,6 +161,7 @@ def test_closed_permission_and_action_catalogue_is_exact_and_non_executable() -> [ (ACTION_DEFINITIONS[:-1], "incomplete"), (ACTION_DEFINITIONS[:-1] + (ACTION_DEFINITIONS[0],), "incomplete"), + (ACTION_DEFINITIONS + (ACTION_DEFINITIONS[0],), "incomplete"), ( ACTION_DEFINITIONS[:-1] + ( @@ -168,6 +198,42 @@ def test_closed_permission_and_action_catalogue_is_exact_and_non_executable() -> ), "invalid row", ), + ( + ACTION_DEFINITIONS[:-1] + + ( + ActionDefinition( + "unknown.action", # type: ignore[arg-type] + PermissionId.ARTIFACT_CHECKER_OUTPUT_WRITE, + ActionOwner.ART_06B, + ActionAvailability.PLANNED, + ), + ), + "invalid row", + ), + ( + ACTION_DEFINITIONS[:-1] + + ( + ActionDefinition( + ActionId.ARTIFACT_CHECKER_OUTPUT_WRITE, + PermissionId.ARTIFACT_CHECKER_OUTPUT_WRITE, + "unknown.owner", # type: ignore[arg-type] + ActionAvailability.PLANNED, + ), + ), + "invalid row", + ), + ( + ACTION_DEFINITIONS[:-1] + + ( + ActionDefinition( + ActionId.ARTIFACT_CHECKER_OUTPUT_WRITE, + PermissionId.ARTIFACT_CHECKER_OUTPUT_WRITE, + ActionOwner.ART_06B, + "unknown.availability", # type: ignore[arg-type] + ), + ), + "invalid row", + ), ], ) def test_action_catalogue_construction_fails_closed( diff --git a/docs/operations_authorization_service.md b/docs/operations_authorization_service.md index 6cf48f27f..6467d6c27 100644 --- a/docs/operations_authorization_service.md +++ b/docs/operations_authorization_service.md @@ -492,10 +492,11 @@ actions. Planned actions can record bounded denial evidence but cannot record an allowed decision through the typed writer. Downgrade is allowed only while every action ID remains null and no permission -outside migration `0018`'s historical 49-value set exists. The migration takes -an exclusive audit-table lock before both checks and keeps it through -destructive DDL. If either kind of forward evidence exists, stop and recover -forward rather than discarding it. +outside migration `0018`'s historical 49-value set exists in the decision, +target-reference, or invalidation-reference fields. The migration takes an +exclusive audit-table lock before these checks and keeps it through destructive +DDL. If any forward evidence exists, stop and recover forward rather than +discarding it. AUTH-07B later activates only canonical actor self-read and self-update. Admin definition reads wait for AUTH-08 grant truth, and project capability context From f35f71b375f4498e8d7943dcb674bdbb910a8f2a Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 15 Jul 2026 11:52:33 +0100 Subject: [PATCH 09/19] Record AUTH-07A internal review state --- .agent-loop/LOOP_STATE.md | 6 +++--- .agent-loop/REVIEW_LOG.md | 12 ++++++++++++ .agent-loop/WORK_QUEUE.md | 2 +- .../CHUNK_MAP.md | 2 +- .../STATUS.md | 13 +++++++------ .agent-loop/merge-intents/WS-AUTH-001-07A.json | 9 +++++++++ 6 files changed, 33 insertions(+), 11 deletions(-) create mode 100644 .agent-loop/merge-intents/WS-AUTH-001-07A.json diff --git a/.agent-loop/LOOP_STATE.md b/.agent-loop/LOOP_STATE.md index 2c696192d..e778f91be 100644 --- a/.agent-loop/LOOP_STATE.md +++ b/.agent-loop/LOOP_STATE.md @@ -17,9 +17,9 @@ - PR #122 merged the first automated post-merge memory implementation as `fc89fb6`; its schema-v1 cross-initiative next pointer is superseded by the schema-v2 initiative-local clean cut. -- Current gate: AUTH-07A's repaired L1 contract passed every required - preimplementation review at `beb85ac`; bounded runtime implementation may - start. +- Current gate: AUTH-07A implementation and repair passed every required + internal reviewer track at `478a819`; deterministic evidence is complete and + PR publication is pending. - Scope checkpoint: AWS S3 is the only v0.1 production provider; MinIO is local/CI S3 protocol proof; LocalStorage is focused development/test; R2 and Flow Node are deferred. Product modules receive narrow artifact capabilities, diff --git a/.agent-loop/REVIEW_LOG.md b/.agent-loop/REVIEW_LOG.md index c052520c0..11b013717 100644 --- a/.agent-loop/REVIEW_LOG.md +++ b/.agent-loop/REVIEW_LOG.md @@ -1,5 +1,17 @@ # Review Log +## 2026-07-15 - WS-AUTH-001-07A Internal Review Passed + +Exact implementation SHA `478a819236b9cff1e1d7b61203015691ce0aaf45` +passed senior engineering, architecture/reuse, security/auth, product/ops, +docs, QA/test, test-delta, and CI-integrity review after all valid findings were +repaired. The final downgrade guard covers action evidence, new permissions, +permission-registry target references, and permission-registry invalidation +references under one exclusive table lock. Focused authorization/audit branch +coverage is 94/93 percent, all 37 focused behavior tests pass, and the full +isolated Alembic suite passes 16 tests at exact migration head. PR publication +is pending; AUTH-07B remains inactive. + ## 2026-07-15 - WS-AUTH-001-07A Repaired Plan Passed Exact-SHA `beb85ac` passed senior engineering, architecture/reuse, diff --git a/.agent-loop/WORK_QUEUE.md b/.agent-loop/WORK_QUEUE.md index 268ed1f60..e667a4048 100644 --- a/.agent-loop/WORK_QUEUE.md +++ b/.agent-loop/WORK_QUEUE.md @@ -4,7 +4,7 @@ | Chunk | Title | Risk | Status | |---|---|---:|---| -| `WS-AUTH-001-07A` | Closed Permission And Action Catalogue | L1 | Repaired contract passed required L1 plan review at `beb85ac`; bounded implementation active | +| `WS-AUTH-001-07A` | Closed Permission And Action Catalogue | L1 | Implementation and repair internally approved at `478a819`; PR publication pending | ## Planned Next diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/CHUNK_MAP.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/CHUNK_MAP.md index b60128cb8..61646a675 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/CHUNK_MAP.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/CHUNK_MAP.md @@ -24,7 +24,7 @@ stopped. | `WS-AUTH-001-05B` | Authority Idempotency And Invalidation Foundation | L1 | Merged through PR #119 as `ad71c7e` | | `WS-AUTH-001-06` | Canonical Actor Profile And Identity Link | L1 | Merged through PR #124 as `f599551` | | `WS-AUTH-001-07` | Authorization Kernel And Permission Registry | L1 | Split before implementation after required L1 plan review | -| `WS-AUTH-001-07A` | Closed Permission And Action Catalogue | L1 | Repaired L1 contract approved at `beb85ac`; implementation active | +| `WS-AUTH-001-07A` | Closed Permission And Action Catalogue | L1 | Implementation and repair internally approved at `478a819`; PR pending | | `WS-AUTH-001-07B` | Deny-By-Default Kernel And Self-Action Cutover | L1 | Inactive until 07A merge/memory and explicit user start | | `WS-AUTH-001-08` | Bootstrap And Administrative Role Grants | L1 | Proposed | | `WS-AUTH-001-09` | Actor State, Identity Revocation, And Service Actors | L1 | Proposed | diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md index 4cbf450e4..4a694f36f 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md @@ -68,12 +68,13 @@ internal and external checks; explicit human approval merged PR #124 as `f599551` on 2026-07-15. Signed automated memory stopped with AUTH-07 requiring a separate start. The user explicitly started AUTH-07 on 2026-07-15; discovery and required L1 plan review are complete. The repaired AUTH-07A contract passed -all required tracks at `beb85ac`; bounded implementation is active. +all required tracks at `beb85ac`; implementation, repair, deterministic +evidence, and required internal review pass at `478a819`. That review rejected the combined AUTH-07 contract before runtime edits because grant-backed/project APIs preceded their authority sources and the audit/API ownership files were incomplete. Parent AUTH-07 is now split into 07A catalogue -and audit parity, followed by 07B kernel and actor self-action cutover. The -repaired 07A contract is under required plan review. +and audit parity, followed by 07B kernel and actor self-action cutover. AUTH-07A +is ready for PR publication; AUTH-07B remains inactive. ## Active planning chunk @@ -104,7 +105,7 @@ None. | `WS-AUTH-001-05B` | Merged | `codex/ws-auth-001-05b-idempotency-invalidation` | #119 | Merged as `ad71c7e`; reviewed runtime SHA `e083890`. | | `WS-AUTH-001-06` | Merged | `codex/ws-auth-001-06-canonical-actor-profile` | #124 | Merged as `f599551`; final PR head `4a2193f`. | | `WS-AUTH-001-07` | Split | `codex/ws-auth-001-07-authorization-kernel` | - | Required L1 review rejected the combined contract before runtime edits. | -| `WS-AUTH-001-07A` | Implementing | `codex/ws-auth-001-07-authorization-kernel` | - | Repaired contract approved at `beb85ac`; closed catalogue and action-aware audit parity only. | +| `WS-AUTH-001-07A` | Internally approved | `codex/ws-auth-001-07-authorization-kernel` | - | Reviewed implementation `478a819`; closed catalogue and action-aware audit parity only. | | `WS-AUTH-001-07B` | Proposed | - | - | Inactive until 07A merge/memory and explicit user start. | | `WS-AUTH-001-08` | Proposed | - | - | Bootstrap and administrative grants. | | `WS-AUTH-001-09` | Proposed | - | - | Actor/link states and service actors. | @@ -137,8 +138,8 @@ behavior ownership. `WS-AUTH-001-CAT` retains only safe registry/conformance rules. This is a scope decision, not an AUTH-05B runtime blocker. PR #118, AUTH-05B PR #119, and AUTH-06 PR #124 are merged. AUTH-07 has an explicit user start. Required review split it before runtime implementation; -the repaired 07A contract passed all required tracks at `beb85ac` and bounded -implementation is active. AUTH-07B and +the repaired 07A contract passed all required tracks at `beb85ac`; implementation +and repair passed at `478a819`. AUTH-07B and POL-002-04 remain inactive until separate explicit human starts and their authorization prerequisites. diff --git a/.agent-loop/merge-intents/WS-AUTH-001-07A.json b/.agent-loop/merge-intents/WS-AUTH-001-07A.json new file mode 100644 index 000000000..334b8bcee --- /dev/null +++ b/.agent-loop/merge-intents/WS-AUTH-001-07A.json @@ -0,0 +1,9 @@ +{ + "chunk_id": "WS-AUTH-001-07A", + "chunk_title": "Closed Permission And Action Catalogue", + "initiative_id": "WS-AUTH-001", + "next_chunk_id": "WS-AUTH-001-07B", + "next_chunk_title": "Deny-By-Default Kernel And Self-Action Cutover", + "next_requires_explicit_start": true, + "schema_version": 2 +} From 5d97b87e858d843cedff1b8866b349cde656b612 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 15 Jul 2026 12:00:46 +0100 Subject: [PATCH 10/19] Record AUTH-07A review evidence --- ...S-AUTH-001-07A-internal-review-evidence.md | 66 ++++++++++++++ .../WS-AUTH-001-07A-pr-trust-bundle.md | 90 +++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 .agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md create mode 100644 .agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md new file mode 100644 index 000000000..b53a454b1 --- /dev/null +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md @@ -0,0 +1,66 @@ +# WS-AUTH-001-07A Internal Review Evidence + +Reviewed code SHA: `f35f71b375f4498e8d7943dcb674bdbb910a8f2a` +Reviewed runtime SHA: `478a819236b9cff1e1d7b61203015691ce0aaf45` +Reviewed at: `2026-07-15T10:58:49Z` +Reviewer run IDs: `auth06_final_ci`, `auth06_final_docs`, +`auth06_final_test_delta` + +## Deterministic Evidence + +- The focused authorization and audit suite passed all 37 collected behavior + tests against isolated PostgreSQL migration head `0021_auth_action_evidence`. +- Branch-aware subsystem coverage is 94 percent for authorization and 93 + percent for audit, above the required 90 percent threshold for materially + changed backend subsystems. +- The exact catalogue/startup matrix passed 10 tests covering the independent + 49 historical and 24 new PermissionId sets, exact 30-action mapping, missing, + duplicate, extra, and hostile typed rows, immutability, and planned-action + non-executability. +- The complete isolated Alembic suite passed 16 tests in 587.24 seconds at + runtime SHA `478a819`. It proves upgrade/downgrade/re-upgrade, historical-row + preservation, exact restored permission behavior, all forward-evidence + refusal paths, and the concurrent insert lock. +- Direct SQL accepts all 30 exact action/permission pairs as denied evidence, + rejects all 30 wrong registered-permission pairs, and rejects all 24 new + permissions without a mapped action. +- Ruff, stale Workstream wording, stale authorization documentation, changed + Markdown links, loop-memory state, and diff integrity passed. +- No workflow, dependency, test skip, coverage exclusion, package script, or + threshold changed. GitHub Backend remains authoritative for the repository- + wide 78 percent floor. + +## Reviewer Results + +| Reviewer | Result | Blocking findings | Notes | +|---|---|---|---| +| senior engineering | PASS AFTER FIXES | none | Closed catalogue, migration custody, and bounded test helpers are maintainable. | +| qa/test | PASS AFTER FIXES | none | Exact typed and SQL matrices, historical preservation, rollback paths, and concurrency are covered. | +| security/auth | PASS AFTER FIXES | none | No action is active; unknown or mismatched identifiers and unsafe rollback fail closed. | +| product/ops | PASS | none | No contributor, reviewer, project, artifact, or workflow capability is activated. | +| architecture | PASS | none | AUTH-07A remains a dependency-free catalogue and audit boundary; kernel and resource composition remain deferred. | +| ci integrity | PASS | none | CI, thresholds, dependencies, exclusions, and skips are unchanged. | +| docs | PASS | none | Contract, specification, operations, lifecycle, and rollback language match runtime behavior. | +| reuse/dedup | PASS | none | Audit validation consumes the single catalogue instead of retaining a second permission registry. | +| test delta | PASS AFTER FIXES | none | Assertions independently prove exact sets and exhaustive negative behavior without weakening prior tests. | + +## Findings Resolved + +Review repair added downgrade custody for new permissions stored in target and +invalidation permission-registry references. It also replaced count-only proof +with independent exact permission sets, made SQL negative coverage exhaustive, +completed startup failure cases, and proved a populated `0020` row survives +upgrade and clean downgrade with null action evidence. + +The final exact-head review confirmed that `478a819..f35f71b` contains only six +lifecycle/merge-intent files, records the evidence accurately, and keeps +`WS-AUTH-001-07B` inactive until merge, signed memory, and explicit human start. + +Valid findings addressed: yes + +Open sub-agent sessions: none + +## Remaining Gate + +GitHub Backend, Agent Gates, CodeRabbit, and explicit human merge approval remain +pending. Do not start `WS-AUTH-001-07B` automatically. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md new file mode 100644 index 000000000..fea309b7c --- /dev/null +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md @@ -0,0 +1,90 @@ +# WS-AUTH-001-07A PR Trust Bundle + +## Chunk + +`WS-AUTH-001-07A` - Closed Permission And Action Catalogue + +## Goal + +Install one closed typed source for all 73 approved PermissionIds and exactly 30 +planned ActionIds, then preserve exact typed/PostgreSQL audit parity without +making any action executable. + +## What Changed + +- Added a dependency-free authorization catalogue containing the exact 49 + historical and 24 new permissions plus 30 four-field planned action rows. +- Made authority audit validation consume the catalogue, enforce exact action- + permission mapping, reject planned allowed decisions, and bound hostile input. +- Added nullable `audit_events.action_id` and PostgreSQL constraints for exact + registry, mapping, decision-event, and new-permission evidence parity. +- Added guarded rollback custody under an exclusive table lock for action IDs + and new permissions stored as decisions, target references, or invalidation + references. +- Added exhaustive typed and PostgreSQL behavior tests and updated the canonical + specification, operations runbook, and future owner contracts. + +## Design Boundaries + +All actions remain `planned`. This PR does not implement a decision evaluator, +grants, principals, resource loaders, composers, guards, caches, route cutovers, +artifact operations, or public permission APIs. AUTH-07B later owns only actor +self-read/self-update kernel activation after this PR merges and a separate +human start. AUTH-08 and AUTH-10 remain the authority prerequisites for admin +and project contexts. + +## Acceptance Proof + +- The independent runtime test literals prove the exact 73-value permission set + and its exact 49/24 historical/new partition. +- Catalogue startup rejects missing, duplicate, extra, unknown, invalid-owner, + invalid-availability, and metadata-mismatched rows. +- Planned ActionIds cannot resolve as executable and cannot be recorded as + allowed decisions through the typed audit boundary. +- Direct SQL proves every exact pair, every wrong pair, every new permission + without action evidence, unknown actions, and non-decision action use. +- Migration proof preserves populated historical evidence, restores the exact + historical registry on downgrade, and blocks every forward-evidence path + before destructive DDL. + +## Tests And Coverage + +- 37 focused authorization/audit behavior tests passed on isolated PostgreSQL. +- Authorization branch-aware coverage: 94 percent. +- Audit branch-aware coverage: 93 percent. +- Full isolated Alembic suite: 16 passed in 587.24 seconds at reviewed runtime + SHA `478a819236b9cff1e1d7b61203015691ce0aaf45`. +- Catalogue/startup matrix: 10 passed. +- Ruff, stale wording, authorization-doc consistency, Markdown links, + loop-memory state, and diff integrity passed. + +GitHub Backend remains authoritative for the repository-wide 78 percent floor; +this PR does not change any coverage threshold or exclusion. + +## Reviewer Results + +Reviewed lifecycle head `f35f71b375f4498e8d7943dcb674bdbb910a8f2a` +and runtime SHA `478a819236b9cff1e1d7b61203015691ce0aaf45` passed +senior engineering, QA/test, security/auth, product/ops, architecture, CI +integrity, docs, reuse/dedup, and test-delta review with no remaining findings. + +## Remaining Risks + +- Database-owner credentials can bypass normal constraints and triggers; + production runtime credentials must remain non-owner. +- Downgrade is intentionally unavailable after any forward action or new- + permission evidence exists; recovery must proceed forward. +- Availability changes are typed-code owner decisions. PostgreSQL deliberately + remains availability-neutral so later owner chunks do not rewrite migration + `0021`. + +## Human Review Focus + +Review the exact 73/30 catalogue, planned-only typed behavior, exact SQL mapping, +historical audit preservation, four downgrade refusal paths, exclusive locking, +and the absence of kernel/grant/resource activation. + +## Human Merge Ownership + +Only the human may approve and merge this PR. GitHub checks, CodeRabbit, and +internal review do not authorize merge. From 6287f57936c5e1ec7621bcbf07ff45ee40f0ee91 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 15 Jul 2026 12:30:05 +0100 Subject: [PATCH 11/19] Address AUTH-07A external review --- ...S-AUTH-001-07A-external-review-response.md | 53 +++++++++++++++++++ .../0021_authorization_action_evidence.py | 1 + backend/tests/test_alembic.py | 25 ++++++++- backend/tests/test_audit.py | 15 +++--- docs/spec_authorization_service.md | 2 +- 5 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 .agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-external-review-response.md diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-external-review-response.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-external-review-response.md new file mode 100644 index 000000000..e55cac1d6 --- /dev/null +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-external-review-response.md @@ -0,0 +1,53 @@ +# External Review Response: WS-AUTH-001-07A + +## Pull Request + +PR #126 - Add closed authorization action catalogue and audit parity + +CodeRabbit run: `920d63e5-f8b3-4525-9e61-18b44373ed1d` + +## Comments Addressed + +1. Corrected the specification compound noun from `self actions` to + `self-actions`. +2. Made the approved availability boundary explicit in code and behavior + evidence. Typed validation rejects allowed evidence for every one of the 30 + currently planned actions. PostgreSQL accepts exact registered allowed pairs + because migration `0021` is intentionally availability-neutral across later + owner activation. +3. Expanded the PR description to the repository trust-bundle structure. + +## Comment Declined + +CodeRabbit proposed restricting migration `0021` to denied action evidence. +That would contradict the approved AUTH-07A contract and freeze temporary +`planned` availability into a permanent database migration. AUTH-07B must be +able to activate its two self-actions through reviewed typed code without an +unowned migration `0022`. PostgreSQL therefore owns stable identifier, exact +mapping, and decision-event shape; typed catalogue validation owns temporal +availability. The direct SQL and typed tests now prove both sides of that +boundary for all 30 actions. + +## Non-Actionable Review Output + +CodeRabbit's diff-local docstring percentage is not a configured repository +gate. Existing public behavior and complex helpers retain useful docstrings; +narration-only docstrings were not added. + +## Commands Rerun + +```text +pytest focused catalogue/audit behavior +pytest targeted authorization action-evidence migration +ruff check changed backend files +python3 scripts/test_agent_gates.py +python3 scripts/check_internal_review_evidence.py +python3 scripts/check_loop_memory_state.py +python3 scripts/check_stale_workstream_wording.py +python3 scripts/check_stale_authorization_docs.py +python3 scripts/check_markdown_links.py +git diff --check +``` + +No GitHub thread is replied to or resolved by this evidence file. Thread writes +remain a separate explicit action after the repaired commit is pushed. diff --git a/backend/alembic/versions/0021_authorization_action_evidence.py b/backend/alembic/versions/0021_authorization_action_evidence.py index dbe622478..f496270e1 100644 --- a/backend/alembic/versions/0021_authorization_action_evidence.py +++ b/backend/alembic/versions/0021_authorization_action_evidence.py @@ -221,6 +221,7 @@ def upgrade() -> None: _create_registry_constraint(PERMISSIONS) _create_privacy_constraint(PERMISSIONS) + # Availability is typed lifecycle state; SQL remains stable across owner activation. pair_tokens = _pair_tokens() op.create_check_constraint( "authorization_action_evidence", diff --git a/backend/tests/test_alembic.py b/backend/tests/test_alembic.py index 6deb5eb00..c4035df23 100644 --- a/backend/tests/test_alembic.py +++ b/backend/tests/test_alembic.py @@ -3641,6 +3641,18 @@ async def _remove_authority_idempotency_fixture( "'{\"allowed\": false}'::json)" ) +_ALLOWED_ACTION_EVIDENCE_INSERT = text( + "insert into audit_events " + "(id, entity_type, entity_id, event_type, actor_id, actor_roles, claim_snapshot, " + "auth_source, is_dev_auth, event_payload, event_domain, event_version, actor_ref_kind, " + "request_id, correlation_id, permission_id, action_id, reason, after_facts) " + "values (:id, 'authorization_decision', :id, 'SensitiveAuthorizationAllowed', " + "'workstream:system:bootstrap', '[]'::json, '{}'::json, 'local_authority', false, " + "'{}'::json, 'authority', 1, 'system_principal', :request_id, :correlation_id, " + ":permission_id, :action_id, 'authorization_evaluation', " + "'{\"allowed\": true}'::json)" +) + def _action_evidence_values( action_id: str | None, permission_id: str @@ -3723,7 +3735,7 @@ async def _authorization_action_row( async def _assert_authorization_action_sql_pairs(database_url: str) -> None: - """Prove all exact pairs persist as denied evidence and invalid pairs fail.""" + """Prove exact pair closure without freezing typed availability in SQL.""" engine = create_async_engine(database_url) try: for definition in ACTION_DEFINITIONS: @@ -3737,6 +3749,17 @@ async def _assert_authorization_action_sql_pairs(database_url: str) -> None: ) await transaction.rollback() + for definition in ACTION_DEFINITIONS: + async with engine.connect() as connection: + transaction = await connection.begin() + await connection.execute( + _ALLOWED_ACTION_EVIDENCE_INSERT, + _action_evidence_values( + definition.action_id.value, definition.permission_id.value + ), + ) + await transaction.rollback() + unknown_action = _action_evidence_values("unknown.action", "actor.profile.read_self") async with engine.connect() as connection: transaction = await connection.begin() diff --git a/backend/tests/test_audit.py b/backend/tests/test_audit.py index 9604d938f..cba757314 100644 --- a/backend/tests/test_audit.py +++ b/backend/tests/test_audit.py @@ -23,7 +23,7 @@ AuthorityEventType, ) from app.modules.audit.service import AuditService -from app.modules.authorization.catalogue import ActionId, PermissionId +from app.modules.authorization.catalogue import ACTION_DEFINITIONS, ActionId, PermissionId from app.modules.tasks.models import AuditEvent @@ -159,12 +159,13 @@ def test_action_aware_audit_input_enforces_mapping_and_planned_availability() -> action_id=None, denial_code="permission_not_granted", ) - with pytest.raises(ValidationError, match="planned action"): - _authority_input( - AuthorityEventType.SENSITIVE_AUTHORIZATION_ALLOWED, - permission_id="artifact.binding.read", - action_id="artifact.binding.read", - ) + for definition in ACTION_DEFINITIONS: + with pytest.raises(ValidationError, match="planned action"): + _authority_input( + AuthorityEventType.SENSITIVE_AUTHORIZATION_ALLOWED, + permission_id=definition.permission_id, + action_id=definition.action_id, + ) with pytest.raises(TypeError, match="invalid authority audit input"): _authority_input( AuthorityEventType.SENSITIVE_AUTHORIZATION_DENIED, diff --git a/docs/spec_authorization_service.md b/docs/spec_authorization_service.md index 5da72ac80..cb0b2e56b 100644 --- a/docs/spec_authorization_service.md +++ b/docs/spec_authorization_service.md @@ -352,7 +352,7 @@ counts, cursors, facets, or distinct values are computed. Registration and completeness are staged with the approved chunk map. Chunk 07A introduces the identifiers and planned registry; 07B introduces the kernel -and first active self actions. Reserved action metadata contains only the +and first active self-actions. Reserved action metadata contains only the stable `ActionId`, approved `PermissionId`, owning specification/chunk, and `planned` availability; it is not executable and does not predefine a foreign-domain target, facts, or guards. Every route-owning chunk from 07B From 044e8b535667b397c635fa2f13fc1cb67d2c3af0 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 15 Jul 2026 12:34:30 +0100 Subject: [PATCH 12/19] Record AUTH-07A external repair review --- .../WS-AUTH-001-07A-internal-review-evidence.md | 15 ++++++++++++--- .../reviews/WS-AUTH-001-07A-pr-trust-bundle.md | 4 ++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md index b53a454b1..3750bf68d 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md @@ -1,8 +1,8 @@ # WS-AUTH-001-07A Internal Review Evidence -Reviewed code SHA: `f35f71b375f4498e8d7943dcb674bdbb910a8f2a` -Reviewed runtime SHA: `478a819236b9cff1e1d7b61203015691ce0aaf45` -Reviewed at: `2026-07-15T10:58:49Z` +Reviewed code SHA: `6287f57936c5e1ec7621bcbf07ff45ee40f0ee91` +Reviewed runtime SHA: `6287f57936c5e1ec7621bcbf07ff45ee40f0ee91` +Reviewed at: `2026-07-15T11:33:28Z` Reviewer run IDs: `auth06_final_ci`, `auth06_final_docs`, `auth06_final_test_delta` @@ -24,6 +24,10 @@ Reviewer run IDs: `auth06_final_ci`, `auth06_final_docs`, - Direct SQL accepts all 30 exact action/permission pairs as denied evidence, rejects all 30 wrong registered-permission pairs, and rejects all 24 new permissions without a mapped action. +- External-review repair proves typed validation rejects allowed evidence for + all 30 planned actions while PostgreSQL accepts all 30 exact allowed pairs as + availability-neutral storage. The targeted isolated migration test passed in + 100.95 seconds at `6287f57`. - Ruff, stale Workstream wording, stale authorization documentation, changed Markdown links, loop-memory state, and diff integrity passed. - No workflow, dependency, test skip, coverage exclusion, package script, or @@ -56,6 +60,11 @@ The final exact-head review confirmed that `478a819..f35f71b` contains only six lifecycle/merge-intent files, records the evidence accurately, and keeps `WS-AUTH-001-07B` inactive until merge, signed memory, and explicit human start. +External repair review at `6287f57` confirmed CodeRabbit's proposed denial-only +SQL constraint would contradict the approved availability-neutral migration +contract. The accepted grammar fix and expanded PR description introduce no +runtime authority, and no prior negative behavior proof was weakened. + Valid findings addressed: yes Open sub-agent sessions: none diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md index fea309b7c..e2f14f89d 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md @@ -63,8 +63,8 @@ this PR does not change any coverage threshold or exclusion. ## Reviewer Results -Reviewed lifecycle head `f35f71b375f4498e8d7943dcb674bdbb910a8f2a` -and runtime SHA `478a819236b9cff1e1d7b61203015691ce0aaf45` passed +Reviewed external-repair head `6287f57936c5e1ec7621bcbf07ff45ee40f0ee91` +and original runtime candidate `478a819236b9cff1e1d7b61203015691ce0aaf45` passed senior engineering, QA/test, security/auth, product/ops, architecture, CI integrity, docs, reuse/dedup, and test-delta review with no remaining findings. From 3365e67e7b44195069a5c7645fdee0af1d4e0180 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 15 Jul 2026 13:34:25 +0100 Subject: [PATCH 13/19] Reserve canonical review authorization actions --- .../DECISIONS.md | 48 +++++- ...-07A-closed-permission-action-catalogue.md | 58 ++++++-- ...01-07B-deny-default-kernel-self-cutover.md | 25 ++++ .../0021_authorization_action_evidence.py | 23 ++- .../app/modules/authorization/catalogue.py | 140 ++++++++++++++++-- backend/tests/test_authorization.py | 30 +++- docs/operations_authorization_service.md | 16 +- docs/spec_authorization_service.md | 66 +++++++-- 8 files changed, 364 insertions(+), 42 deletions(-) diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DECISIONS.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DECISIONS.md index 8edaafd22..d25543baf 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DECISIONS.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DECISIONS.md @@ -267,7 +267,7 @@ The reviewed proposal did not receive independent normative precedence. Its artifact, review, contribution, and compensation resources were rejected. The adopted `/api/v1` namespace and original 52 approved permission identifiers were unchanged by that catalogue review. The later approved artifact-storage -contract adds 21 exact identifiers, making the current closed total 73. +contract added 21 exact identifiers, making the pre-D17 closed total 73. AUTH-05A currently enforces a 49-identifier typed/PostgreSQL audit base; the three already approved Operator recovery identifiers and 21 artifact identifiers remain planned and non-executable until AUTH-07A adds typed/SQL audit @@ -291,7 +291,8 @@ combined AUTH-07 contract placed grant-backed administrative APIs before AUTH-08, project capability composition before AUTH-10, and mixed the audit migration with executable kernel/API behavior. No runtime code had started. -AUTH-07A now owns only the exact 73-PermissionId catalogue, 30 four-field +At the split boundary, AUTH-07A owned only the exact 73-PermissionId catalogue, +30 four-field planned ActionId definitions, including both later self actions, and action-aware audit migration `0021`. AUTH-07B later owns the minimal deny-by-default kernel and activates only @@ -299,3 +300,46 @@ later owns the minimal deny-by-default kernel and activates only admin-role definition APIs move to AUTH-08; project-scoped authorization context moves to AUTH-10. Each child retains its own merge and explicit-start gate. + +## D17: Adopt canonical review actions without moving review behavior into AUTH + +Status: accepted by the user on 2026-07-15 after mapping the revised WS-REV +source against the implemented AUTH and ART contracts and completing internal +architecture, docs/security, and test/migration review. + +AUTH-07A expands the closed catalogue to exactly 74 PermissionIds and 50 +planned ActionIds. `review.queue.override` is the only additive PermissionId and +is explicitly the 25th post-`0020` permission; the historical 49-value set does +not change. Twenty planned actions are added: canonical `submission.create` +owned by `WS-AUTH-001-14`, plus 19 review actions owned by exact +`WS-REV-001-05`, `06`, `07`, `08`, `09A`, `11`, and `12` chunks. All additions +remain four-field planned metadata and cannot authorize until their owner +supplies resource composition, guards, candidates, surface declarations, +revalidation, and behavior tests. + +Initial and revision submissions share the same `submission.create` action, +permission, and route. There is no `submission.revise`, `review.assign`, +`review_revision.record`, or separately callable revision-preparation action. +Revision preparation is an internal participant and lifecycle guard of the +canonical submission command. Finding and finding-response evidence intake use +distinct actions mapped to existing `review.decision` and `submission.create` +permissions because each is a protected human command that can create artifact +state before the final transaction. + +`artifact_recovery.request` is rejected. Operator recovery consumes the already +registered `artifact.verification_job.retry` action through the ART-owned +`ArtifactOperatorRecoveryPort`; ART retains recovery-attempt, execution, +fencing, and idempotency ownership. REV owns no projection dispatch/retry action +because the shared outbox owns dispatch, attempts, retry, dead-letter, and +delivery state. Immediate grant-revocation recovery remains a participant of +the originating AUTH mutation, while missed recovery uses the planned +`review.reconcile.run` action. + +AUTH-07B must expose one stable public feature boundary: a request-scoped +`AuthorizationService` bound to the current `AuthorizationContext` and +caller-owned `AsyncSession`. Feature modules call +`require(action_id, typed_resource_context, uow=...)`; the service returns and +stages one bounded decision, never commits, and never accepts a raw +PermissionId, candidate grant, or guard. REV owns its ResourceContext composers +and lifecycle invariants and may import only that public AUTH interface and +closed types, never AUTH persistence or grant queries. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md index 322a37f8d..adc67ee2d 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07A-closed-permission-action-catalogue.md @@ -6,9 +6,10 @@ ## Goal -Create one closed typed catalogue for all 73 approved PermissionIds and the -exact reserved self/recovery/artifact ActionIds, then add typed and PostgreSQL action -evidence parity without making any action executable. +Create one closed typed catalogue for all 74 approved PermissionIds and the +exact 50 reserved self, recovery, submission, review, and artifact ActionIds, +then add typed and PostgreSQL action evidence parity without making any action +executable. ## Why this chunk exists @@ -69,6 +70,26 @@ are invalid. Every row has `availability=planned` and cannot authorize. | `operations.task.start_override` | `operations.task.start_override` | `WS-AUTH-001-13` | `planned` | | `operations.submission_gate.repair` | `operations.submission_gate.repair` | `WS-AUTH-001-14` | `planned` | | `operations.checker.retry` | `operations.checker.retry` | `WS-AUTH-001-14` | `planned` | +| `submission.create` | `submission.create` | `WS-AUTH-001-14` | `planned` | +| `review.queue.read` | `review.queue.read` | `WS-REV-001-05` | `planned` | +| `review.queue.inspect` | `review.queue.inspect` | `WS-REV-001-05` | `planned` | +| `review.claim` | `review.claim` | `WS-REV-001-06` | `planned` | +| `review.release` | `review.release` | `WS-REV-001-06` | `planned` | +| `review.decline_preference` | `review.decline_preference` | `WS-REV-001-06` | `planned` | +| `review.preference_expiry.run` | `operations.timer.run` | `WS-REV-001-06` | `planned` | +| `review.lease_expiry.run` | `operations.timer.run` | `WS-REV-001-06` | `planned` | +| `review.context.read` | `submission.read_for_review` | `WS-REV-001-07` | `planned` | +| `review.chain.read` | `review.chain.read` | `WS-REV-001-07` | `planned` | +| `review.finding_evidence.ingest` | `review.decision` | `WS-REV-001-07` | `planned` | +| `review.decision` | `review.decision` | `WS-REV-001-08` | `planned` | +| `review.finding_response_evidence.ingest` | `submission.create` | `WS-REV-001-09A` | `planned` | +| `review.lease.force_release` | `review.lease.force_release` | `WS-REV-001-11` | `planned` | +| `review.queue.routing.override` | `review.queue.override` | `WS-REV-001-11` | `planned` | +| `review.queue.routing.correct` | `review.queue.override` | `WS-REV-001-11` | `planned` | +| `review.queue.close` | `review.queue.override` | `WS-REV-001-11` | `planned` | +| `review.reconcile.run` | `operations.reconcile.run` | `WS-REV-001-11` | `planned` | +| `review.artifact_reference.reconcile` | `operations.reconcile.run` | `WS-REV-001-12` | `planned` | +| `review.projection.rebuild` | `operations.projection.rebuild` | `WS-REV-001-12` | `planned` | | `artifact.binding.read` | `artifact.binding.read` | `WS-ART-001-02D` | `planned` | | `artifact.replica.read` | `artifact.replica.read` | `WS-ART-001-02D` | `planned` | | `artifact.receipt.read` | `artifact.receipt.read` | `WS-ART-001-02D` | `planned` | @@ -101,11 +122,11 @@ owned by later chunks and are not registered here. ## Acceptance criteria -- `PermissionId` is the single closed typed source for exactly the 73 approved +- `PermissionId` is the single closed typed source for exactly the 74 approved values in `docs/spec_authorization_service.md`. - Existing audit validation consumes that source rather than maintaining a second permission literal set. -- A frozen action catalogue contains exactly the 30 planned ActionIds above, +- A frozen action catalogue contains exactly the 50 planned ActionIds above, with exact approved PermissionId mapping, owner, and availability. - Catalogue construction fails on duplicate or unknown actions, unknown permissions, invalid owners, invalid availability, missing approved entries, @@ -118,22 +139,25 @@ owned by later chunks and are not registered here. historical rows as null, and constrains every non-null value to the registered ActionId set. - Typed validation and PostgreSQL enforce every non-null ActionId's exact - PermissionId mapping from the 30-row catalogue; an action cannot be persisted + PermissionId mapping from the 50-row catalogue; an action cannot be persisted with another registered permission. - Every PermissionId outside migration `0018`'s historical 49-value set requires - a non-null ActionId whose catalogue row maps to that PermissionId. The three - planned actions mapped to an existing PermissionId may still carry their + a non-null ActionId whose catalogue row maps to that PermissionId. The 20 + planned actions mapped to a historical PermissionId may still carry their registered action while the historical permission remains valid without one for pre-`0021` event shapes. - A non-null `action_id` is valid only for `SensitiveAuthorizationAllowed` or `SensitiveAuthorizationDenied`; unrelated authority/lifecycle events must remain null. - Migration `0021` replaces the 49-value permission constraint with exact - 73-value typed/PostgreSQL parity without changing historical values. + 74-value typed/PostgreSQL parity without changing historical values. The + historical set remains the exact 49 identifiers from migration `0018`, and + `review.queue.override` is explicitly the 25th post-`0020` permission rather + than being inferred as historical by prefix. - `AuthorityAuditEventInput` admits a bounded registered `action_id`; unknown values and action/permission mismatches fail before rejected input can escape diagnostics. -- Because all 30 actions remain `planned`, `AuthorityAuditEventInput` rejects +- Because all 50 actions remain `planned`, `AuthorityAuditEventInput` rejects `SensitiveAuthorizationAllowed` when any of them is present. A planned action may be persisted only as bounded `SensitiveAuthorizationDenied` evidence; activation chunks own later allowed evidence. @@ -152,11 +176,13 @@ owned by later chunks and are not registered here. both refusal predicates or changing constraints. Deterministic independent- session proof shows a concurrent insert cannot pass between the checks and destructive DDL. -- Direct SQL proves unknown ActionIds and mismatched action/permission pairs - fail and every new permission without its mapped action fails. Direct-SQL - positive fixtures use denied evidence for every planned ActionId; denial-only - availability is tested at `AuthorityAuditEventInput`, not as a PostgreSQL - constraint. +- Direct SQL accepts all 50 exact action/permission pairs as + `SensitiveAuthorizationDenied` and separately as + `SensitiveAuthorizationAllowed`, rejects all 50 wrong action/permission + pairs, and rejects all 25 new permissions without a mapped action. Typed + `AuthorityAuditEventInput` separately rejects allowed evidence for all 50 + while they remain planned; PostgreSQL deliberately does not freeze + availability. - The canonical specification separates four-field planned registry metadata from later feature activation blueprints. - Operations docs cover startup catalogue failure, evidence inspection, and the @@ -190,7 +216,7 @@ percent floor. ## Human review focus -Review exact 73-permission parity, exact 30-action planned mapping, typed/SQL +Review exact 74-permission parity, exact 50-action planned mapping, typed/SQL mapping enforcement, inability to record planned actions as allowed, audit privacy, PostgreSQL constraint parity, historical null preservation, and guarded downgrade. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07B-deny-default-kernel-self-cutover.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07B-deny-default-kernel-self-cutover.md index 5591873f5..a10fe7252 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07B-deny-default-kernel-self-cutover.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07B-deny-default-kernel-self-cutover.md @@ -10,6 +10,28 @@ human start. Implement the minimal request-scoped AuthorizationService and activate it only for canonical actor self-read and self-update. +## Public feature interface + +The application dependency constructs one request-scoped +`AuthorizationService` bound to the current `AuthorizationContext` and +caller-owned `AsyncSession`. Feature application services call only: + +```python +decision = await authorization_service.require( + action_id, + typed_resource_context, + uow=caller_session, +) +``` + +The service returns and stages one bounded `AuthorizationDecision`; it never +commits. It never accepts a raw `PermissionId`, candidate grant, guard, role, or +caller-authored resource fact. The caller owns commit/rollback and mutation +revalidation stays inside the same caller transaction. Feature modules own +their typed ResourceContext composers and lifecycle invariants and may import +only this public interface and closed AUTH types, never AUTH repositories, +models, grant loaders, or private evaluator helpers. + ## Risk routing - Risk class: L1 @@ -107,6 +129,9 @@ to 404. Feature-owned concealment matrices begin in their owning cutover chunks. PermissionId, allowed/denial code, resource reference, matched authority kind, revalidation status, request ID, and correlation ID. - Unknown permissions, unknown actions, and every planned action deny. +- The public feature interface has the exact request-scoped `require` contract + above; import-boundary tests reject feature imports of AUTH persistence or + private evaluation helpers and reject raw permission/candidate/guard input. - System scope is not superuser authority. - Default human authority is exactly the two self candidates above. Token role changes do not alter either decision. diff --git a/backend/alembic/versions/0021_authorization_action_evidence.py b/backend/alembic/versions/0021_authorization_action_evidence.py index f496270e1..d3d7a8034 100644 --- a/backend/alembic/versions/0021_authorization_action_evidence.py +++ b/backend/alembic/versions/0021_authorization_action_evidence.py @@ -35,7 +35,8 @@ artifact.upload_session.read artifact.upload_item.write artifact.upload_session.seal artifact.upload_session.cancel artifact.upload_session.expire artifact.binding.create artifact.verification.execute artifact.pending_work.scan artifact.put_attempt.resolve -artifact.guide_source.read artifact.checker_input.materialize artifact.checker_output.write""".split() +artifact.guide_source.read artifact.checker_input.materialize artifact.checker_output.write +review.queue.override""".split() PERMISSIONS = HISTORICAL_PERMISSIONS + NEW_PERMISSIONS @@ -45,6 +46,26 @@ ("operations.task.start_override", "operations.task.start_override"), ("operations.submission_gate.repair", "operations.submission_gate.repair"), ("operations.checker.retry", "operations.checker.retry"), + ("submission.create", "submission.create"), + ("review.queue.read", "review.queue.read"), + ("review.queue.inspect", "review.queue.inspect"), + ("review.claim", "review.claim"), + ("review.release", "review.release"), + ("review.decline_preference", "review.decline_preference"), + ("review.preference_expiry.run", "operations.timer.run"), + ("review.lease_expiry.run", "operations.timer.run"), + ("review.context.read", "submission.read_for_review"), + ("review.chain.read", "review.chain.read"), + ("review.finding_evidence.ingest", "review.decision"), + ("review.decision", "review.decision"), + ("review.finding_response_evidence.ingest", "submission.create"), + ("review.lease.force_release", "review.lease.force_release"), + ("review.queue.routing.override", "review.queue.override"), + ("review.queue.routing.correct", "review.queue.override"), + ("review.queue.close", "review.queue.override"), + ("review.reconcile.run", "operations.reconcile.run"), + ("review.artifact_reference.reconcile", "operations.reconcile.run"), + ("review.projection.rebuild", "operations.projection.rebuild"), ("artifact.binding.read", "artifact.binding.read"), ("artifact.replica.read", "artifact.replica.read"), ("artifact.receipt.read", "artifact.receipt.read"), diff --git a/backend/app/modules/authorization/catalogue.py b/backend/app/modules/authorization/catalogue.py index 41112d19b..30e8de24c 100644 --- a/backend/app/modules/authorization/catalogue.py +++ b/backend/app/modules/authorization/catalogue.py @@ -47,6 +47,7 @@ class PermissionId(StrEnum): REVIEW_DECISION = "review.decision" REVIEW_LEASE_FORCE_RELEASE = "review.lease.force_release" REVIEW_CHAIN_READ = "review.chain.read" + REVIEW_QUEUE_OVERRIDE = "review.queue.override" CONTRIBUTION_READ_SELF = "contribution.read_self" CONTRIBUTION_READ_PROJECT = "contribution.read_project" COMPENSATION_POLICY_MANAGE = "compensation.policy.manage" @@ -95,6 +96,26 @@ class ActionId(StrEnum): OPERATIONS_TASK_START_OVERRIDE = "operations.task.start_override" OPERATIONS_SUBMISSION_GATE_REPAIR = "operations.submission_gate.repair" OPERATIONS_CHECKER_RETRY = "operations.checker.retry" + SUBMISSION_CREATE = "submission.create" + REVIEW_QUEUE_READ = "review.queue.read" + REVIEW_QUEUE_INSPECT = "review.queue.inspect" + REVIEW_CLAIM = "review.claim" + REVIEW_RELEASE = "review.release" + REVIEW_DECLINE_PREFERENCE = "review.decline_preference" + REVIEW_PREFERENCE_EXPIRY_RUN = "review.preference_expiry.run" + REVIEW_LEASE_EXPIRY_RUN = "review.lease_expiry.run" + REVIEW_CONTEXT_READ = "review.context.read" + REVIEW_CHAIN_READ = "review.chain.read" + REVIEW_FINDING_EVIDENCE_INGEST = "review.finding_evidence.ingest" + REVIEW_DECISION = "review.decision" + REVIEW_FINDING_RESPONSE_EVIDENCE_INGEST = "review.finding_response_evidence.ingest" + REVIEW_LEASE_FORCE_RELEASE = "review.lease.force_release" + REVIEW_QUEUE_ROUTING_OVERRIDE = "review.queue.routing.override" + REVIEW_QUEUE_ROUTING_CORRECT = "review.queue.routing.correct" + REVIEW_QUEUE_CLOSE = "review.queue.close" + REVIEW_RECONCILE_RUN = "review.reconcile.run" + REVIEW_ARTIFACT_REFERENCE_RECONCILE = "review.artifact_reference.reconcile" + REVIEW_PROJECTION_REBUILD = "review.projection.rebuild" ARTIFACT_BINDING_READ = "artifact.binding.read" ARTIFACT_REPLICA_READ = "artifact.replica.read" ARTIFACT_RECEIPT_READ = "artifact.receipt.read" @@ -131,6 +152,13 @@ class ActionOwner(StrEnum): AUTH_07B = "WS-AUTH-001-07B" AUTH_13 = "WS-AUTH-001-13" AUTH_14 = "WS-AUTH-001-14" + REV_05 = "WS-REV-001-05" + REV_06 = "WS-REV-001-06" + REV_07 = "WS-REV-001-07" + REV_08 = "WS-REV-001-08" + REV_09A = "WS-REV-001-09A" + REV_11 = "WS-REV-001-11" + REV_12 = "WS-REV-001-12" ART_02D = "WS-ART-001-02D" ART_03 = "WS-ART-001-03" ART_04A = "WS-ART-001-04A" @@ -190,6 +218,82 @@ def _planned( PermissionId.OPERATIONS_CHECKER_RETRY, ActionOwner.AUTH_14, ), + _planned(ActionId.SUBMISSION_CREATE, PermissionId.SUBMISSION_CREATE, ActionOwner.AUTH_14), + _planned(ActionId.REVIEW_QUEUE_READ, PermissionId.REVIEW_QUEUE_READ, ActionOwner.REV_05), + _planned( + ActionId.REVIEW_QUEUE_INSPECT, + PermissionId.REVIEW_QUEUE_INSPECT, + ActionOwner.REV_05, + ), + _planned(ActionId.REVIEW_CLAIM, PermissionId.REVIEW_CLAIM, ActionOwner.REV_06), + _planned(ActionId.REVIEW_RELEASE, PermissionId.REVIEW_RELEASE, ActionOwner.REV_06), + _planned( + ActionId.REVIEW_DECLINE_PREFERENCE, + PermissionId.REVIEW_DECLINE_PREFERENCE, + ActionOwner.REV_06, + ), + _planned( + ActionId.REVIEW_PREFERENCE_EXPIRY_RUN, + PermissionId.OPERATIONS_TIMER_RUN, + ActionOwner.REV_06, + ), + _planned( + ActionId.REVIEW_LEASE_EXPIRY_RUN, + PermissionId.OPERATIONS_TIMER_RUN, + ActionOwner.REV_06, + ), + _planned( + ActionId.REVIEW_CONTEXT_READ, + PermissionId.SUBMISSION_READ_FOR_REVIEW, + ActionOwner.REV_07, + ), + _planned(ActionId.REVIEW_CHAIN_READ, PermissionId.REVIEW_CHAIN_READ, ActionOwner.REV_07), + _planned( + ActionId.REVIEW_FINDING_EVIDENCE_INGEST, + PermissionId.REVIEW_DECISION, + ActionOwner.REV_07, + ), + _planned(ActionId.REVIEW_DECISION, PermissionId.REVIEW_DECISION, ActionOwner.REV_08), + _planned( + ActionId.REVIEW_FINDING_RESPONSE_EVIDENCE_INGEST, + PermissionId.SUBMISSION_CREATE, + ActionOwner.REV_09A, + ), + _planned( + ActionId.REVIEW_LEASE_FORCE_RELEASE, + PermissionId.REVIEW_LEASE_FORCE_RELEASE, + ActionOwner.REV_11, + ), + _planned( + ActionId.REVIEW_QUEUE_ROUTING_OVERRIDE, + PermissionId.REVIEW_QUEUE_OVERRIDE, + ActionOwner.REV_11, + ), + _planned( + ActionId.REVIEW_QUEUE_ROUTING_CORRECT, + PermissionId.REVIEW_QUEUE_OVERRIDE, + ActionOwner.REV_11, + ), + _planned( + ActionId.REVIEW_QUEUE_CLOSE, + PermissionId.REVIEW_QUEUE_OVERRIDE, + ActionOwner.REV_11, + ), + _planned( + ActionId.REVIEW_RECONCILE_RUN, + PermissionId.OPERATIONS_RECONCILE_RUN, + ActionOwner.REV_11, + ), + _planned( + ActionId.REVIEW_ARTIFACT_REFERENCE_RECONCILE, + PermissionId.OPERATIONS_RECONCILE_RUN, + ActionOwner.REV_12, + ), + _planned( + ActionId.REVIEW_PROJECTION_REBUILD, + PermissionId.OPERATIONS_PROJECTION_REBUILD, + ActionOwner.REV_12, + ), _planned( ActionId.ARTIFACT_BINDING_READ, PermissionId.ARTIFACT_BINDING_READ, ActionOwner.ART_02D ), @@ -309,18 +413,36 @@ def _planned( PERMISSION_IDS = frozenset(PermissionId) ACTION_IDS = frozenset(ActionId) -HISTORICAL_PERMISSION_IDS = frozenset( - permission - for permission in PermissionId - if not permission.value.startswith("artifact.") - and permission - not in { +NEW_PERMISSION_IDS = frozenset( + { PermissionId.OPERATIONS_TASK_START_OVERRIDE, PermissionId.OPERATIONS_SUBMISSION_GATE_REPAIR, PermissionId.OPERATIONS_CHECKER_RETRY, + PermissionId.REVIEW_QUEUE_OVERRIDE, + PermissionId.ARTIFACT_BINDING_READ, + PermissionId.ARTIFACT_REPLICA_READ, + PermissionId.ARTIFACT_RECEIPT_READ, + PermissionId.ARTIFACT_VERIFICATION_JOB_READ, + PermissionId.ARTIFACT_VERIFICATION_JOB_RETRY, + PermissionId.ARTIFACT_RECOVERY_ATTEMPT_READ, + PermissionId.ARTIFACT_AUDIT_READ, + PermissionId.ARTIFACT_GUIDE_SOURCE_INGEST, + PermissionId.ARTIFACT_UPLOAD_SESSION_CREATE, + PermissionId.ARTIFACT_UPLOAD_SESSION_READ, + PermissionId.ARTIFACT_UPLOAD_ITEM_WRITE, + PermissionId.ARTIFACT_UPLOAD_SESSION_SEAL, + PermissionId.ARTIFACT_UPLOAD_SESSION_CANCEL, + PermissionId.ARTIFACT_UPLOAD_SESSION_EXPIRE, + PermissionId.ARTIFACT_BINDING_CREATE, + PermissionId.ARTIFACT_VERIFICATION_EXECUTE, + PermissionId.ARTIFACT_PENDING_WORK_SCAN, + PermissionId.ARTIFACT_PUT_ATTEMPT_RESOLVE, + PermissionId.ARTIFACT_GUIDE_SOURCE_READ, + PermissionId.ARTIFACT_CHECKER_INPUT_MATERIALIZE, + PermissionId.ARTIFACT_CHECKER_OUTPUT_WRITE, } ) -NEW_PERMISSION_IDS = PERMISSION_IDS - HISTORICAL_PERMISSION_IDS +HISTORICAL_PERMISSION_IDS = PERMISSION_IDS - NEW_PERMISSION_IDS def _index_actions( @@ -336,11 +458,11 @@ def _index_actions( ): raise RuntimeError("authorization action catalogue contains an invalid row") indexed = {definition.action_id: definition for definition in definitions} - if len(PERMISSION_IDS) != 73 or len(ACTION_IDS) != 30: + if len(PERMISSION_IDS) != 74 or len(ACTION_IDS) != 50: raise RuntimeError("authorization catalogue count mismatch") if len(indexed) != len(definitions) or set(indexed) != ACTION_IDS: raise RuntimeError("authorization action catalogue is incomplete") - if len(HISTORICAL_PERMISSION_IDS) != 49 or len(NEW_PERMISSION_IDS) != 24: + if len(HISTORICAL_PERMISSION_IDS) != 49 or len(NEW_PERMISSION_IDS) != 25: raise RuntimeError("authorization permission boundary mismatch") if any(definition.availability is not ActionAvailability.PLANNED for definition in definitions): raise RuntimeError("AUTH-07A actions must remain planned") diff --git a/backend/tests/test_authorization.py b/backend/tests/test_authorization.py index b396419af..915ed2a85 100644 --- a/backend/tests/test_authorization.py +++ b/backend/tests/test_authorization.py @@ -93,7 +93,7 @@ def test_closed_permission_and_action_catalogue_is_exact_and_non_executable() -> artifact.upload_session.cancel artifact.upload_session.expire artifact.binding.create artifact.verification.execute artifact.pending_work.scan artifact.put_attempt.resolve artifact.guide_source.read artifact.checker_input.materialize - artifact.checker_output.write""".split() + artifact.checker_output.write review.queue.override""".split() ) expected = { "actor.profile.read_self": ("actor.profile.read_self", "WS-AUTH-001-07B"), @@ -101,6 +101,32 @@ def test_closed_permission_and_action_catalogue_is_exact_and_non_executable() -> "operations.task.start_override": ("operations.task.start_override", "WS-AUTH-001-13"), "operations.submission_gate.repair": ("operations.submission_gate.repair", "WS-AUTH-001-14"), "operations.checker.retry": ("operations.checker.retry", "WS-AUTH-001-14"), + "submission.create": ("submission.create", "WS-AUTH-001-14"), + "review.queue.read": ("review.queue.read", "WS-REV-001-05"), + "review.queue.inspect": ("review.queue.inspect", "WS-REV-001-05"), + "review.claim": ("review.claim", "WS-REV-001-06"), + "review.release": ("review.release", "WS-REV-001-06"), + "review.decline_preference": ("review.decline_preference", "WS-REV-001-06"), + "review.preference_expiry.run": ("operations.timer.run", "WS-REV-001-06"), + "review.lease_expiry.run": ("operations.timer.run", "WS-REV-001-06"), + "review.context.read": ("submission.read_for_review", "WS-REV-001-07"), + "review.chain.read": ("review.chain.read", "WS-REV-001-07"), + "review.finding_evidence.ingest": ("review.decision", "WS-REV-001-07"), + "review.decision": ("review.decision", "WS-REV-001-08"), + "review.finding_response_evidence.ingest": ( + "submission.create", + "WS-REV-001-09A", + ), + "review.lease.force_release": ("review.lease.force_release", "WS-REV-001-11"), + "review.queue.routing.override": ("review.queue.override", "WS-REV-001-11"), + "review.queue.routing.correct": ("review.queue.override", "WS-REV-001-11"), + "review.queue.close": ("review.queue.override", "WS-REV-001-11"), + "review.reconcile.run": ("operations.reconcile.run", "WS-REV-001-11"), + "review.artifact_reference.reconcile": ( + "operations.reconcile.run", + "WS-REV-001-12", + ), + "review.projection.rebuild": ("operations.projection.rebuild", "WS-REV-001-12"), "artifact.binding.read": ("artifact.binding.read", "WS-ART-001-02D"), "artifact.replica.read": ("artifact.replica.read", "WS-ART-001-02D"), "artifact.receipt.read": ("artifact.receipt.read", "WS-ART-001-02D"), @@ -136,7 +162,7 @@ def test_closed_permission_and_action_catalogue_is_exact_and_non_executable() -> assert {item.value for item in HISTORICAL_PERMISSION_IDS} == historical_permissions assert {item.value for item in NEW_PERMISSION_IDS} == new_permissions assert {item.value for item in PERMISSION_IDS} == historical_permissions | new_permissions - assert len(ACTION_IDS) == len(ACTION_DEFINITIONS) == len(ACTION_BY_ID) == 30 + assert len(ACTION_IDS) == len(ACTION_DEFINITIONS) == len(ACTION_BY_ID) == 50 assert set(ACTION_BY_ID) == ACTION_IDS assert {definition.owner for definition in ACTION_DEFINITIONS} == set(ActionOwner) assert all( diff --git a/docs/operations_authorization_service.md b/docs/operations_authorization_service.md index 6467d6c27..c42ab498c 100644 --- a/docs/operations_authorization_service.md +++ b/docs/operations_authorization_service.md @@ -477,7 +477,7 @@ resource loader, lifecycle guards, negative tests, and evidence path exist. ### Catalogue And Action-Evidence Staging -AUTH-07A installs exactly 73 PermissionIds and 30 planned ActionIds. Planned +AUTH-07A installs exactly 74 PermissionIds and 50 planned ActionIds. Planned entries contain only action, permission, owner, and availability; they are not executable and must not receive deployment configuration, principals, resource facts, or guards. Startup validation failure is a release blocker, not a reason @@ -491,6 +491,20 @@ and every permission added after migration `0018` must carry one of its mapped actions. Planned actions can record bounded denial evidence but cannot record an allowed decision through the typed writer. +The historical permission set remains exactly 49 values. The post-`0020` set +contains exactly 25 values, including `review.queue.override`; do not derive +historical status from identifier prefixes. All submission/review rows remain +planned. Initial and revision submission share `submission.create`, and no +revision-specific permission or preparation action exists. + +Review code must consume the request-scoped public +`AuthorizationService.require(action_id, typed_resource_context, uow=...)` +boundary. It must not query grants, import AUTH persistence, select raw +PermissionIds, or implement permission unions. Artifact recovery remains the +ART-owned `artifact.verification_job.retry` action through +`ArtifactOperatorRecoveryPort`; shared outbox dispatch/retry remains outside +REV ownership. + Downgrade is allowed only while every action ID remains null and no permission outside migration `0018`'s historical 49-value set exists in the decision, target-reference, or invalidation-reference fields. The migration takes an diff --git a/docs/spec_authorization_service.md b/docs/spec_authorization_service.md index cb0b2e56b..3c576427f 100644 --- a/docs/spec_authorization_service.md +++ b/docs/spec_authorization_service.md @@ -152,6 +152,7 @@ review.decline_preference review.decision review.lease.force_release review.chain.read +review.queue.override contribution.read_self contribution.read_project @@ -207,23 +208,66 @@ permissions or inferring authority. Artifact actions activate only through the paired feature model below; AUTH-12, AUTH-14, and AUTH-15 do not activate or attach artifact actions on behalf of WS-ART. -These are 73 approved `PermissionId` values. `ActionId` values are a separate -closed registry layer and are not included in that permission count. AUTH-05A's current typed and -PostgreSQL audit registry accepts 49. The three approved Operator recovery -identifiers `operations.task.start_override`, -`operations.submission_gate.repair`, and `operations.checker.retry`, plus the -21 artifact identifiers above, are reserved planned metadata. AUTH-07A adds -their matching typed/SQL audit parity without making them executable. An -artifact action becomes active only when the owning WS-ART chunk supplies its -canonical resource composer, guards, surface declaration, behavior tests, and -transaction-local revalidation where required. Both halves are mandatory; -registry presence alone never grants authority. +These are 74 approved `PermissionId` values. `ActionId` values are a separate +closed registry layer and are not included in that permission count. AUTH-05A's +typed and PostgreSQL audit registry accepts the exact historical 49. The three +approved Operator recovery identifiers, 21 artifact identifiers, and +`review.queue.override` are the exact 25 post-`0020` permissions. AUTH-07A adds +their matching typed/SQL audit parity without making them executable. + +The closed action registry contains 50 planned rows: two actor-self actions, +three Operator recovery actions, 25 artifact actions, canonical +`submission.create`, and 19 review actions. An action becomes active only when +its owning chunk supplies its canonical resource composer, guards, surface or +command declaration, behavior tests, and transaction-local revalidation where +required. Both halves are mandatory; registry presence alone never grants +authority. AUTH-07A also reserves `actor.profile.read_self` and `actor.profile.update_self` as four-field planned action metadata. AUTH-07B later supplies their complete active definitions and self-route behavior proof without changing migration `0021`. +The submission/review dependency matrix is closed. AUTH-07A registers only the +four stable planned fields shown here; resource facts, candidates, guards, and +runtime activation remain with the listed owner. + +| ActionId | PermissionId | Owner | +|---|---|---| +| `submission.create` | `submission.create` | `WS-AUTH-001-14` | +| `review.queue.read` | `review.queue.read` | `WS-REV-001-05` | +| `review.queue.inspect` | `review.queue.inspect` | `WS-REV-001-05` | +| `review.claim` | `review.claim` | `WS-REV-001-06` | +| `review.release` | `review.release` | `WS-REV-001-06` | +| `review.decline_preference` | `review.decline_preference` | `WS-REV-001-06` | +| `review.preference_expiry.run` | `operations.timer.run` | `WS-REV-001-06` | +| `review.lease_expiry.run` | `operations.timer.run` | `WS-REV-001-06` | +| `review.context.read` | `submission.read_for_review` | `WS-REV-001-07` | +| `review.chain.read` | `review.chain.read` | `WS-REV-001-07` | +| `review.finding_evidence.ingest` | `review.decision` | `WS-REV-001-07` | +| `review.decision` | `review.decision` | `WS-REV-001-08` | +| `review.finding_response_evidence.ingest` | `submission.create` | `WS-REV-001-09A` | +| `review.lease.force_release` | `review.lease.force_release` | `WS-REV-001-11` | +| `review.queue.routing.override` | `review.queue.override` | `WS-REV-001-11` | +| `review.queue.routing.correct` | `review.queue.override` | `WS-REV-001-11` | +| `review.queue.close` | `review.queue.override` | `WS-REV-001-11` | +| `review.reconcile.run` | `operations.reconcile.run` | `WS-REV-001-11` | +| `review.artifact_reference.reconcile` | `operations.reconcile.run` | `WS-REV-001-12` | +| `review.projection.rebuild` | `operations.projection.rebuild` | `WS-REV-001-12` | + +Initial and revision submission use the same `submission.create` action, +permission, and route. Revision preparation is an internal participant and +lifecycle guard of that command; no `submission.revise` or revision-prepare +action exists. Finding and finding-response evidence intake are distinct +protected commands mapped to existing permissions. The only new permission is +`review.queue.override`. + +Artifact verification recovery remains the existing +`artifact.verification_job.retry` action through the ART-owned +`ArtifactOperatorRecoveryPort`; no `artifact_recovery.request` permission is +registered. Shared outbox dispatch/retry remains owned by the shared-outbox +subsystem and is not represented as a REV-owned projection action. + Migration `0021` is availability-neutral. PostgreSQL enforces the closed ActionId set, authorization-decision event shape, exact ActionId-to-PermissionId mapping, and the requirement that every post-`0018` permission carry a mapped From 160af8afd030f042ee72ec963e6f47cd8b7d4c9a Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 15 Jul 2026 13:46:54 +0100 Subject: [PATCH 14/19] Clarify authorization transaction ownership --- .../DECISIONS.md | 5 +++-- .../WS-AUTH-001-workstream-authorization-service/PLAN.md | 2 +- .../WS-AUTH-001-07B-deny-default-kernel-self-cutover.md | 8 ++++---- docs/decision_0012_workstream_authorization_service.md | 2 +- docs/operations_authorization_service.md | 6 ++++-- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DECISIONS.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DECISIONS.md index d25543baf..bbe85ba65 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DECISIONS.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DECISIONS.md @@ -338,8 +338,9 @@ the originating AUTH mutation, while missed recovery uses the planned AUTH-07B must expose one stable public feature boundary: a request-scoped `AuthorizationService` bound to the current `AuthorizationContext` and caller-owned `AsyncSession`. Feature modules call -`require(action_id, typed_resource_context, uow=...)`; the service returns and -stages one bounded decision, never commits, and never accepts a raw +`require(action_id, typed_resource_context)`; the bound session is the only +transaction source. The service returns and stages one bounded decision, never +commits, and never accepts a raw PermissionId, candidate grant, or guard. REV owns its ResourceContext composers and lifecycle invariants and may import only that public AUTH interface and closed types, never AUTH persistence or grant queries. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/PLAN.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/PLAN.md index 2a6ed8083..16f77de53 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/PLAN.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/PLAN.md @@ -15,7 +15,7 @@ Bearer token -> ActorResolver -> ActorIdentityLink + ActorProfile -> request-scoped AuthorizationContext --> AuthorizationService.require(permission, ResourceContext, uow) +-> AuthorizationService.require(ActionId, typed ResourceContext) -> AdminRoleGrant / ProjectRoleGrant candidates -> canonical project and ownership resolution -> actor, resource, and lifecycle guards diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07B-deny-default-kernel-self-cutover.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07B-deny-default-kernel-self-cutover.md index a10fe7252..25cc9ce2b 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07B-deny-default-kernel-self-cutover.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-07B-deny-default-kernel-self-cutover.md @@ -20,7 +20,6 @@ caller-owned `AsyncSession`. Feature application services call only: decision = await authorization_service.require( action_id, typed_resource_context, - uow=caller_session, ) ``` @@ -129,9 +128,10 @@ to 404. Feature-owned concealment matrices begin in their owning cutover chunks. PermissionId, allowed/denial code, resource reference, matched authority kind, revalidation status, request ID, and correlation ID. - Unknown permissions, unknown actions, and every planned action deny. -- The public feature interface has the exact request-scoped `require` contract - above; import-boundary tests reject feature imports of AUTH persistence or - private evaluation helpers and reject raw permission/candidate/guard input. +- The public feature interface has the exact two-argument request-scoped + `require` contract above. An API-signature test rejects any session or `uow` + argument, and import-boundary tests reject feature imports of AUTH persistence + or private evaluation helpers and reject raw permission/candidate/guard input. - System scope is not superuser authority. - Default human authority is exactly the two self candidates above. Token role changes do not alter either decision. diff --git a/docs/decision_0012_workstream_authorization_service.md b/docs/decision_0012_workstream_authorization_service.md index 4b66b8e65..1ab5ccefc 100644 --- a/docs/decision_0012_workstream_authorization_service.md +++ b/docs/decision_0012_workstream_authorization_service.md @@ -45,7 +45,7 @@ external bearer token -> ActorResolver -> ActorProfile + ActorIdentityLink -> AuthorizationContext --> AuthorizationService.require(permission, ResourceContext, AsyncSession) +-> AuthorizationService.require(ActionId, typed ResourceContext) -> candidate grants -> resource and lifecycle guards -> allow or stable denial diff --git a/docs/operations_authorization_service.md b/docs/operations_authorization_service.md index c42ab498c..09226f509 100644 --- a/docs/operations_authorization_service.md +++ b/docs/operations_authorization_service.md @@ -498,8 +498,10 @@ planned. Initial and revision submission share `submission.create`, and no revision-specific permission or preparation action exists. Review code must consume the request-scoped public -`AuthorizationService.require(action_id, typed_resource_context, uow=...)` -boundary. It must not query grants, import AUTH persistence, select raw +`AuthorizationService.require(action_id, typed_resource_context)` boundary. +The service's bound caller-owned `AsyncSession` is the only transaction source; +the method accepts no session or `uow` argument. Review code must not query +grants, import AUTH persistence, select raw PermissionIds, or implement permission unions. Artifact recovery remains the ART-owned `artifact.verification_job.retry` action through `ArtifactOperatorRecoveryPort`; shared outbox dispatch/retry remains outside From f5af7986c7b85a4b45fbf21ee9f0a265c5c45177 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 15 Jul 2026 13:55:22 +0100 Subject: [PATCH 15/19] Record canonical review authorization evidence --- .agent-loop/LOOP_STATE.md | 11 +++--- .agent-loop/REVIEW_LOG.md | 13 +++++++ .agent-loop/WORK_QUEUE.md | 2 +- .../STATUS.md | 14 ++++--- ...S-AUTH-001-07A-internal-review-evidence.md | 37 ++++++++++--------- .../WS-AUTH-001-07A-pr-trust-bundle.md | 23 +++++++----- 6 files changed, 62 insertions(+), 38 deletions(-) diff --git a/.agent-loop/LOOP_STATE.md b/.agent-loop/LOOP_STATE.md index e778f91be..3ca554dbc 100644 --- a/.agent-loop/LOOP_STATE.md +++ b/.agent-loop/LOOP_STATE.md @@ -17,15 +17,16 @@ - PR #122 merged the first automated post-merge memory implementation as `fc89fb6`; its schema-v1 cross-initiative next pointer is superseded by the schema-v2 initiative-local clean cut. -- Current gate: AUTH-07A implementation and repair passed every required - internal reviewer track at `478a819`; deterministic evidence is complete and - PR publication is pending. +- Current gate: AUTH-07A's canonical review/revision amendment passed every + required internal reviewer track at `160af8a`; deterministic evidence is + complete and PR #126 awaits external checks and explicit human approval. - Scope checkpoint: AWS S3 is the only v0.1 production provider; MinIO is local/CI S3 protocol proof; LocalStorage is focused development/test; R2 and Flow Node are deferred. Product modules receive narrow artifact capabilities, and AWS cannot instantiate in production without release-bound live proof. -- Authorization checkpoint: the approved catalogue contains 73 identifiers, - including 21 artifact permissions. AUTH-07 registers them, AUTH-08 defines +- Authorization checkpoint: the approved catalogue contains 74 PermissionIds + and 50 planned ActionIds, including 21 artifact permissions and one additive + `review.queue.override` permission. AUTH-07 registers them, AUTH-08 defines applicable Operator grants, AUTH-09 provisions fixed service principals, and each owning WS-ART feature chunk activates only its own canonical actions. - Next artifact candidate: `WS-ART-001-02A1` remains inactive until the user diff --git a/.agent-loop/REVIEW_LOG.md b/.agent-loop/REVIEW_LOG.md index 11b013717..ba795e286 100644 --- a/.agent-loop/REVIEW_LOG.md +++ b/.agent-loop/REVIEW_LOG.md @@ -1,5 +1,18 @@ # Review Log +## 2026-07-15 - WS-AUTH-001-07A Canonical Review Amendment Passed + +Exact reviewed head `160af8afd030f042ee72ec963e6f47cd8b7d4c9a` reserves +the canonical `submission.create` dependency and 19 review actions. The closed +catalogue is now exactly 74 PermissionIds and 50 planned ActionIds; only +`review.queue.override` is additive. Initial and revision submissions share +`submission.create`. Required exact-head review passed after removing duplicate +session authority from the public contract: request-scoped +`AuthorizationService` binds the caller-owned `AsyncSession` and exposes only +`require(action_id, typed_resource_context)`. Full migration proof passed 16 +tests; focused authorization/audit coverage remains above 90 percent. PR #126 +is the current external and explicit-human gate; AUTH-07B remains inactive. + ## 2026-07-15 - WS-AUTH-001-07A Internal Review Passed Exact implementation SHA `478a819236b9cff1e1d7b61203015691ce0aaf45` diff --git a/.agent-loop/WORK_QUEUE.md b/.agent-loop/WORK_QUEUE.md index e667a4048..dc3dd19a4 100644 --- a/.agent-loop/WORK_QUEUE.md +++ b/.agent-loop/WORK_QUEUE.md @@ -4,7 +4,7 @@ | Chunk | Title | Risk | Status | |---|---|---:|---| -| `WS-AUTH-001-07A` | Closed Permission And Action Catalogue | L1 | Implementation and repair internally approved at `478a819`; PR publication pending | +| `WS-AUTH-001-07A` | Closed Permission And Action Catalogue | L1 | Review/revision amendment internally approved at `160af8a`; PR #126 external/human review pending | ## Planned Next diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md index 4a694f36f..90943cea9 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md @@ -69,12 +69,15 @@ internal and external checks; explicit human approval merged PR #124 as a separate start. The user explicitly started AUTH-07 on 2026-07-15; discovery and required L1 plan review are complete. The repaired AUTH-07A contract passed all required tracks at `beb85ac`; implementation, repair, deterministic -evidence, and required internal review pass at `478a819`. +evidence, and required internal review pass at `478a819`. The canonical +review/revision amendment then passed exact-head review at `160af8a`, with 74 +PermissionIds and 50 planned ActionIds. That review rejected the combined AUTH-07 contract before runtime edits because grant-backed/project APIs preceded their authority sources and the audit/API ownership files were incomplete. Parent AUTH-07 is now split into 07A catalogue and audit parity, followed by 07B kernel and actor self-action cutover. AUTH-07A -is ready for PR publication; AUTH-07B remains inactive. +is published as PR #126 and awaits external checks and explicit human approval; +AUTH-07B remains inactive. ## Active planning chunk @@ -105,7 +108,7 @@ None. | `WS-AUTH-001-05B` | Merged | `codex/ws-auth-001-05b-idempotency-invalidation` | #119 | Merged as `ad71c7e`; reviewed runtime SHA `e083890`. | | `WS-AUTH-001-06` | Merged | `codex/ws-auth-001-06-canonical-actor-profile` | #124 | Merged as `f599551`; final PR head `4a2193f`. | | `WS-AUTH-001-07` | Split | `codex/ws-auth-001-07-authorization-kernel` | - | Required L1 review rejected the combined contract before runtime edits. | -| `WS-AUTH-001-07A` | Internally approved | `codex/ws-auth-001-07-authorization-kernel` | - | Reviewed implementation `478a819`; closed catalogue and action-aware audit parity only. | +| `WS-AUTH-001-07A` | Internally approved | `codex/ws-auth-001-07-authorization-kernel` | #126 | Reviewed amendment `160af8a`; 74 permissions, 50 planned actions, and action-aware audit parity only. | | `WS-AUTH-001-07B` | Proposed | - | - | Inactive until 07A merge/memory and explicit user start. | | `WS-AUTH-001-08` | Proposed | - | - | Bootstrap and administrative grants. | | `WS-AUTH-001-09` | Proposed | - | - | Actor/link states and service actors. | @@ -129,7 +132,7 @@ inferred kinds before the owning canonical actor migration. The proposed external catalogue cannot be adopted as a normative handoff: it conflicts with `/api/v1`, AUTH-05A's merged 49-identifier persisted audit base, -current project and artifact models, and staged domain ownership. All 73 +current project and artifact models, and staged domain ownership. All 74 permission identifiers are approved, including `operations.task.start_override`, `operations.submission_gate.repair`, and `operations.checker.retry`; AUTH-07A gives those recovery identifiers exact @@ -139,7 +142,8 @@ rules. This is a scope decision, not an AUTH-05B runtime blocker. PR #118, AUTH-05B PR #119, and AUTH-06 PR #124 are merged. AUTH-07 has an explicit user start. Required review split it before runtime implementation; the repaired 07A contract passed all required tracks at `beb85ac`; implementation -and repair passed at `478a819`. AUTH-07B and +and repair passed at `478a819`, and the review/revision amendment passed at +`160af8a`. AUTH-07B and POL-002-04 remain inactive until separate explicit human starts and their authorization prerequisites. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md index 3750bf68d..deae91357 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md @@ -1,8 +1,8 @@ # WS-AUTH-001-07A Internal Review Evidence -Reviewed code SHA: `6287f57936c5e1ec7621bcbf07ff45ee40f0ee91` -Reviewed runtime SHA: `6287f57936c5e1ec7621bcbf07ff45ee40f0ee91` -Reviewed at: `2026-07-15T11:33:28Z` +Reviewed code SHA: `160af8afd030f042ee72ec963e6f47cd8b7d4c9a` +Reviewed runtime SHA: `3365e67e7b44195069a5c7645fdee0af1d4e0180` +Reviewed at: `2026-07-15T12:51:26Z` Reviewer run IDs: `auth06_final_ci`, `auth06_final_docs`, `auth06_final_test_delta` @@ -10,24 +10,24 @@ Reviewer run IDs: `auth06_final_ci`, `auth06_final_docs`, - The focused authorization and audit suite passed all 37 collected behavior tests against isolated PostgreSQL migration head `0021_auth_action_evidence`. -- Branch-aware subsystem coverage is 94 percent for authorization and 93 +- Branch-aware subsystem coverage is 95 percent for authorization and 93 percent for audit, above the required 90 percent threshold for materially changed backend subsystems. -- The exact catalogue/startup matrix passed 10 tests covering the independent - 49 historical and 24 new PermissionId sets, exact 30-action mapping, missing, +- The exact catalogue/startup matrix covers the independent 49 historical and + 25 new PermissionId sets, exact 50-action mapping, missing, duplicate, extra, and hostile typed rows, immutability, and planned-action non-executability. -- The complete isolated Alembic suite passed 16 tests in 587.24 seconds at - runtime SHA `478a819`. It proves upgrade/downgrade/re-upgrade, historical-row +- The complete isolated Alembic suite passed 16 tests in 503.16 seconds at + runtime SHA `3365e67`. It proves upgrade/downgrade/re-upgrade, historical-row preservation, exact restored permission behavior, all forward-evidence refusal paths, and the concurrent insert lock. -- Direct SQL accepts all 30 exact action/permission pairs as denied evidence, - rejects all 30 wrong registered-permission pairs, and rejects all 24 new +- Direct SQL accepts all 50 exact action/permission pairs as denied evidence, + rejects all 50 wrong registered-permission pairs, and rejects all 25 new permissions without a mapped action. -- External-review repair proves typed validation rejects allowed evidence for - all 30 planned actions while PostgreSQL accepts all 30 exact allowed pairs as +- Amendment proof confirms typed validation rejects allowed evidence for all 50 + planned actions while PostgreSQL accepts all 50 exact allowed pairs as availability-neutral storage. The targeted isolated migration test passed in - 100.95 seconds at `6287f57`. + 43.53 seconds at runtime SHA `3365e67`. - Ruff, stale Workstream wording, stale authorization documentation, changed Markdown links, loop-memory state, and diff integrity passed. - No workflow, dependency, test skip, coverage exclusion, package script, or @@ -62,8 +62,11 @@ lifecycle/merge-intent files, records the evidence accurately, and keeps External repair review at `6287f57` confirmed CodeRabbit's proposed denial-only SQL constraint would contradict the approved availability-neutral migration -contract. The accepted grammar fix and expanded PR description introduce no -runtime authority, and no prior negative behavior proof was weakened. +contract. The review/revision amendment at `3365e67` adds one permission and 20 +planned action dependencies without runtime authority. Exact-head repair +`160af8a` removes duplicate session authority: the request-scoped service binds +the caller-owned session once and exposes only +`require(action_id, typed_resource_context)`. Valid findings addressed: yes @@ -71,5 +74,5 @@ Open sub-agent sessions: none ## Remaining Gate -GitHub Backend, Agent Gates, CodeRabbit, and explicit human merge approval remain -pending. Do not start `WS-AUTH-001-07B` automatically. +PR #126, GitHub Backend, Agent Gates, CodeRabbit, and explicit human merge +approval remain pending. Do not start `WS-AUTH-001-07B` automatically. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md index e2f14f89d..8b45fedde 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md @@ -6,14 +6,16 @@ ## Goal -Install one closed typed source for all 73 approved PermissionIds and exactly 30 +Install one closed typed source for all 74 approved PermissionIds and exactly 50 planned ActionIds, then preserve exact typed/PostgreSQL audit parity without making any action executable. ## What Changed - Added a dependency-free authorization catalogue containing the exact 49 - historical and 24 new permissions plus 30 four-field planned action rows. + historical and 25 new permissions plus 50 four-field planned action rows. +- Reserved canonical `submission.create` plus 19 review actions. Revision + submission reuses `submission.create`; no revision-specific permission exists. - Made authority audit validation consume the catalogue, enforce exact action- permission mapping, reject planned allowed decisions, and bound hostile input. - Added nullable `audit_events.action_id` and PostgreSQL constraints for exact @@ -35,8 +37,8 @@ and project contexts. ## Acceptance Proof -- The independent runtime test literals prove the exact 73-value permission set - and its exact 49/24 historical/new partition. +- The independent runtime test literals prove the exact 74-value permission set + and its exact 49/25 historical/new partition. - Catalogue startup rejects missing, duplicate, extra, unknown, invalid-owner, invalid-availability, and metadata-mismatched rows. - Planned ActionIds cannot resolve as executable and cannot be recorded as @@ -50,10 +52,11 @@ and project contexts. ## Tests And Coverage - 37 focused authorization/audit behavior tests passed on isolated PostgreSQL. -- Authorization branch-aware coverage: 94 percent. +- Authorization branch-aware coverage: 95 percent. - Audit branch-aware coverage: 93 percent. -- Full isolated Alembic suite: 16 passed in 587.24 seconds at reviewed runtime - SHA `478a819236b9cff1e1d7b61203015691ce0aaf45`. +- Full isolated Alembic suite: 16 passed in 503.16 seconds at reviewed runtime + SHA `3365e67e7b44195069a5c7645fdee0af1d4e0180`. +- Targeted exhaustive migration proof: 1 passed in 43.53 seconds. - Catalogue/startup matrix: 10 passed. - Ruff, stale wording, authorization-doc consistency, Markdown links, loop-memory state, and diff integrity passed. @@ -63,8 +66,8 @@ this PR does not change any coverage threshold or exclusion. ## Reviewer Results -Reviewed external-repair head `6287f57936c5e1ec7621bcbf07ff45ee40f0ee91` -and original runtime candidate `478a819236b9cff1e1d7b61203015691ce0aaf45` passed +Reviewed exact head `160af8afd030f042ee72ec963e6f47cd8b7d4c9a` +and runtime amendment `3365e67e7b44195069a5c7645fdee0af1d4e0180` passed senior engineering, QA/test, security/auth, product/ops, architecture, CI integrity, docs, reuse/dedup, and test-delta review with no remaining findings. @@ -80,7 +83,7 @@ integrity, docs, reuse/dedup, and test-delta review with no remaining findings. ## Human Review Focus -Review the exact 73/30 catalogue, planned-only typed behavior, exact SQL mapping, +Review the exact 74/50 catalogue, planned-only typed behavior, exact SQL mapping, historical audit preservation, four downgrade refusal paths, exclusive locking, and the absence of kernel/grant/resource activation. From b23c7c9c594169f29ba69cb6e5ba9145802cfcdd Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 15 Jul 2026 14:01:33 +0100 Subject: [PATCH 16/19] Bind AUTH-07A evidence to reviewed head --- .../reviews/WS-AUTH-001-07A-internal-review-evidence.md | 5 +++-- .../reviews/WS-AUTH-001-07A-pr-trust-bundle.md | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md index deae91357..d377a7da7 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md @@ -1,6 +1,6 @@ # WS-AUTH-001-07A Internal Review Evidence -Reviewed code SHA: `160af8afd030f042ee72ec963e6f47cd8b7d4c9a` +Reviewed code SHA: `f5af7986c7b85a4b45fbf21ee9f0a265c5c45177` Reviewed runtime SHA: `3365e67e7b44195069a5c7645fdee0af1d4e0180` Reviewed at: `2026-07-15T12:51:26Z` Reviewer run IDs: `auth06_final_ci`, `auth06_final_docs`, @@ -66,7 +66,8 @@ contract. The review/revision amendment at `3365e67` adds one permission and 20 planned action dependencies without runtime authority. Exact-head repair `160af8a` removes duplicate session authority: the request-scoped service binds the caller-owned session once and exposes only -`require(action_id, typed_resource_context)`. +`require(action_id, typed_resource_context)`. Evidence-only head `f5af798` then +passed exact lifecycle, docs, and test-evidence review. Valid findings addressed: yes diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md index 8b45fedde..c0aa893b5 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md @@ -66,7 +66,7 @@ this PR does not change any coverage threshold or exclusion. ## Reviewer Results -Reviewed exact head `160af8afd030f042ee72ec963e6f47cd8b7d4c9a` +Reviewed exact evidence head `f5af7986c7b85a4b45fbf21ee9f0a265c5c45177` and runtime amendment `3365e67e7b44195069a5c7645fdee0af1d4e0180` passed senior engineering, QA/test, security/auth, product/ops, architecture, CI integrity, docs, reuse/dedup, and test-delta review with no remaining findings. From 6f1b7ce5171d763cff2b5d6393784f80d4248187 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 15 Jul 2026 14:26:29 +0100 Subject: [PATCH 17/19] Reconcile merged artifact checkpoint --- .agent-loop/LOOP_STATE.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.agent-loop/LOOP_STATE.md b/.agent-loop/LOOP_STATE.md index 3ca554dbc..62150a604 100644 --- a/.agent-loop/LOOP_STATE.md +++ b/.agent-loop/LOOP_STATE.md @@ -29,8 +29,10 @@ `review.queue.override` permission. AUTH-07 registers them, AUTH-08 defines applicable Operator grants, AUTH-09 provisions fixed service principals, and each owning WS-ART feature chunk activates only its own canonical actions. -- Next artifact candidate: `WS-ART-001-02A1` remains inactive until the user - gives a separate explicit start signal. +- Parallel artifact checkpoint: `WS-ART-001-02A1` was explicitly started and + merged through PR #127 as `f64a8e5`; it is at the post-merge memory/stop + checkpoint. `WS-ART-001-02A2` remains inactive until signed memory completes + and the user gives a separate explicit start signal. - Authorization checkpoint: `WS-AUTH-001-06` merged through PR #124 as `f599551`. The user separately started parent `WS-AUTH-001-07`; required L1 review split it into 07A/07B before runtime implementation. AUTH-07B and From 38b11040c09ab0c589aad7f329331c3930bf2733 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 15 Jul 2026 14:31:55 +0100 Subject: [PATCH 18/19] Bind AUTH evidence to latest main merge --- .../reviews/WS-AUTH-001-07A-internal-review-evidence.md | 6 ++++-- .../reviews/WS-AUTH-001-07A-pr-trust-bundle.md | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md index d377a7da7..0edd47b63 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-internal-review-evidence.md @@ -1,6 +1,6 @@ # WS-AUTH-001-07A Internal Review Evidence -Reviewed code SHA: `f5af7986c7b85a4b45fbf21ee9f0a265c5c45177` +Reviewed code SHA: `6f1b7ce5171d763cff2b5d6393784f80d4248187` Reviewed runtime SHA: `3365e67e7b44195069a5c7645fdee0af1d4e0180` Reviewed at: `2026-07-15T12:51:26Z` Reviewer run IDs: `auth06_final_ci`, `auth06_final_docs`, @@ -67,7 +67,9 @@ planned action dependencies without runtime authority. Exact-head repair `160af8a` removes duplicate session authority: the request-scoped service binds the caller-owned session once and exposes only `require(action_id, typed_resource_context)`. Evidence-only head `f5af798` then -passed exact lifecycle, docs, and test-evidence review. +passed exact lifecycle, docs, and test-evidence review. Latest-main merge and +checkpoint repair head `6f1b7ce` then passed merge-integrity, architecture, QA, +CI, lifecycle, and docs confirmation without AUTH runtime changes. Valid findings addressed: yes diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md index c0aa893b5..19bf072a6 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-pr-trust-bundle.md @@ -66,7 +66,7 @@ this PR does not change any coverage threshold or exclusion. ## Reviewer Results -Reviewed exact evidence head `f5af7986c7b85a4b45fbf21ee9f0a265c5c45177` +Reviewed exact merged head `6f1b7ce5171d763cff2b5d6393784f80d4248187` and runtime amendment `3365e67e7b44195069a5c7645fdee0af1d4e0180` passed senior engineering, QA/test, security/auth, product/ops, architecture, CI integrity, docs, reuse/dedup, and test-delta review with no remaining findings. From 3ab25cf3b1e99336c635a318101375bb4bebdf91 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 15 Jul 2026 14:42:35 +0100 Subject: [PATCH 19/19] Update AUTH action review evidence count --- .../reviews/WS-AUTH-001-07A-external-review-response.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-external-review-response.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-external-review-response.md index e55cac1d6..1f4491423 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-external-review-response.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-07A-external-review-response.md @@ -11,7 +11,7 @@ CodeRabbit run: `920d63e5-f8b3-4525-9e61-18b44373ed1d` 1. Corrected the specification compound noun from `self actions` to `self-actions`. 2. Made the approved availability boundary explicit in code and behavior - evidence. Typed validation rejects allowed evidence for every one of the 30 + evidence. Typed validation rejects allowed evidence for every one of the 50 currently planned actions. PostgreSQL accepts exact registered allowed pairs because migration `0021` is intentionally availability-neutral across later owner activation. @@ -26,7 +26,7 @@ able to activate its two self-actions through reviewed typed code without an unowned migration `0022`. PostgreSQL therefore owns stable identifier, exact mapping, and decision-event shape; typed catalogue validation owns temporal availability. The direct SQL and typed tests now prove both sides of that -boundary for all 30 actions. +boundary for all 50 actions. ## Non-Actionable Review Output