diff --git a/.agent-loop/REVIEW_LOG.md b/.agent-loop/REVIEW_LOG.md index e6bd76f0d..a935506df 100644 --- a/.agent-loop/REVIEW_LOG.md +++ b/.agent-loop/REVIEW_LOG.md @@ -2719,3 +2719,36 @@ any branch-policy-required renewed approval remain after publication. The later documentation-only audit reconciliation is reviewed independently and bound by the chunk's internal-review evidence; it does not replace `acc15aa5` as the canonical implementation and closeout evidence head. +## 2026-07-21 - WS-AUTH-001-10 Internal Plan Review + +The inherited combined AUTH-10 runtime contract failed L1 plan review because +it mixed migration/evidence, privacy-sensitive reads, and PREP-bound mutations. +The user approved a sequential 10A/10B/10C design. The repaired plan freezes +independent submitter/reviewer/adjudicator grants, exact API and privacy shapes, +deterministic lock/replay behavior, migration refusal predicates, and planned +catalogue/evidence custody without activating a route or changing runtime. + +Planning candidate `ca52fd6a6c51f78b3e3a10faf77f4ab235843ad2` passes +senior engineering, QA/test, security/auth, product/ops, architecture, CI +integrity, docs, reuse/dedup, and test-delta review with no open finding. Stale +authorization wording, Markdown links, merge-intent validation, and diff +integrity pass. GitHub Agent Gates, CodeRabbit, and human review remain; 10A +requires a separate signed start after this planning parent merges. + +## 2026-07-21 - WS-AUTH-001-10 External Gate Repair + +PR #168 preflight rejected a noncanonical internal-evidence SHA heading, while +Agent Gates rejected branch-local live state in AUTH `STATUS.md`. The repair +uses the required `Reviewed code SHA` provenance label and restores the authored +status file to trusted-main state; signed automation remains the sole owner of +live active/review projection. The Backend aggregate failure was consequential: +preflight blocked shards and API E2E, so no runtime test failed. All 88 Agent +Gate regression tests pass locally. Exact-SHA internal repair review, an +evidence-only binding descendant, fresh GitHub checks, and CodeRabbit remain. + +CodeRabbit later posted five valid planning/specification findings. The repair +adds review-log scope to the parent contract, persists the exact active-1/ +revoked-2 grant version, keeps child actions planned until their owning child +activates them with routes, records the complete PREP/idempotency/advisory-lock +issue order, and makes `/api/v1` the sole endpoint namespace. Exact-SHA internal +review and fresh external checks remain. 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 59ed1bdae..cf5125704 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 @@ -40,7 +40,10 @@ stopped. | `WS-AUTH-001-ART-CUSTODY` | ART Activation Custody Transfer | L1 | Merged through PR #158 as `be2a79a2`; all 25 ART actions remain planned | | `WS-AUTH-001-REV-CUSTODY` | REV Activation Custody Transfer | L1 | Merged through PR #160 as `fe0e4492`; all 19 REV actions remain planned | | `WS-AUTH-001-PREP` | Prepared Mutation Authorization Protocol | L1 | Merged through PR #162 as `c559d556`; no feature consumer or activation | -| `WS-AUTH-001-10` | Project Qualification And Contributor Role Grants | L1 | Proposed | +| `WS-AUTH-001-10` | Project Qualification And Contributor Role Grants | L1 | Active planning-only parent; split approved after failed L1 combined review | +| `WS-AUTH-001-10A` | Project Role Grant Data And Evidence Foundation | L1 | Proposed successor; migration `0031`, no active surface | +| `WS-AUTH-001-10B` | Project Role Grant Read And Candidate Surfaces | L1 | Proposed after 10A | +| `WS-AUTH-001-10C` | Project Role Grant Mutations | L1 | Proposed after 10B | | `WS-AUTH-001-11` | Project Identity, Guide, Source, And Visibility Cutover | L1 | Proposed | | `WS-AUTH-001-12` | Project Policy And Setup Mutation Cutover | L1 | Proposed | | `WS-AUTH-001-13` | Task Management And Assignment Cutover | L1 | Proposed | @@ -103,6 +106,9 @@ WS-AUTH-001-PLAN -> WS-AUTH-001-ART-CUSTODY and WS-AUTH-001-REV-CUSTODY -> WS-AUTH-001-PREP -> WS-AUTH-001-10 +-> WS-AUTH-001-10A +-> WS-AUTH-001-10B +-> WS-AUTH-001-10C -> WS-AUTH-001-11 -> WS-AUTH-001-12 -> WS-AUTH-001-13 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 cb9f65f05..ca618f4a3 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 @@ -684,3 +684,35 @@ The foundation allocates only the then-current next migration from trusted `main`. This decision supersedes only the future fixed-number reservation clauses in D29, D30, and earlier decisions; their other boundaries remain in force. Merged migration ownership through AUTH-09D-A `0026` is unchanged. +## D32: Split project-role truth, reads, and mutations + +Status: accepted by the user on 2026-07-21 after required AUTH-10 L1 plan +review rejected the combined runtime boundary. + +AUTH-10 is a planning-only parent and splits into exactly three sequential +same-initiative children. AUTH-10A owns migration +`0031_project_role_grants`, the immutable qualification/grant aggregates, and +the typed/PostgreSQL three-role evidence clean cut. It registers all five +project-role actions as planned with exact AUTH-10B/AUTH-10C owners but activates +no action or route. AUTH-10B owns activation of exactly the candidate, +grant-list, and grant-detail read actions and privacy-safe pagination. AUTH-10C +owns activation of exactly issue and revoke, +multi-principal PREP ordering, idempotency, audit, invalidation, and mutation +concurrency. Each child requires a separate signed start and stop. + +The project roles are independent `submitter`, `reviewer`, and `adjudicator` +rows. `both`, replacement, automated issuance, conversion, and one-active-role- +total semantics are removed from current contracts. Discovery and issuance are +allowed only for draft, active, and paused projects. Historical grant reads and +revocation remain possible for every existing project state so evidence cannot +be hidden and authority cannot become irremovable. + +The current trusted-main Alembic head is `0030`; this decision assigns `0031` +only to AUTH-10A and supersedes every older inactive AUTH-10 migration-number +reservation in D21, D27, D28, D29, and D30. Historical migration ownership is +unchanged. + +`GET /api/v1/actors/me/authorization-context` moves to AUTH-11. AUTH-10B does +not expose a context that advertises still-inactive project/task/review actions; +AUTH-11 must enumerate that surface with its exact ActionId, target, guards, +and disclosure contract before implementation. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DISCOVERY.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DISCOVERY.md index a1240fe4b..ed5aae3be 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DISCOVERY.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/DISCOVERY.md @@ -3,6 +3,46 @@ Discovery is read-only. No application code was changed while producing this artifact. +## AUTH-10 delta discovery - 2026-07-21 + +Trusted `main` is `5a8a924d`; Alembic head is +`0030_artifact_verification_fencing`, making `0031` the only current migration +candidate. PREP is merged and supports actor-self plus active administrative +mutations, but deliberately rejects ProjectRoleGrant preparation because no +project grant table, resource context, or evaluator branch exists. + +The permission catalogue already contains `project.role_grant.read` and +`project.role_grant.manage`. Project Manager has both permissions and may be +system- or exact-project-scoped; Audit Authority has read only. No matching +ProjectRoleGrant ActionIds or owner exist. Five surfaces therefore require five +new action declarations: candidate list, grant list, grant detail, issue, and +revoke. The existing authorization router is mounted directly under +`/api/v1`, and AdminRoleGrant routes provide the closest route/idempotency/audit +convention, but AUTH-10 mutations must use PREP rather than copying their older +single-phase `require()` flow. + +`ActorProfile` already contains the only candidate fields AUTH-10 may expose: +canonical ID and nullable display name. It also contains contact and lifecycle +metadata that candidate responses must not disclose. `ActorIdentityLink` is +one-to-one and supplies the active-link eligibility predicate without exposing +issuer, subject, or link metadata. `ProjectRepository.get_project(..., +for_update=...)` is the canonical project loader; AUTH must not add a duplicate +project repository. + +Typed audit/idempotency foundations already reserve project issue/revoke +operations, but still admit superseded `both`, `ProjectRoleGrantReplaced`, and +replacement evidence. AUTH-10 must replace those current validators in typed +code and migration `0031`, while leaving historical migrations immutable and +refusing unsafe upgrade/downgrade rather than converting evidence. + +The inherited contract combined schema, five active routes, candidate privacy, +PREP extension, idempotency, audit/invalidation, concurrency, and migration +round-trip proof without the D27 surface inventory. Required L1 review rejected +that combined boundary and identified crossed-manager lock ordering, replay, +privacy-shape, lifecycle, reference-spec, and local-versus-hosted proof gaps. +The user approved the resulting 10A durable-truth, 10B read, and 10C mutation +split on 2026-07-21. D32 records the exact boundaries and successor order. + ## AUTH-09 delta discovery - 2026-07-16 Merged AUTH-08 provides `AuthorityControl`, immutable AdminRoleGrants, seven diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/INTENT.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/INTENT.md index dae8de64e..8aed7b144 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/INTENT.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/INTENT.md @@ -163,6 +163,10 @@ Resolved: on 2026-07-11 after planning and post-merge memory closed. - D20-D22 establish service ActorProfiles, independent three-role project grants, and fixed service runtime admission. +- D32 records the user-approved AUTH-10A/10B/10C split: durable truth first, + privacy-safe reads second, and PREP-bound mutations third. It assigns current + migration `0031` to 10A and defers actor authorization-context disclosure to + AUTH-11. External deployment details such as issuer URL, JWKS URL, approved algorithms, claim names, and introspection policy are configuration inputs. Their absence 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 8354e48b5..6c3275753 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 @@ -191,9 +191,12 @@ proving the same issuer role metadata alone no longer authorizes. 11. Transfer all current ART/REV owner labels to exact AUTH custodians without changing availability, then add the prepared mutation authorization protocol. -12. Implement qualification snapshots and independent contributor grants scoped - to the exact project, including the typed/PostgreSQL clean cut from `both` - and replacement evidence. +12. Close planning parent AUTH-10, then deliver project-role authority in three + separately started children: 10A establishes migration `0031`, immutable + snapshots/grants, the typed/PostgreSQL three-role clean cut, and all five + planned action rows with exact 10B/10C owners but no active surface; 10B + activates candidate/list/detail reads; 10C activates PREP-bound + issue/revoke mutations and concurrency proof. 13. Cut project identity, guide, source, and visibility queries over to local permissions. 14. Cut project policy mutations, approvals, activation, and setup operations diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-10-project-role-grants.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-10-project-role-grants.md index 97e2192db..bb35c7bb4 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-10-project-role-grants.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-10-project-role-grants.md @@ -1,190 +1,107 @@ -# Chunk Contract: WS-AUTH-001-10 - Project Qualification And Contributor Role Grants +# Chunk Contract: WS-AUTH-001-10 — Project Qualification And Contributor Role Grants ## Status -Proposed and inactive. Before implementation review, this contract must add the -exact ActionId/target/guard inventory required by D27. AUTH-PREP is a hard -runtime prerequisite. +Active planning-only parent. Signed start event +`github-actions:29815937933:start` activated this exact chunk. Required L1 plan +review rejected the combined runtime scope at `5ad6e116`; the user approved the +10A/10B/10C design on 2026-07-21. This parent changes planning and contracts +only and names `WS-AUTH-001-10A` as its same-initiative successor. ## Parent initiative -`WS-AUTH-001` - Workstream Authorization Service +`WS-AUTH-001` — Workstream Authorization Service ## Goal -Implement immutable qualification snapshots and independent -`ProjectRoleGrant(submitter|reviewer|adjudicator)` create, list, and revoke -behavior scoped to the exact project under Project Manager authority. +Freeze the reviewed three-stage delivery design for independent project-role +authority before any AUTH-10 runtime or migration edit. ## Why this chunk exists -Project contributor authority must be durable, explicit, revocable, and -separate from both token claims and administrative roles. - -## Approved plan reference - -- 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` +The inherited chunk mixed durable schema, a validator clean cut, five active +surfaces, privacy-sensitive pagination, PREP multi-principal locking, mutations, +and concurrency proof. Those boundaries require separate review and rollback. ## Risk class -L1 +L1 planning and authorization architecture. ## SLA P1 +## Approved split + +1. `WS-AUTH-001-10A` — Project Role Grant Data And Evidence Foundation. +2. `WS-AUTH-001-10B` — Project Role Grant Read And Candidate Surfaces. +3. `WS-AUTH-001-10C` — Project Role Grant Mutations. + +Each child requires its own signed start, internal review, merge intent, hosted +checks, human merge approval, signed merge memory, and stop. No child begins +automatically. + ## Allowed files ```text -backend/app/modules/actors/** -backend/app/modules/authorization/** -backend/app/modules/projects/models.py -backend/app/modules/projects/repository.py -backend/app/api/router.py -backend/app/db/models.py -backend/app/modules/audit/** -backend/alembic/versions/_*.py -backend/tests/test_actors.py -backend/tests/test_projects.py -backend/tests/test_auth.py -backend/tests/test_alembic.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/merge-intents/WS-AUTH-001-10.json -.agent-loop/LOOP_STATE.md -.agent-loop/WORK_QUEUE.md .agent-loop/REVIEW_LOG.md +docs/reference_specs/WS-AUTH-001-actor-profile-role-and-authorization-service-specification.md ``` -## Not allowed +## Not allowed changes ```text -automated grants from skills or reputation -self-grant or self-revoke of the issuer's own contributor grant through the -administrative project-grant operation -admin roles satisfying submitter/reviewer/adjudicator permissions -task/review lifecycle implementation -project/task/checker authorization cutover -`both`, compatibility alias, replacement event/reason, `replaced_grant_id`, or -silent conversion of combined/replacement evidence -editing migrations `0018`, `0019`, or `0022` +backend or frontend runtime +database models or migrations +ActionId or permission availability +API routes or schemas +CI workflows or dependencies +starting AUTH-10A before this parent merges and signed memory stops ``` ## Acceptance criteria -- Snapshot is immutable, actor/project/requested-role-bound, privacy-bounded, - and records unavailable evidence explicitly. -- Qualification snapshots and ProjectRoleGrants are owned by the authorization - module; ActorProfile/IdentityLink remain actor-owned, and ProjectRepository - remains the canonical project loader. No duplicate grant/project repository - is introduced. -- Only a Project Manager whose active grant covers the project can issue or - revoke a contributor grant. -- The same covered `project.role_grant.manage` permission provides a scoped, - paginated contributor-candidate lookup for the grant workflow. It returns - only minimal actor fields, filters unauthorized rows before totals/cursors, - cannot enumerate unrelated project activity, and never exposes issuer - subject, identity-link metadata, contact data, skills, or reputation as - authority. -- Target must be an active human and cannot be the issuing manager. -- A manager who separately holds a contributor grant cannot revoke that grant - through their own administrative request; denial is stable and audited. -- A partial unique index on `(actor_profile_id, project_id, role) WHERE status = - 'active'` permits at most one active grant for the same exact role while a - contributor may hold active submitter, reviewer, and adjudicator rows - concurrently. -- Issue never revokes another role. Regrant after revocation creates a new - immutable row. -- Typed schemas, audit facts, idempotency evidence, and current PostgreSQL - validators accept only `submitter`, `reviewer`, and `adjudicator`; only issued - and revoked success events remain. `both`, replacement fields/events/reasons, - aliases, and conversion branches are absent. -- Snapshot ownership is composite across snapshot ID, actor, project, and exact - requested role. There is no replacement/supersession column. -- Create and revoke require canonical request hashing: same key and - same request returns the committed graph; same key with different request is - rejected. -- Issue hashing includes the exact requested role. Same key/different role is - `idempotency_mismatch`; a new-key duplicate same-role issue is a stable audited - conflict; distinct keys may issue different roles concurrently. Revoke derives - role from the locked grant and replay reloads/re-authorizes before disclosure. -- State, idempotency result, audit event, and invalidation event commit in one - transaction. -- Only manual creation is enabled; automated schema value cannot be emitted. -- Revocation is visible on the next authorization context build. -- Revocation evidence and invalidation identify the exact revoked role; - downstream consumers reconcile only the matching task, review, or future - adjudication responsibility. -- The linked invalidation retains exact grant and cause-event references. A - submitter revocation creates only the task-assignment obligation and reviewer - revocation may create only its exact REV-owned review obligation. Adjudicator - revocation persists exact invalidation only and creates or consumes no - adjudication obligation until that separately approved lifecycle is active. - No path changes another project role or an AdminRoleGrant. -- Project manager/admin role alone never creates contributor capability. -- PostgreSQL concurrency tests cover identical-role creates, concurrent - different-role creates, regrant versus revoke, and revocation versus - authorization. -- `POST/GET /api/v1/projects/{project_id}/role-grants`, grant detail, and grant - revoke routes have multi-role, self-revoke, scope, privacy, rate-limit, - replay, and negative tests. -- Every protected grant/candidate route declares one active `ActionId` mapped to - `project.role_grant.read` or `project.role_grant.manage` against the - canonically loaded project/grant target. Generated manifest-delta tests prove - every surface introduced here has exactly one declaration. -- Contributor-candidate lookup has covered-manager allow, uncovered/cross- - project deny, pagination/count concealment, minimal-field, rate-limit, and - inactive/non-human exclusion tests; no UUID must be recovered from logs or - direct database access. -- The then-current migration enforces exact three-role checks, composite snapshot/grant - ownership, partial unique/supporting indexes, database-time fields, and - immutability. It refuses upgrade when obsolete combined or - replacement evidence exists and never converts or deletes those rows. It - replaces current audit/idempotency validators without editing historical - migrations and refuses an unsafe downgrade without mutating evidence. - Prior-head, fresh replay, preserved history, and both refusal paths are tested. +- D32 records the user-approved 10A/10B/10C design and exact dependency order. +- Every child has a complete contract with allowed files, exclusions, + acceptance criteria, verification, reviewers, human focus, and stop rules. +- Migration `0031` belongs only to 10A; 10B and 10C add no migration. +- 10A registers all five actions as planned with exact 10B/10C owner values and + activates no action or route; 10B owns activation of exactly three read + actions; 10C owns activation of exactly two mutation actions. +- Project lifecycle behavior is frozen: discovery and issuance allow draft, + active, and paused projects; list/detail and revocation remain available for + every existing project state so evidence is inspectable and authority can + always be removed. +- `/api/v1/actors/me/authorization-context` is explicitly deferred to AUTH-11, + where the first complete project read cutover can expose useful contributor + context without advertising inactive task/review actions. +- The canonical reference specification removes `both`, replacement, automated + issuance, one-active-role-total, and noncanonical AUTH-10 route prefixes. +- Exactly one merge intent names 10A as successor with explicit start required. ## Verification commands ```bash -(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 && .venv/bin/python -m ruff check app tests) -(cd backend && WORKSTREAM_DATABASE_URL= .venv/bin/python -m pytest -q \ - tests/test_authorization.py tests/test_auth.py tests/test_actors.py \ - tests/test_projects.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_stale_authorization_docs.py python3 scripts/check_markdown_links.py +python3 scripts/update_post_merge_memory.py validate-merge-intent --base-ref origin/main git diff --check ``` ## Required reviewers -- senior engineering -- QA/test -- security/auth -- product/ops -- architecture -- CI integrity -- docs -- reuse/dedup -- test delta +Senior engineering, QA/test, security/auth, product/ops, architecture, CI +integrity, docs, reuse/dedup, and test delta. ## Human review focus -Review exact-project scope, self-grant protection, role-specific snapshot -privacy, independent issue/revoke semantics, and absence of implicit grants. +Confirm the three boundaries, exact successor order, migration custody, +authorization-context deferral, and absence of runtime changes. ## Stop conditions -Stop if contributor authority depends on a token role, inferred qualification, -project ID supplied without canonical database resolution, compatibility for -`both`, evidence conversion, or a mutation path that bypasses AUTH-PREP. +Stop if the planning PR changes runtime, combines child boundaries again, +starts a child automatically, or names a cross-initiative successor. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-10A-project-role-grant-data-evidence.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-10A-project-role-grant-data-evidence.md new file mode 100644 index 000000000..2e6a8b216 --- /dev/null +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-10A-project-role-grant-data-evidence.md @@ -0,0 +1,174 @@ +# Chunk Contract: WS-AUTH-001-10A - Project Role Grant Data And Evidence Foundation + +## Parent initiative + +`WS-AUTH-001` — Workstream Authorization Service + +## Status and prerequisite + +Proposed and inactive. Start only after planning parent AUTH-10 merges, signed +memory names 10A, and a fresh explicit start event activates this exact child. + +## Goal + +Create the immutable qualification-snapshot and independent three-role grant +truth plus typed/PostgreSQL evidence parity and planned action registrations, +without exposing a route or active action. + +## Why this chunk exists + +Reads and mutations must depend on database-enforced ownership and exact-role +history rather than shipping schema and behavior together. + +## Risk class + +L1 schema, authorization evidence, and privacy. + +## SLA + +P1 + +## Allowed files + +```text +backend/app/modules/authorization/models.py +backend/app/modules/authorization/schemas.py +backend/app/modules/authorization/repository.py +backend/app/modules/authorization/catalogue.py +backend/app/modules/audit/** +backend/app/db/models.py +backend/alembic/versions/0031_project_role_grants.py +backend/tests/test_authorization.py +backend/tests/test_alembic.py +docs/operations_authorization_service.md +docs/spec_authorization_service.md +.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/** +.agent-loop/merge-intents/WS-AUTH-001-10A.json +.agent-loop/REVIEW_LOG.md +``` + +## Not allowed changes + +```text +API routes or OpenAPI declarations +active ActionDefinition rows or callable action behavior +authorization kernel or PREP behavior +candidate/list/detail/issue/revoke services +project/task/review lifecycle behavior +`both`, replacement, automated issuance, aliases, or evidence conversion +editing a historical migration +``` + +## Exact durable contract + +`ProjectRoleQualificationSnapshot` contains `id`, `project_id`, +`actor_profile_id`, exact `requested_role`, two structured availability +snapshots, bounded prior-work UUID references, bounded external-expertise +reference tokens, capturer profile/grant provenance, and database capture time. + +Each availability snapshot is exactly: + +```text +availability: available | unavailable +reference_ids: array[string], 0..20 items, each matching + ^[A-Za-z0-9][A-Za-z0-9._:/-]{0,119}$ and containing no `://` +unavailable_reason: not_collected | source_unavailable | no_record | null +``` + +`available` requires one or more reference IDs and null reason. `unavailable` +requires no references and one reason. Prior-work references are 0..20 UUIDs. +External-expertise references are 0..20 tokens with the same grammar. Issue and +revoke reasons are 1..500 UTF-8 bytes, must equal Python `str.strip()`, and +reject Unicode control characters. No free-form evidence narrative, score, contact field, issuer subject, raw claim, +secret, URL credential, or automatically inferred authority is stored. + +`ProjectRoleGrant` is one immutable issuance row for exactly `submitter`, +`reviewer`, or `adjudicator`, with active/revoked lifecycle, manual method, +composite snapshot reference, issuer profile/grant provenance, bounded reason, +database timestamps, terminal revocation provenance, and a persisted integer +`version`. The invariant is exact: active grants persist version 1 and revoked +grants persist version 2. Only the active-version-1 to revoked-version-2 +transition may mutate lifecycle fields. + +## Acceptance criteria + +- Migration `0031` creates both authorization-owned tables; 10A adds no route, + active action, or callable behavior. +- Composite key `(snapshot_id, actor_profile_id, project_id, requested_role)` + is referenced by the matching grant facts. +- Partial uniqueness permits one active actor/project/exact-role row while all + three distinct roles may coexist. +- Regrant after revocation creates a new row; issuance provenance is immutable. +- Typed and PostgreSQL audit/idempotency validators accept only the three exact + roles and issued/revoked success events. Replacement fields/events/reasons + and `both` are absent. +- 10A adds the five `ActionId` enum members and closed `ActionDefinition` rows + below with `ActionAvailability.PLANNED`; it adds exact `ActionOwner.AUTH_10B` + and `ActionOwner.AUTH_10C` enum values and assigns each row to its named + future owner. Migration `0031` reserves matching PostgreSQL evidence parity. + No route or callable behavior is added: + + ```text + project.contributor_candidate.list -> project.role_grant.manage -> AUTH_10B + project_role_grant.list -> project.role_grant.read -> AUTH_10B + project_role_grant.read -> project.role_grant.read -> AUTH_10B + project_role_grant.issue -> project.role_grant.manage -> AUTH_10C + project_role_grant.revoke -> project.role_grant.manage -> AUTH_10C + ``` + + It likewise reserves exactly the future denial codes + `project_role_grant_already_revoked` and + `project_role_grant_replay_state_changed` in the typed and PostgreSQL denial + vocabularies. Planned registration does not make an action active; 10B and + 10C own the transition to active availability, routes, and emissions for + their respective rows. +- Migration and schema tests prove all five pairs and both denial codes are + admitted, a neighboring unreserved value is rejected, every new catalogue row + is planned with its exact owner, and no route/OpenAPI surface exists in 10A. +- Upgrade inspects exact existing storage and refuses before DDL when any + `audit_events` row with `event_domain='authority'` has + `before_facts->>'role'='both'`, `after_facts->>'role'='both'`, a + `replaced_grant_id` key in either facts object, + `event_type='ProjectRoleGrantReplaced'`, or + `reason='authority_replacement'`. It also refuses any + `authority_idempotency_records.operation` in + `('project_role_grant.issue','project_role_grant.revoke')`, because those + records contain only a digest/resource reference and cannot prove an + independent-role request safely. No other replacement/supersession key or + fuzzy reason search is implied. +- Downgrade refuses before DDL when either new table contains any row or an + authority audit row has `before_facts->>'role'='adjudicator'` or + `after_facts->>'role'='adjudicator'`. Each individual predicate and their + combined form have transaction-level no-mutation proof. +- Fresh install, prior-head upgrade, downgrade/refusal, replay, constraints, + immutability, and preserved unrelated history are proven on PostgreSQL. +- No migration, model, schema, or fixture accepts automated creation. + +## Verification commands + +```bash +(cd backend && .venv/bin/python -m ruff check app/modules/authorization app/modules/audit tests/test_authorization.py tests/test_alembic.py) +(cd backend && WORKSTREAM_TEST_ADMIN_DATABASE_URL= .venv/bin/python scripts/run_isolated_tests.py --metadata-json --timeout-seconds 300 -- .venv/bin/python -m pytest -q tests/test_alembic.py tests/test_authorization.py -k 'project_role or qualification') +python3 scripts/check_stale_authorization_docs.py +python3 scripts/check_markdown_links.py +git diff --check +``` + +The full suite, all shards, aggregate 78 percent coverage, authorization 90 +percent coverage, API E2E, and Agent Gates run only on GitHub before PR +readiness. + +## Required reviewers + +Senior engineering, QA/test, security/auth, product/ops, architecture, CI +integrity, docs, reuse/dedup, and test delta. + +## Human review focus + +Review privacy shapes, composite ownership, independent-role uniqueness, +immutability, refusal predicates, and absence of exposed behavior. + +## Stop conditions + +Stop if migration data would be converted/deleted, evidence becomes free-form, +an action/route becomes active, or one role affects another. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-10B-project-role-grant-reads.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-10B-project-role-grant-reads.md new file mode 100644 index 000000000..7d1da867e --- /dev/null +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-10B-project-role-grant-reads.md @@ -0,0 +1,143 @@ +# Chunk Contract: WS-AUTH-001-10B - Project Role Grant Read And Candidate Surfaces + +## Parent initiative + +`WS-AUTH-001` — Workstream Authorization Service + +## Status and prerequisite + +Proposed and inactive. Start only after 10A merges, signed memory names 10B, +and a fresh explicit start event activates this exact child. + +## Goal + +Expose privacy-safe contributor candidates and project-role grant history +through three exact read actions after 10A establishes durable truth. + +## Why this chunk exists + +Read disclosure, candidate eligibility, filtering-before-count, and cursor +binding can be reviewed independently from mutation locking. + +## Risk class + +L1 authorization, privacy, and API disclosure. + +## SLA + +P1 + +## Allowed files + +```text +backend/app/modules/actors/repository.py +backend/app/modules/authorization/** +backend/app/modules/projects/repository.py +backend/app/api/router.py +backend/app/core/config.py +backend/tests/test_actors.py +backend/tests/test_authorization.py +backend/tests/test_projects.py +backend/tests/test_config.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/merge-intents/WS-AUTH-001-10B.json +.agent-loop/REVIEW_LOG.md +``` + +## Not allowed changes + +```text +migration or durable schema changes +issue/revoke routes or mutation behavior +PREP extension +task/review/project product cutover +identity-link, contact, skills, reputation, or cross-project activity disclosure +``` + +## Exact surface inventory + +| ActionId | PermissionId | Authority | Canonical target | Surface | +|---|---|---|---|---| +| `project.contributor_candidate.list` | `project.role_grant.manage` | covered Project Manager only | canonical project | `GET /api/v1/projects/{project_id}/contributor-candidates` | +| `project_role_grant.list` | `project.role_grant.read` | covered Project Manager or Audit Authority | canonical project | `GET /api/v1/projects/{project_id}/role-grants` | +| `project_role_grant.read` | `project.role_grant.read` | covered Project Manager or Audit Authority | grant joined to canonical project | `GET /api/v1/projects/{project_id}/role-grants/{grant_id}` | + +All are human-only and registered as planned by 10A with +`ActionOwner.AUTH_10B`. They remain planned and non-callable until this exact +10B child is separately started and implements them; 10B then changes each row +to active atomically with its route. They use non-locking canonical project +reads. System-scoped candidates cover every project only for the retained +permission; project scope must equal the loaded project. Services, agents, and +Space principals deny before lookup. + +## Acceptance criteria + +- Candidate SQL filters active human profile, active link, and caller exclusion + before total/cursor calculation and returns only ID plus nullable display name. +- Candidate discovery allows draft, active, and paused projects and conceals + terminal/archived, nonexistent, and unauthorized projects equivalently. +- Grant list/detail remain readable for every existing project state so + immutable history is inspectable; unauthorized/nonexistent/path-mismatch + cases are indistinguishable before disclosure. +- Candidate accepts only `limit` (default 50, range 1..100) and `cursor` (at + most 512 characters). Its item is exactly `{actor_profile_id, display_name}` + with nullable display name. Grant list accepts only `status` (optional exact + `active|revoked`), `role` (optional exact + `submitter|reviewer|adjudicator`), `limit` (default 50, range 1..100), and + `cursor` (at most 512 characters). Both list envelopes are exactly + `{items, next_cursor}` and intentionally expose no total count. +- Grant list items and detail responses share one strict schema containing + exactly `id`, `project_id`, `actor_profile_id`, `role`, `status`, `version`, + `grant_method`, `qualification_snapshot`, + `granted_by_actor_profile_id`, `granted_by_admin_role_grant_id`, `granted_at`, + `grant_reason`, `revoked_by_actor_profile_id`, `revoked_at`, and + `revoked_reason`. The nested snapshot contains exactly `id`, `requested_role`, + the two 10A availability objects, `prior_project_work_refs`, + `external_expertise_refs`, `captured_by_actor_profile_id`, + `captured_by_admin_role_grant_id`, and `captured_at`. Nullable revocation + fields remain present. Strict schemas reject all other fields, especially + identity issuer/subject/link status, contact data, raw claims, and secrets. +- 10B introduces one shared authorization pagination codec using HMAC-SHA256 and + a required base64-decoded 32-byte + `WORKSTREAM_PAGINATION_CURSOR_HMAC_SECRET`. The versioned cursor payload binds + action, project, status, exact optional role, limit, boundary timestamp/UUID, + and canonical query digest. Signature comparison is constant-time; + malformed/tampered/cross-project/cross-filter cursors reveal neither count + nor rows. The secret is configuration-only, never logged, serialized, stored, + or reused from auth/rate-limit secrets. +- Existing read rate control is reused. Every surface has exactly one active + OpenAPI declaration and manifest-delta proof. +- `/actors/me/authorization-context` remains absent and planned for AUTH-11. +- No write, PREP, idempotency, invalidation, or migration behavior changes. + +## Verification commands + +```bash +(cd backend && .venv/bin/python -m ruff check app/modules/actors app/modules/authorization app/modules/projects app/core/config.py tests/test_actors.py tests/test_authorization.py tests/test_projects.py tests/test_config.py) +(cd backend && WORKSTREAM_TEST_ADMIN_DATABASE_URL= .venv/bin/python scripts/run_isolated_tests.py --metadata-json --timeout-seconds 300 -- .venv/bin/python -m pytest -q tests/test_actors.py tests/test_authorization.py tests/test_projects.py tests/test_config.py -k 'project_role or contributor_candidate or pagination_cursor') +(cd backend && WORKSTREAM_DATABASE_URL= .venv/bin/python scripts/api_contract_e2e.py) +python3 scripts/check_stale_authorization_docs.py +python3 scripts/check_markdown_links.py +git diff --check +``` + +GitHub owns the full sharded suite, aggregate and subsystem coverage, API E2E, +and Agent Gates before PR readiness. + +## Required reviewers + +Senior engineering, QA/test, security/auth, product/ops, architecture, CI +integrity, docs, reuse/dedup, and test delta. + +## Human review focus + +Review scope coverage, filter-before-count, lifecycle concealment, minimal +candidate fields, cursor binding, and absence of mutation behavior. + +## Stop conditions + +Stop on client-supplied project truth, post-query authorization, count leakage, +new persistence, or any contributor capability implied by a read. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-10C-project-role-grant-mutations.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-10C-project-role-grant-mutations.md new file mode 100644 index 000000000..cbdce3001 --- /dev/null +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-10C-project-role-grant-mutations.md @@ -0,0 +1,188 @@ +# Chunk Contract: WS-AUTH-001-10C - Project Role Grant Mutations + +## Parent initiative + +`WS-AUTH-001` — Workstream Authorization Service + +## Status and prerequisite + +Proposed and inactive. Start only after 10B merges, signed memory names 10C, +and a fresh explicit start event activates this exact child. + +## Goal + +Issue and revoke independent project contributor roles through PREP-bound, +idempotent, auditable, exact-project transactions. + +## Why this chunk exists + +Multi-principal locking, absence serialization, replay disclosure, audit, +invalidation, and failure atomicity require a mutation-only security review. + +## Risk class + +L1 authorization mutation and concurrency. + +## SLA + +P1 + +## Allowed files + +```text +backend/app/modules/actors/repository.py +backend/app/modules/authorization/** +backend/app/modules/projects/repository.py +backend/app/api/router.py +backend/tests/test_actors.py +backend/tests/test_authorization.py +backend/tests/test_projects.py +backend/scripts/api_contract_e2e.py +backend/scripts/auth_api_e2e.py +docs/operations_authorization_service.md +docs/spec_authorization_service.md +.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/** +.agent-loop/merge-intents/WS-AUTH-001-10C.json +.agent-loop/REVIEW_LOG.md +``` + +## Not allowed changes + +```text +migration or durable schema changes +read/candidate surface redesign +task assignment or review reconciliation implementation +admin/service/project-role authority substitution +`both`, replacement, automated grants, or role conversion +commit inside PREP, kernel, service, or repository +``` + +## Exact surface inventory + +| ActionId | PermissionId | Authority | Surface | +|---|---|---|---| +| `project_role_grant.issue` | `project.role_grant.manage` | covered Project Manager only | `POST /api/v1/projects/{project_id}/role-grants` | +| `project_role_grant.revoke` | `project.role_grant.manage` | covered Project Manager only | `POST /api/v1/projects/{project_id}/role-grants/{grant_id}/revoke` | + +Both are human-only and registered as planned by 10A with +`ActionOwner.AUTH_10C`. They remain planned and non-callable until this exact +10C child is separately started and implements them; 10C then changes each row +to active atomically with its route. They use existing mutation rate controls. +Audit Authority, Access Administrator, Operator, Finance Authority, contributor +grants, services, agents, and Space do not substitute for Project Manager +authority. + +## Exact lock and transaction order + +PREP first locks `AuthorityControl(id=1)` as the shared authorization-order +barrier used by administrative lifecycle mutations. It then accepts the caller +plus the known target human principal for issuance, sorts distinct ActorProfile +IDs lexically, and locks each profile then its exact active link. It finally +locks the caller's one deterministic covered Project Manager AdminRoleGrant. +The barrier is ordering coordination, not final-admin permission or safety. + +After PREP returns, issue locks canonical project, then takes one transaction +advisory key for `(actor_profile_id, project_id, requested_role)` to serialize +absence, then reloads the target/scope facts and active exact-role selector. +Revoke locks canonical project then the exact grant. Consume recomposes every +fact and evaluates once; repositories flush only. The route commits once. + +Crossed tests prove two managers targeting one another, issue versus target +profile/link lifecycle, and issue versus caller-grant revocation cannot deadlock +because all paths share the barrier and compatible principal order. + +## Exact request and response contract + +Issue requires `Idempotency-Key: ` and a strict body containing exactly +`target_actor_profile_id`, `role`, `qualification`, and `reason`. +`qualification` contains the two exact availability objects and reference lists +defined by 10A. Revoke requires the same header and exactly `reason`. Reasons +are 1..500 UTF-8 bytes, equal their Python `str.strip()` result, and contain no +Unicode control character. Issue returns HTTP 201 with exactly `{id, +qualification_snapshot_id, project_id, actor_profile_id, role, status: +"active", version: 1}`. Revoke returns HTTP 200 with the same fields, the +original snapshot ID, `status: "revoked"`, and `version: 2`. Strict response +schemas reject undeclared fields, including identity-link and contact data. +Stable errors are HTTP 400 `invalid_request`, HTTP 403 +`self_grant_forbidden` or `self_role_revoke_forbidden`, concealed HTTP 404 +`resource_not_found`, HTTP 409 `idempotency_mismatch` or +`project_role_grant_exists`, `project_role_grant_already_revoked`, or +`project_role_grant_replay_state_changed`, HTTP 422 +`qualification_snapshot_invalid`, and fail-closed HTTP 503 +`service_unavailable`. A same-key/same-body issue replay reauthorizes and +returns the prior canonical HTTP 201 response while the grant remains active; +after that grant is revoked it returns HTTP 409 +`project_role_grant_replay_state_changed` without the stale response. A +same-key/same-body revoke replay reauthorizes and returns the prior canonical +HTTP 200 revoked response; a new-key revoke of an already-revoked grant returns +HTTP 409 `project_role_grant_already_revoked`. + +## Acceptance criteria + +- Issue permits draft, active, and paused projects; terminal/archived, + nonexistent, and unauthorized projects deny without target disclosure. +- Revoke remains available for every existing project state so authority can + always be removed. +- Target is a different active human with active link. Agent, Space, service, + self-grant, and self-revoke deny stably before disclosure. +- Issue creates one exact-role snapshot, one manual grant, + `ProjectRoleQualificationSnapshotCaptured`, and `ProjectRoleGrantIssued` in + one transaction. It never changes another role. +- Revoke changes only the locked exact role and creates + `ProjectRoleGrantRevoked` plus one linked `AuthorityInvalidationRequested` + carrying grant, actor, project, role, and cause-event references. +- Submitter invalidation names only the future AUTH-13 assignment obligation; + reviewer names only its REV-owned obligation; adjudicator creates no product + obligation until separately activated. +- Same key/same request replay reloads canonical project, grant, and snapshot + and reauthorizes the current caller before response disclosure. Suspended, + revoked-link, removed/out-of-scope manager, or now-concealed project replay + denies without returning stale response data. +- Same key/different request or role is `idempotency_mismatch`; new-key active + same-role issue is one audited stable conflict; distinct roles may issue + concurrently. +- Cancellation and injected failure at snapshot, grant, idempotency, audit, + invalidation, flush, and commit leave no orphan row or partial evidence. +- PostgreSQL tests cover identical-role issue, different-role issue, crossed + managers, revoke versus regrant, revoke versus authorization, replay after + authority loss, timeout, and cancellation. +- Live API proof uses the same unexpired manager token to issue, read the active + grant through 10B, revoke, read the historical revoked grant, and prove a + same-key/same-body revoke replay returns the prior HTTP 200 response after + reauthorization while a new-key second revoke returns HTTP 409 + `project_role_grant_already_revoked`; an old issue replay after revocation is + reauthorized and returns HTTP 409 `project_role_grant_replay_state_changed` + without stale disclosure or direct database edits. Unit and PostgreSQL tests + assert the same-key active issue replay remains HTTP 201 and the post-revoke + issue replay changes to that exact closed conflict. + Contributor-capability denial remains explicitly deferred to AUTH-11/13 where + a real consumer action exists. + +## Verification commands + +```bash +(cd backend && .venv/bin/python -m ruff check app/modules/actors app/modules/authorization app/modules/projects tests/test_actors.py tests/test_authorization.py tests/test_projects.py scripts/auth_api_e2e.py) +(cd backend && WORKSTREAM_TEST_ADMIN_DATABASE_URL= .venv/bin/python scripts/run_isolated_tests.py --metadata-json --timeout-seconds 300 -- .venv/bin/python -m pytest -q tests/test_actors.py tests/test_authorization.py tests/test_projects.py -k 'project_role_grant') +(cd backend && WORKSTREAM_DATABASE_URL= .venv/bin/python scripts/auth_api_e2e.py) +python3 scripts/check_stale_authorization_docs.py +python3 scripts/check_markdown_links.py +git diff --check +``` + +GitHub owns the full sharded suite, aggregate and authorization coverage, API +E2E, and Agent Gates before PR readiness. + +## Required reviewers + +Senior engineering, QA/test, security/auth, product/ops, architecture, CI +integrity, docs, reuse/dedup, and test delta. + +## Human review focus + +Review multi-principal lock order, no-self controls, exact-project scope, +replay reauthorization, role-specific invalidation, and single commit ownership. + +## Stop conditions + +Stop on target-after-caller deadlock ordering, unlocked absence checks, stale +replay disclosure, cross-role mutation, lifecycle implementation, or PREP bypass. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-10-external-review-response.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-10-external-review-response.md new file mode 100644 index 000000000..1b98d4cb0 --- /dev/null +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-10-external-review-response.md @@ -0,0 +1,60 @@ +# WS-AUTH-001-10 External Review Response + +PR: `#168` + +Pre-repair published head: `e0af0a6dddb5ede4707d309d2894409344733446` + +## Comments addressed + +- GitHub preflight run `29825312611` rejected the internal evidence because it + used `Reviewed planning SHA` instead of the gate's canonical provenance label + `Reviewed code SHA`. The evidence now uses the exact required label. +- Agent Gates run `29825436222` found branch-local current-state prose in the + authored AUTH `STATUS.md`. That file is restored to trusted-main state; + signed automation, not a PR branch, owns live active/review projection. +- The Backend aggregate `test` job failed only because preflight failed and its + shards/API E2E prerequisites were skipped. No runtime test executed and no + backend defect was reported. +- CodeRabbit posted five valid contract findings. The parent now explicitly + allows `.agent-loop/REVIEW_LOG.md`; the grant `version` is persisted with the + exact active-1/revoked-2 invariant; 10B/10C state that their 10A registrations + remain planned until the owning child activates them with routes; the + canonical issue operation includes idempotency, AuthorityControl/PREP locks, + advisory absence serialization, and one route commit; and every endpoint + example uses `/api/v1`. + +## Comments deferred + +None. + +## Human decisions needed + +None. + +## Commands rerun + +```bash +python3 scripts/test_agent_gates.py +python3 scripts/check_internal_review_evidence.py +python3 scripts/check_stale_authorization_docs.py +python3 scripts/check_stale_workstream_wording.py +python3 scripts/check_markdown_links.py +python3 scripts/update_post_merge_memory.py validate-merge-intent --base-ref origin/main +git diff --check +``` + +Agent Gate regression result: 88 passed. The evidence check intentionally +required exact-SHA repair review and an evidence-only descendant; internal +review passed repair SHA `1623e5b2dd85cc65df92af89989fda2ce7881bd0`, and the +initial evidence-only descendant bound that SHA. + +All required internal tracks subsequently passed the five-finding CodeRabbit +repair at exact SHA `6a89dd5018d28be149dc6e77f1466a0b3c707296`; the final +PREP wording was repaired once more before that pass so final authorization +occurs only through `consume()` after canonical feature facts are locked. + +## Remaining risks + +Fresh GitHub checks, CodeRabbit re-review, and human review remain. No runtime, +migration, CI, test, or coverage behavior was changed by these documentation +repairs. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-10-internal-review-evidence.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-10-internal-review-evidence.md new file mode 100644 index 000000000..f90c57c01 --- /dev/null +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-10-internal-review-evidence.md @@ -0,0 +1,72 @@ +# WS-AUTH-001-10 Internal Review Evidence + +Reviewed code SHA: `6a89dd5018d28be149dc6e77f1466a0b3c707296` + +Reviewed planning SHA: `ca52fd6a6c51f78b3e3a10faf77f4ab235843ad2` + +Reviewed against trusted main: `5a8a924d9b3b347d4cc74b4682865518539c837e` + +Reviewed at: `2026-07-21T11:10:59Z` + +Reviewer run IDs: `auth10_plan_core`, `auth10_plan_security_qa`, +`auth10_plan_ops_ci_docs` + +Reviewer tracks: senior engineering, QA/test, security/auth, product/ops, +architecture, CI integrity, docs, reuse/dedup, and test delta + +## Deterministic Evidence + +- `python3 scripts/check_stale_authorization_docs.py`: PASS. +- `python3 scripts/check_markdown_links.py`: PASS for all changed Markdown. +- `python3 scripts/update_post_merge_memory.py validate-merge-intent --base-ref origin/main`: + PASS for `WS-AUTH-001-10`, with same-initiative successor + `WS-AUTH-001-10A` and explicit start required. +- `git diff --check`: PASS. +- This parent changes planning/specification artifacts only. No runtime, + migration, test, CI, dependency, threshold, or product behavior changed. +- The exact external-gate repair restores `STATUS.md` to trusted-main state and + uses the canonical evidence provenance label. All three reviewer groups + passed repair SHA `1623e5b2dd85cc65df92af89989fda2ce7881bd0` with no + remaining finding; 88 Agent Gate regression tests pass locally. +- All three reviewer groups also pass CodeRabbit repair SHA + `6a89dd5018d28be149dc6e77f1466a0b3c707296`. The five findings close parent + scope, persisted versioning, planned activation wording, canonical PREP + consumption/idempotency order, and `/api/v1` namespace consistency. +- GitHub remains the owner of the full sharded suite, aggregate 78 percent and + changed-authorization 90 percent coverage gates, API E2E, and Agent Gates for + each implementation child. + +## Reviewer Results + +| Reviewer | Result | Blocking findings | Notes | +|---|---|---|---| +| senior engineering | PASS AFTER FIXES | none | The parent and three children are executable and correctly sequenced. | +| QA/test | PASS AFTER FIXES | none | Exact migration predicates, API schemas, replay states, and proof obligations are frozen. | +| security/auth | PASS AFTER FIXES | none | Privacy, lock ordering, replay reauthorization, disclosure, and fail-closed behavior are explicit. | +| product/ops | PASS AFTER FIXES | none | Independent roles, project lifecycle availability, and downstream consumer deferrals are coherent. | +| architecture | PASS AFTER FIXES | none | 10A owns data and planned catalogue registration; 10B/10C exclusively activate their rows. | +| CI integrity | PASS | none | No workflow, threshold, command, dependency, or hosted-test ownership was weakened. | +| docs | PASS AFTER FIXES | none | Parent, children, D32, plan, and canonical reference specification agree. | +| reuse/dedup | PASS AFTER FIXES | none | The design reuses the closed catalogue, PREP, repositories, evidence, and idempotency conventions. | +| test delta | PASS | none | No test was changed, removed, skipped, or weakened. | + +## Findings Resolved + +The initial combined runtime contract was rejected as too broad and split into +10A data/evidence, 10B reads, and 10C mutations. Review then closed stale +combined-role specification language, privacy and pagination ambiguity, +migration refusal predicates, action/evidence migration custody, PREP lock +ordering, strict request/response schemas, and issue/revoke replay states. The +final planning repair places all five future actions in the existing closed catalogue as +planned rows with exact 10B/10C owners; no parallel allowlist or active surface +is created. The later external-gate repair changes no approved plan semantics. + +Valid findings addressed: yes + +Open sub-agent sessions: none after evidence publication review + +## Remaining Risk And Gate + +GitHub Agent Gates, CodeRabbit, and explicit human review remain. This parent +does not authorize migration or runtime work. After merge and signed memory +generation, 10A still requires a separate exact-main explicit start event. diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-10-pr-trust-bundle.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-10-pr-trust-bundle.md new file mode 100644 index 000000000..6cc51db9a --- /dev/null +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-10-pr-trust-bundle.md @@ -0,0 +1,88 @@ +# WS-AUTH-001-10 PR Trust Bundle + +## Chunk + +`WS-AUTH-001-10` - Project Qualification And Contributor Role Grants Planning + +Merge intent: `.agent-loop/merge-intents/WS-AUTH-001-10.json` + +## Goal And Human-Approved Intent + +Turn the approved independent project-role design into executable, separately +gated implementation contracts without shipping a migration or runtime surface. +The user approved the 10A/10B/10C split and asked to start this planning chunk. + +## What Changed And Why + +- Recasts AUTH-10 as a planning-only parent and declares 10A as its exact + same-initiative successor. +- Defines 10A for migration `0031`, immutable qualification/grant truth, + availability-neutral evidence parity, and five planned catalogue rows. +- Defines 10B for the three privacy-safe candidate/list/detail reads and a + signed, filter-bound pagination codec. +- Defines 10C for PREP-bound issue/revoke mutations, deterministic locking, + idempotency, audit/invalidation, replay, failure atomicity, and live API proof. +- Reconciles D32 and the canonical reference specification with independent + submitter, reviewer, and adjudicator grants. + +The split was chosen because schema/evidence, privacy-sensitive reads, and +multi-principal mutations have different risks and proof boundaries. One +combined implementation PR was rejected during L1 plan review. + +## Scope And Product Behavior + +Only initiative planning, chunk contracts, the canonical reference spec, one +merge intent, review evidence, and this trust bundle change. No Workstream +product behavior, migration, action availability, route, CI workflow, or test +changes in this parent. + +## Acceptance Proof And Test Delta + +- Stale authorization documentation: PASS. +- Markdown links: PASS. +- Merge-intent validation: PASS. +- Diff integrity: PASS. +- All nine required internal review tracks pass exact planning SHA + `ca52fd6a6c51f78b3e3a10faf77f4ab235843ad2` after fixes, and all required + tracks pass external-gate repair SHA + `1623e5b2dd85cc65df92af89989fda2ce7881bd0` and CodeRabbit repair SHA + `6a89dd5018d28be149dc6e77f1466a0b3c707296`. +- No tests were added, modified, removed, skipped, or weakened because this is + a planning-only parent. Each child owns focused local tests; GitHub owns its + full suite and coverage proof. + +## CI Integrity + +Coverage floors, lint/typecheck commands, workflow behavior, dependencies, and +test selection are unchanged. Child contracts preserve GitHub ownership of the +full sharded suite, aggregate 78 percent coverage, authorization 90 percent +coverage, API E2E, and Agent Gates. + +## External Review And Remaining Risks + +Initial GitHub preflight rejected a noncanonical reviewed-SHA heading, and Agent +Gates rejected branch-authored live status. The external response records the +minimal repairs: use the canonical evidence label and restore `STATUS.md` to +trusted-main state because signed automation owns live projection. All 88 Agent +Gate regression tests pass locally. CodeRabbit then identified five valid +planning/specification gaps: review-log scope, persisted grant version, +planned-versus-active child wording, canonical PREP/idempotency ordering, and +API namespace consistency. Each is corrected without runtime or migration +changes. CodeRabbit re-review and fresh GitHub checks remain. +Implementation risk is intentionally isolated into the three children: +migration privacy/refusal in 10A, read disclosure/cursors in 10B, and mutation +concurrency/replay in 10C. None may start from this PR alone. + +## Follow-Up And Human Review Focus + +After this parent merges, trusted automation must name 10A and a fresh explicit +start event must activate it. Review the independent-role model, exact child +boundaries, 10A planned-versus-active distinction, lock ordering, privacy-safe +read schemas, replay state machine, and the absence of runtime or migration +changes in this parent. + +## Human Merge Ownership + +The agent may publish and repair this branch but may not merge it. Only the user +may approve this specific PR for merge. Trusted-main automation owns signed +post-merge memory generation. diff --git a/.agent-loop/merge-intents/WS-AUTH-001-10.json b/.agent-loop/merge-intents/WS-AUTH-001-10.json new file mode 100644 index 000000000..81a7d138f --- /dev/null +++ b/.agent-loop/merge-intents/WS-AUTH-001-10.json @@ -0,0 +1,9 @@ +{ + "chunk_id": "WS-AUTH-001-10", + "chunk_title": "Project Qualification And Contributor Role Grants", + "initiative_id": "WS-AUTH-001", + "next_chunk_id": "WS-AUTH-001-10A", + "next_chunk_title": "Project Role Grant Data And Evidence Foundation", + "next_requires_explicit_start": true, + "schema_version": 2 +} diff --git a/docs/reference_specs/WS-AUTH-001-actor-profile-role-and-authorization-service-specification.md b/docs/reference_specs/WS-AUTH-001-actor-profile-role-and-authorization-service-specification.md index 3eb3c2fba..e338e7257 100644 --- a/docs/reference_specs/WS-AUTH-001-actor-profile-role-and-authorization-service-specification.md +++ b/docs/reference_specs/WS-AUTH-001-actor-profile-role-and-authorization-service-specification.md @@ -77,7 +77,8 @@ The coding agent is not permitted to reinterpret the following decisions. 8. Every active human ActorProfile belongs to the Contributor domain by default. 9. Contributor-domain membership is not task, submission, or review authority. 10. Project work requires an active `ProjectRoleGrant` for that exact project. -11. Project contributor roles are `submitter`, `reviewer`, or `both`. +11. Project contributor roles are independent `submitter`, `reviewer`, or + `adjudicator` grants; there is no combined role. 12. Project roles are explicitly granted by an authorized Project Manager in v0.1. 13. Skills and reputation may inform a ProjectRoleGrant but do not automatically create one. 14. Administrative authority is represented by separate `AdminRoleGrant` records. @@ -116,7 +117,7 @@ ActorProfile (one local Workstream actor) | | | +-- ProjectRoleGrant(project A, submitter) | +-- ProjectRoleGrant(project B, reviewer) - | +-- ProjectRoleGrant(project C, both) + | +-- ProjectRoleGrant(project C, adjudicator) | +-- Zero or more AdminRoleGrants +-- access_administrator @@ -277,15 +278,22 @@ The qualification snapshot records the evidence visible when a Project Manager m ProjectRoleQualificationSnapshot: id: uuid project_id: uuid - contributor_id: uuid + actor_profile_id: uuid + requested_role: enum [submitter, reviewer, adjudicator] - skills_snapshot: object - reputation_snapshot: object + skills_snapshot: QualificationAvailabilitySnapshot + reputation_snapshot: QualificationAvailabilitySnapshot prior_project_work_refs: list[uuid] - external_expertise_refs: list[string] + external_expertise_refs: list[bounded_reference_token] - captured_by: actor_id + captured_by_actor_profile_id: uuid + captured_by_admin_role_grant_id: uuid captured_at: timestamp + +QualificationAvailabilitySnapshot: + availability: enum [available, unavailable] + reference_ids: list[bounded_reference_token] + unavailable_reason: enum [not_collected, source_unavailable, no_record] | null ``` #### Qualification snapshot invariants @@ -295,6 +303,13 @@ ProjectRoleQualificationSnapshot: - Missing reputation does not block a manual v0.1 grant. - The snapshot is evidence for the grant decision, not an authorization decision by itself. - Snapshot payloads MUST exclude secrets and unnecessary personal information. +- Each availability `reference_ids` list contains zero to 20 tokens. A bounded + reference token is one to 120 characters, matches + `^[A-Za-z0-9][A-Za-z0-9._:/-]{0,119}$`, and MUST NOT contain `://`. +- `available` requires at least one reference and a null unavailable reason; + `unavailable` requires no references and exactly one unavailable reason. +- `prior_project_work_refs` contains zero to 20 UUIDs and + `external_expertise_refs` contains zero to 20 bounded reference tokens. ### 6.5 ProjectRoleGrant @@ -304,20 +319,20 @@ ProjectRoleQualificationSnapshot: ProjectRoleGrant: id: uuid project_id: uuid - contributor_id: uuid - role: enum [submitter, reviewer, both] + actor_profile_id: uuid + role: enum [submitter, reviewer, adjudicator] status: enum [active, revoked] + version: integer - grant_method: enum [manual, automated] - automation_policy_ref: uuid | null - qualification_snapshot_ref: uuid + grant_method: literal [manual] + qualification_snapshot_id: uuid - granted_by: actor_id + granted_by_actor_profile_id: uuid granted_by_admin_role_grant_id: uuid granted_at: timestamp grant_reason: string - revoked_by: actor_id | null + revoked_by_actor_profile_id: uuid | null revoked_at: timestamp | null revoked_reason: string | null ``` @@ -328,14 +343,21 @@ ProjectRoleGrant: - The grant is valid for one project only. - A Project Manager whose effective scope covers the project may issue or revoke it. - The issuing Project Manager cannot grant a ProjectRoleGrant to themselves. -- `grant_method = manual` is the only creation path enabled in v0.1. -- `grant_method = automated` is schema-reserved and MUST NOT be emitted by v0.1 code. -- `automation_policy_ref` is null for a manual grant. -- `qualification_snapshot_ref` is mandatory and must match the same project and contributor. -- At most one active ProjectRoleGrant exists for a contributor in a project. -- Changing `submitter`, `reviewer`, or `both` requires revoking the active grant and creating a new grant in one transaction. -- A submitter action requires `role IN (submitter, both)`. -- A reviewer action requires `role IN (reviewer, both)`. +- `grant_method = manual` is the only accepted creation value in v0.1. +- `qualification_snapshot_id` is mandatory and its snapshot must match the same + project, actor profile, and exact requested role. +- At most one active ProjectRoleGrant exists for a contributor, project, and + exact role. All three distinct roles may be active concurrently. +- Revoking or regranting one role never changes another role. +- `version` is persisted. The invariant is exact: active grants persist version + 1 and revoked grants persist version 2. Only the active-version-1 to + revoked-version-2 transition may mutate lifecycle fields. +- Grant and revocation reasons are one to 500 UTF-8 bytes, equal their Python + `str.strip()` result, and contain no Unicode control character. +- A submitter action requires an active exact `submitter` grant. +- A reviewer action requires an active exact `reviewer` grant. +- An `adjudicator` grant creates no adjudication capability until WS-REV defines + that lifecycle and AUTH activates its exact actions. - AdminRoleGrant does not satisfy either requirement. - Revocation does not delete or invalidate historical work completed while the grant was active. @@ -791,7 +813,7 @@ It may not claim tasks, create submissions, read reviewer queues, or review work ### 13.2 Submitter grant -An active `ProjectRoleGrant(submitter|both)` supplies candidate permissions for the same project: +An active exact `ProjectRoleGrant(submitter)` supplies candidate permissions for the same project: ```text project.read @@ -806,7 +828,7 @@ TaskAssignment state, task ban, task availability, policy state, and other submi ### 13.3 Reviewer grant -An active `ProjectRoleGrant(reviewer|both)` supplies candidate permissions for the same project: +An active exact `ProjectRoleGrant(reviewer)` supplies candidate permissions for the same project: ```text project.read @@ -821,9 +843,11 @@ review.chain.read Self-review, active-lease capacity, preferred-reviewer routing, lease state, and decision-state guards still apply. -### 13.4 Both grant +### 13.4 Adjudicator grant -`both` is the union of submitter and reviewer project permissions. It does not bypass self-review or any other resource guard. +An `adjudicator` grant is independently persisted authority evidence. It grants +no adjudication action until the separately approved WS-REV lifecycle and exact +AUTH activation exist. --- @@ -935,7 +959,7 @@ Effects: - bearer token verification may succeed; - ActorResolver returns the suspended ActorProfile; - all business mutations are denied with `actor_suspended`; -- `/v1/actors/me` may return minimal status and support information; +- `/api/v1/actors/me` may return minimal status and support information; - active AdminRoleGrants and ProjectRoleGrants remain recorded but ineffective; - active review leases and other exclusive work claims are invalidated through `AuthorityInvalidationRequested` and consuming lifecycle reconciliation; - historical records remain unchanged. @@ -1022,7 +1046,7 @@ The first Access Administrator cannot be created through an endpoint that alread ### 19.2 Bootstrap sequence -1. The initial human signs in normally and calls `/v1/actors/me`. +1. The initial human signs in normally and calls `/api/v1/actors/me`. 2. Workstream creates the human ActorProfile and ActorIdentityLink. 3. An authorized deployment operator runs the Workstream-local bootstrap management operation with the exact ActorProfile ID. 4. The operation verifies that no active Access Administrator exists. @@ -1113,30 +1137,48 @@ The transaction MUST: The transaction MUST: -1. authorize `project.role_grant.manage` for the exact project; -2. lock the target ActorProfile and project active-grant selector; -3. require an active human target; -4. prohibit self-grant; -5. create the immutable qualification snapshot; -6. revoke an existing active grant only when the request explicitly replaces it; -7. create the new manual ProjectRoleGrant; -8. append audit and invalidation events where replacement occurred; -9. commit. +1. require and reserve the typed `Idempotency-Key` in the caller/operation + namespace, rejecting a mismatched replay; +2. run PREP by locking `AuthorityControl(id=1)`, then locking distinct caller + and target ActorProfiles in lexical ID order, each followed by its exact + active identity link, and finally locking the caller's deterministic covered + Project Manager grant; preparation validates candidate authority for the + normalized requested project scope but does not make the final decision; +3. lock and load the canonical project; +4. take the transaction advisory key for + `(actor_profile_id, project_id, requested_role)` to serialize absence; +5. reload the target, caller scope, and active exact-role selector under those + locks, require an active human target, prohibit self-grant, and reject an + existing active grant for the same exact role; +6. consume the exact transaction-bound PREP handle, recompose the final locked + authority and canonical-project facts, and evaluate + `project.role_grant.manage` exactly once; no product, evidence, or + idempotency response mutation is staged before successful consumption; +7. create the immutable qualification snapshot and new manual ProjectRoleGrant + without changing another role; +8. append snapshot and grant-issued audit evidence and commit the idempotency + response reference; +9. commit once at the route boundary. --- ## 21. API Contract -All endpoints use the independent `/v1` API namespace. This does not change the product release from Workstream v0.1. +Repository implementation and every endpoint example below use the canonical +`/api/v1` namespace. ### 21.1 Current actor ```http -GET /v1/actors/me -PATCH /v1/actors/me -GET /v1/actors/me/authorization-context?project_id={project_id} +GET /api/v1/actors/me +PATCH /api/v1/actors/me +GET /api/v1/actors/me/authorization-context?project_id={project_id} ``` +The authorization-context surface is assigned to AUTH-11, not AUTH-10. It +ships only with the first complete project-read cutover and must not advertise +inactive task or review actions. + PATCH request: ```json @@ -1164,11 +1206,11 @@ Capabilities are computed server-side from a registered allowlist. Clients canno ### 21.2 Actor administration ```http -GET /v1/admin/actors -GET /v1/admin/actors/{actor_profile_id} -POST /v1/admin/actors/{actor_profile_id}/suspend -POST /v1/admin/actors/{actor_profile_id}/reactivate -POST /v1/admin/actors/{actor_profile_id}/deactivate +GET /api/v1/admin/actors +GET /api/v1/admin/actors/{actor_profile_id} +POST /api/v1/admin/actors/{actor_profile_id}/suspend +POST /api/v1/admin/actors/{actor_profile_id}/reactivate +POST /api/v1/admin/actors/{actor_profile_id}/deactivate ``` State-change request: @@ -1182,9 +1224,9 @@ State-change request: ### 21.3 Identity-link administration ```http -GET /v1/admin/actors/{actor_profile_id}/identity-link -POST /v1/admin/identity-links/{identity_link_id}/revoke -POST /v1/admin/identity-links/{identity_link_id}/reactivate +GET /api/v1/admin/actors/{actor_profile_id}/identity-link +POST /api/v1/admin/identity-links/{identity_link_id}/revoke +POST /api/v1/admin/identity-links/{identity_link_id}/reactivate ``` No endpoint adds a second human identity link in v0.1. @@ -1192,18 +1234,18 @@ No endpoint adds a second human identity link in v0.1. ### 21.4 Service actors ```http -POST /v1/admin/service-actors -GET /v1/admin/service-actors -GET /v1/admin/service-actors/{actor_profile_id} +POST /api/v1/admin/service-actors +GET /api/v1/admin/service-actors +GET /api/v1/admin/service-actors/{actor_profile_id} ``` ### 21.5 Admin-role grants ```http -POST /v1/admin-role-grants -GET /v1/admin-role-grants -GET /v1/actors/{actor_profile_id}/admin-role-grants -POST /v1/admin-role-grants/{grant_id}/revoke +POST /api/v1/admin-role-grants +GET /api/v1/admin-role-grants +GET /api/v1/actors/{actor_profile_id}/admin-role-grants +POST /api/v1/admin-role-grants/{grant_id}/revoke ``` Create request: @@ -1221,35 +1263,42 @@ Create request: ### 21.6 Project-role grants ```http -POST /v1/projects/{project_id}/role-grants -GET /v1/projects/{project_id}/role-grants -GET /v1/projects/{project_id}/role-grants/{grant_id} -POST /v1/projects/{project_id}/role-grants/{grant_id}/revoke +GET /api/v1/projects/{project_id}/contributor-candidates +POST /api/v1/projects/{project_id}/role-grants +GET /api/v1/projects/{project_id}/role-grants +GET /api/v1/projects/{project_id}/role-grants/{grant_id} +POST /api/v1/projects/{project_id}/role-grants/{grant_id}/revoke ``` Create request: ```json { - "contributor_id": "uuid", - "role": "reviewer", - "grant_method": "manual", - "grant_reason": "Qualified project reviewer", + "target_actor_profile_id": "uuid", + "role": "submitter", + "reason": "Qualified project submitter", "qualification": { - "skills_snapshot": {}, - "reputation_snapshot": {"status": "unavailable"}, + "skills_snapshot": { + "availability": "available", + "reference_ids": ["skill:python"], + "unavailable_reason": null + }, + "reputation_snapshot": { + "availability": "unavailable", + "reference_ids": [], + "unavailable_reason": "no_record" + }, "prior_project_work_refs": [], "external_expertise_refs": [] - }, - "replace_active_grant": false + } } ``` ### 21.7 Permission catalog ```http -GET /v1/authorization/permissions -GET /v1/authorization/admin-role-definitions +GET /api/v1/authorization/permissions +GET /api/v1/authorization/admin-role-definitions ``` These endpoints return registered definitions. They do not expose or accept arbitrary dynamic policy code. @@ -1286,7 +1335,7 @@ Errors MUST use the Workstream structured error envelope. | 409 | `actor_deactivated_terminal` | Transition attempted from deactivated state | | 409 | `last_access_administrator` | Operation would remove final Access Administrator | | 409 | `admin_role_grant_exists` | Identical effective grant already active | -| 409 | `project_role_grant_exists` | Active project grant exists and replacement not requested | +| 409 | `project_role_grant_exists` | Active exact-role project grant exists | | 409 | `identity_link_conflict` | Issuer subject already maps to another ActorProfile | | 409 | `resource_project_mismatch` | Related resources belong to different projects | | 409 | `idempotency_mismatch` | Idempotency key reused with different request | @@ -1316,7 +1365,7 @@ AdminRoleGrant: WHERE status = 'active' AND scope_type = 'project' ProjectRoleGrant: - partial UNIQUE(project_id, contributor_id) + partial UNIQUE(project_id, actor_profile_id, role) WHERE status = 'active' ProjectRoleQualificationSnapshot: @@ -1346,9 +1395,9 @@ AdminRoleGrant: access_administrator/operator => system scope ProjectRoleGrant: - role IN ('submitter', 'reviewer', 'both') + role IN ('submitter', 'reviewer', 'adjudicator') status IN ('active', 'revoked') - manual => automation_policy_ref IS NULL + grant_method = 'manual' ``` Foreign keys MUST prevent project/snapshot/contributor mismatches. Composite foreign keys MUST be used where necessary. @@ -1361,9 +1410,9 @@ ActorProfile(last_seen_at) ActorIdentityLink(issuer, subject, status) AdminRoleGrant(actor_profile_id, status) AdminRoleGrant(role, scope_type, scope_project_id, status) -ProjectRoleGrant(project_id, contributor_id, status) -ProjectRoleGrant(contributor_id, status) -ProjectRoleQualificationSnapshot(project_id, contributor_id, captured_at) +ProjectRoleGrant(project_id, actor_profile_id, role, status) +ProjectRoleGrant(actor_profile_id, role, status) +ProjectRoleQualificationSnapshot(project_id, actor_profile_id, requested_role, captured_at) AuditEvent(actor_profile_id, occurred_at) AuditEvent(project_id, occurred_at) ``` @@ -1384,7 +1433,9 @@ Two concurrent identical grant requests result in one active grant. An exact ide ### 24.3 Duplicate ProjectRoleGrant race -Two concurrent project-grant requests for one contributor/project result in one active grant. Replacement requires locking the active grant selector and an explicit replacement request. +Two concurrent same-role project-grant requests for one contributor/project +result in one active exact-role grant. Concurrent distinct-role requests may +each succeed and never replace one another. ### 24.4 Last Access Administrator race @@ -1448,7 +1499,6 @@ All events are append-only. - `ProjectRoleQualificationSnapshotCaptured` - `ProjectRoleGrantIssued` -- `ProjectRoleGrantReplaced` - `ProjectRoleGrantRevoked` ### Authorization @@ -1691,10 +1741,12 @@ Alert on: - Missing reputation may be recorded unavailable. - Submitter grant cannot review. - Reviewer grant cannot claim submission tasks. -- Both grant supplies union but not self-review bypass. +- Concurrent active submitter and reviewer grants supply the union of their + exact permissions but do not bypass self-review guards. - Admin role alone cannot submit or review. - Duplicate active project grant is prevented. -- Replacement creates a new grant and revokes old history. +- Regrant after revocation creates a new row and preserves revoked history; + issuance never implicitly replaces or revokes another row. - Revocation is visible on next request. ### 31.6 Permission matrix tests @@ -1721,7 +1773,8 @@ For every permission identifier, create table-driven tests proving: - Concurrent human first access leaves one profile/link. - Concurrent identical AdminRoleGrant leaves one active grant. -- Concurrent ProjectRoleGrant leaves one active grant. +- Concurrent same-exact-role ProjectRoleGrant issue leaves one active grant; + concurrent distinct-role issue may leave each independent role active. - Concurrent final-admin suspension/revocation attempts leave at least one effective Access Administrator. - Grant revocation versus sensitive command produces one serializable authority outcome. @@ -1742,7 +1795,7 @@ The live drill MUST use supported Workstream APIs and the bootstrap operation. D ### 32.1 Drill sequence 1. Start Workstream with no ActorProfiles or Access Administrators. -2. Human A presents a valid issuer token and calls `/v1/actors/me`. +2. Human A presents a valid issuer token and calls `/api/v1/actors/me`. 3. Prove Human A has one active ActorProfile, Contributor domain, and no roles/grants. 4. Run the one-time bootstrap operation for Human A. 5. Prove Human A now has system-scoped `access_administrator` only. @@ -1831,7 +1884,7 @@ Exit: actor-state and service-actor tests pass. - qualification snapshot; - project grant schema/constraints; - Project Manager authority; -- create/replace/revoke APIs; +- independent create/read/revoke APIs; - project permission candidates. Exit: project-grant and concurrency tests pass.