Skip to content

feat(auth): add submission policy authority foundation - #286

Merged
abiorh-claw merged 11 commits into
mainfrom
codex/ws-auth-001-12f1-submission-policy-foundation
Aug 5, 2026
Merged

feat(auth): add submission policy authority foundation#286
abiorh-claw merged 11 commits into
mainfrom
codex/ws-auth-001-12f1-submission-policy-foundation

Conversation

@Abiorh001

@Abiorh001 Abiorh001 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Workstream PR Trust Bundle

Chunk

WS-AUTH-001-12F1 - Submission Policy Authority Foundation

Goal

Install exact PREP, replay, provenance, audit, and transaction custody needed by
12F2-12F4 without activating any submission-policy mutation action or changing
route, worker, or product behavior.

What changed

  • Extended the typed submission-policy authorization context with exact
    operation, request, policy, setup, compiler, catalogue, and output facts.
  • Added whole-context PREP equality and canonical resource-digest binding.
  • Added flush-only human/fixed-service replay reservation and typed completion.
  • Added migration 0057 nullable provenance, replay constraints, immutable
    completion, deferred product/evidence custody, and guarded downgrade.
  • Aligned kernel, audit schema, and database evidence vocabulary for the future
    exact submission-policy mutation resource.
  • Added focused unit, PostgreSQL convergence, migration, audit, documentation,
    and per-file coverage proof.

Scope and behavior

  • No action is activated.
  • No API route, Celery worker, agent call, or product-policy writer changes.
  • Historical product rows remain readable with all authority provenance null.
  • 12F2 owns manual create/update, 12F3 owns fixed-service derive, and 12F4 owns
    approval plus effective/pre-submit output creation.

Evidence

Ruff app/tests/scripts: passed
AUTH exact selector: 3 passed
Replay/service unit selector: 2 passed
CI lane contract: 33 passed
Focused per-file coverage: repository 92.31%, service 94.74%, total 93.75%
Semantic collection: 3,277 tests
Stale authorization docs: passed
Markdown links: passed
git diff --check: passed

PostgreSQL and Alembic selectors require WORKSTREAM_TEST_DATABASE_URL and are
delegated to the hosted Backend matrix rather than the user's slow local host.

Acceptance proof

  • All submission-policy mutation actions remain planned/unavailable.
  • PREP binds the exact typed context and computed resource digest.
  • Human and fixed-service replay namespaces are disjoint and exact.
  • Completion uses operation UUID plus the complete immutable namespace.
  • Product provenance, replay completion, and allowed evidence are linked by
    deferred database custody.
  • Historical null provenance survives upgrade and empty roundtrip.
  • Pending replay and admitted audit evidence block downgrade.
  • New subsystem files have a non-weakened hosted 90 percent coverage gate.

Internal review

Architecture, security, QA, senior engineering, test delta, CI integrity,
documentation, and reuse/dedup completed with no blocking findings. Product/ops
review passed after approval/output provenance immutability was added.

Remaining risk and follow-up

  • Hosted PostgreSQL migration/custody tests and repository-wide aggregate
    coverage must pass on the exact pushed head.
  • Full real-writer fault injection is intentionally proved in 12F2-12F4 because
    this foundation activates no product writer.

Human review focus

  • Zero activation and absence of route/worker behavior changes.
  • Full human/service replay namespace and single completion transition.
  • Deferred custody across policy/effective/pre-submit rows and AUTH evidence.
  • Historical all-null provenance and downgrade refusal predicates.

Human merge ownership

  • The user explicitly approved this specific PR for merge.

Summary by CodeRabbit

  • New Features

    • Added foundational authorization safeguards for submission-policy changes, including replay protection, provenance tracking, and audit evidence.
    • Bound prepared actions to the intended project, policy, compilation details, and outputs.
    • Preserved access to historical policy records without requiring backfill.
  • Bug Fixes

    • Prevented mismatched, duplicate, or incomplete replay requests from being accepted.
    • Prevented removal of authorization evidence while it remains in use.
  • Documentation

    • Documented the authorization foundation and confirmed submission-policy actions remain inactive.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds the submission-policy authorization foundation. It adds typed bindings, replay custody, provenance storage, migration protections, targeted coverage checks, and review records. The planned submission-policy actions remain inactive.

Changes

Submission policy authority

Layer / File(s) Summary
Authorization contracts and binding validation
backend/app/modules/authorization/*, backend/app/modules/audit/schemas.py, backend/tests/test_authorization.py
Adds submission-policy mutation typing, compilation validation, exact prepared bindings, custody checks, denial handling, and audit staging.
Provenance and migration custody
backend/app/modules/projects/models.py, backend/alembic/versions/0057_submission_policy_authority.py, backend/tests/test_alembic.py, backend/tests/conftest.py, backend/tests/test_review_lease_persistence.py
Adds replay storage, provenance fields, immutable triggers, deferred custody checks, downgrade blocking, and migration validation.
Replay reservation and completion service
backend/app/modules/projects/submission_policy_mutation_*.py, backend/tests/test_projects.py
Adds transaction-scoped replay reservation, classification, concurrency handling, and flush-only completion.
Coverage, workflow, and review records
.github/workflows/backend.yml, .agent-loop/..., docs/*authorization_service.md
Adds the coverage gate, review evidence, trust-bundle records, status updates, and authorization documentation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AuthorizationPreparation
  participant AuthorizationKernel
  participant ReplayService
  participant Database
  AuthorizationPreparation->>AuthorizationKernel: validate and bind submission-policy mutation
  AuthorizationKernel->>ReplayService: validate replay and custody facts
  ReplayService->>Database: reserve or complete replay record
  AuthorizationKernel->>Database: stage authorization audit evidence
Loading

Possibly related PRs

Suggested reviewers: abiorh-claw

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the submission policy authority foundation, which is the primary change.
Description check ✅ Passed The description covers the goal, changes, scope, evidence, acceptance criteria, risks, review focus, and merge ownership in sufficient detail.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/ws-auth-001-12f1-submission-policy-foundation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (7)
backend/tests/test_projects.py (3)

9466-9474: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the remaining reserve and _find_namespace branches.

Two branches of SubmissionPolicyMutationReplayRepository stay unexercised.

First, the claimed path always resolves. When session.scalar returns a record_id and session.get returns None, reserve raises ProjectRepositoryIntegrityError. Setting session.get_result = None while session.scalar_result holds a UUID covers it.

Second, _find_namespace is stubbed in every case here, and the PostgreSQL test at Line 9654 uses the human namespace only. The service branch, which filters on service_identity, setup_run_id, setup_generation, setup_task_id, correlation_id, and action_id, never runs.

The CI step at .github/workflows/backend.yml Lines 462-478 enforces 90% per file for this module.

♻️ Proposed addition
     state, record = await repository.reserve(**facts)  # type: ignore[arg-type]
     assert (state, record) == ("claimed", claimed)
 
+    session.get_result = None
+    with pytest.raises(ProjectRepositoryIntegrityError, match="reservation disappeared"):
+        await repository.reserve(**facts)  # type: ignore[arg-type]
+    session.get_result = claimed
+
     session.scalar_result = claimed
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/test_projects.py` around lines 9466 - 9474, Add tests in the
existing SubmissionPolicyMutationReplayRepository coverage to exercise the
claimed reserve integrity branch by setting session.scalar_result to a UUID and
session.get_result to None, then asserting ProjectRepositoryIntegrityError. Also
configure _find_namespace inputs with a service identity and setup_run_id,
setup_generation, setup_task_id, correlation_id, and action_id, and assert the
service namespace lookup path is used.

9637-9651: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the accepted service path and the inactive-transaction branch.

The loop proves that three mutated custody values are rejected. It never asserts that the unmodified service_facts is accepted. A change that rejects every service reservation would still pass this test.

_require_root_transaction has three failure conditions: the transaction is None, the transaction is not active, and the session is nested. Lines 9645-9651 cover None and nested. The not transaction.is_active branch stays uncovered.

♻️ Proposed additions
+    assert await service.reserve_replay(service_facts) == ("claimed", record)
     for changed in (
         replace(service_facts, setup_run_id=str(uuid4())),
         replace(service_facts, setup_task_id=uuid4()),
         replace(service_facts, correlation_id=uuid4()),
     ):
         with pytest.raises(ValueError, match="service replay custody is invalid"):
             await service.reserve_replay(changed)
 
     session.nested = True
     with pytest.raises(RuntimeError, match="one root transaction"):
         await service.reserve_replay(facts)
     session.nested = False
+
+    class InactiveTransaction:
+        is_active = False
+
+    session.sync_session.transaction = InactiveTransaction()
+    with pytest.raises(RuntimeError, match="one root transaction"):
+        await service.reserve_replay(facts)
     session.sync_session.transaction = None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/test_projects.py` around lines 9637 - 9651, Add assertions in
the service replay reservation test around `service.reserve_replay`: verify the
unchanged `service_facts` succeeds before testing mutated custody values, and
add coverage for `_require_root_transaction` when the root transaction exists
but `is_active` is false, preserving the existing `None` and nested transaction
assertions.

9743-9753: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Replace the fixed sleep with a deterministic wait for the blocked insert.

await asyncio.sleep(0.05) assumes the competing task reaches its blocking insert within 50 ms. On a loaded CI runner the task can still be starting when first.commit() runs. The competing insert then succeeds instead of conflicting, reserve returns "claimed", and the assertion at Line 9752 fails. The test becomes flaky.

Poll pg_stat_activity until the competing backend waits on a lock, then commit.

♻️ Proposed fix
         competing = asyncio.create_task(reserve_second())
-        await asyncio.sleep(0.05)
+        async with engine.connect() as observer:
+            for _ in range(200):
+                waiting = await observer.scalar(
+                    text(
+                        "select count(*) from pg_stat_activity where "
+                        "wait_event_type='Lock' and state='active' "
+                        "and query ilike '%submission_policy_mutation_idempotency_records%'"
+                    )
+                )
+                if waiting:
+                    break
+                await asyncio.sleep(0.05)
+            else:
+                raise AssertionError("competing reservation never blocked")
         await first.commit()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/test_projects.py` around lines 9743 - 9753, Update the
competing insert setup in the reserve test to remove the fixed asyncio.sleep and
wait deterministically until the task started by reserve_second is actually
blocked on the database lock. Use the existing async test flow around
SubmissionPolicyMutationReplayRepository(second).reserve and poll
pg_stat_activity for the competing backend’s lock wait state before calling
first.commit(), then keep the current assertions on second_result unchanged.
backend/app/modules/projects/submission_policy_mutation_service.py (2)

119-122: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add the return annotation to reserve_replay.

The method returns whatever SubmissionPolicyMutationReplayRepository.reserve returns, which is a typed tuple. Declare it so callers in 12F2 get the classification literal from type checking rather than from reading the repository.

♻️ Proposed change
-    async def reserve_replay(self, facts: SubmissionPolicyReplayFacts):
+    async def reserve_replay(
+        self, facts: SubmissionPolicyReplayFacts
+    ) -> tuple[
+        Literal["claimed", "mismatch", "pending", "replayed"],
+        SubmissionPolicyMutationIdempotencyRecord,
+    ]:

Add the supporting imports:

 from dataclasses import dataclass
+from typing import Literal
 from uuid import UUID
+from app.modules.projects.models import SubmissionPolicyMutationIdempotencyRecord
 from app.modules.projects.submission_policy_mutation_repository import (
     SubmissionPolicyMutationReplayRepository,
 )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/modules/projects/submission_policy_mutation_service.py` around
lines 119 - 122, Add the appropriate typed-tuple return annotation to
SubmissionPolicyMutationService.reserve_replay, matching the return type of
SubmissionPolicyMutationReplayRepository.reserve, and add any required typing
imports. Preserve the existing delegation and transaction behavior.

79-88: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Bind submission service identity comparisons to ServiceIdentity.PROJECT_SETUP.

_replay_values() compares facts.service_identity to "workstream.project.setup" for setup-service replay. Use ServiceIdentity.PROJECT_SETUP from app.modules.actors.service_identities at this check so the service identity value cannot drift from the enum.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/modules/projects/submission_policy_mutation_service.py` around
lines 79 - 88, Update the service identity comparison in _replay_values() for
setup_service replays to use ServiceIdentity.PROJECT_SETUP instead of the
hard-coded "workstream.project.setup" string. Import and reuse ServiceIdentity
from app.modules.actors.service_identities, leaving the other custody
validations unchanged.
backend/app/modules/projects/submission_policy_mutation_repository.py (1)

195-220: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the nullable predicates into a helper.

Five criteria use an inline conditional expression, for example:

Model.service_identity
.is_(None) if service_identity is None else
Model.service_identity == service_identity

The semantics are correct. Python binds the method call and the == comparison tighter than the conditional expression, so each criterion produces one clause. The formatting hides that precedence. A future edit that wraps part of the expression in parentheses would change the predicate without an obvious diff signal.

Replace the five occurrences with one helper so the intent is explicit.

♻️ Proposed helper
def _matches(column, value):
    """Return an exact null-safe equality predicate for one nullable column."""
    return column.is_(None) if value is None else column == value
             .where(
                 SubmissionPolicyMutationIdempotencyRecord.operation_id == operation_id,
                 SubmissionPolicyMutationIdempotencyRecord.actor_profile_id == actor_profile_id,
                 SubmissionPolicyMutationIdempotencyRecord.identity_link_id == identity_link_id,
-                SubmissionPolicyMutationIdempotencyRecord.service_identity
-                .is_(None) if service_identity is None else
-                SubmissionPolicyMutationIdempotencyRecord.service_identity == service_identity,
+                _matches(
+                    SubmissionPolicyMutationIdempotencyRecord.service_identity, service_identity
+                ),
                 SubmissionPolicyMutationIdempotencyRecord.action_id == action_id,
-                SubmissionPolicyMutationIdempotencyRecord.idempotency_key
-                .is_(None) if idempotency_key is None else
-                SubmissionPolicyMutationIdempotencyRecord.idempotency_key == idempotency_key,
+                _matches(
+                    SubmissionPolicyMutationIdempotencyRecord.idempotency_key, idempotency_key
+                ),
                 SubmissionPolicyMutationIdempotencyRecord.request_digest == request_digest,
                 SubmissionPolicyMutationIdempotencyRecord.resource_context_digest
                 == resource_context_digest,
-                SubmissionPolicyMutationIdempotencyRecord.setup_run_id
-                .is_(None) if setup_run_id is None else
-                SubmissionPolicyMutationIdempotencyRecord.setup_run_id == setup_run_id,
+                _matches(SubmissionPolicyMutationIdempotencyRecord.setup_run_id, setup_run_id),
                 SubmissionPolicyMutationIdempotencyRecord.setup_generation == setup_generation,
-                SubmissionPolicyMutationIdempotencyRecord.setup_task_id
-                .is_(None) if setup_task_id is None else
-                SubmissionPolicyMutationIdempotencyRecord.setup_task_id == setup_task_id,
+                _matches(SubmissionPolicyMutationIdempotencyRecord.setup_task_id, setup_task_id),
-                SubmissionPolicyMutationIdempotencyRecord.correlation_id
-                .is_(None) if correlation_id is None else
-                SubmissionPolicyMutationIdempotencyRecord.correlation_id == correlation_id,
+                _matches(SubmissionPolicyMutationIdempotencyRecord.correlation_id, correlation_id),
                 SubmissionPolicyMutationIdempotencyRecord.status == "pending",
             )

Apply the same helper to _find_namespace, where idempotency_key, setup_task_id, and correlation_id also use plain == against values that the type signature allows to be None.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/modules/projects/submission_policy_mutation_repository.py` around
lines 195 - 220, Introduce a shared _matches helper that returns a null-safe
equality predicate, then replace the five inline nullable conditional
expressions in the shown query with calls to it. Apply the same helper in
_find_namespace for idempotency_key, setup_task_id, and correlation_id, while
leaving non-nullable predicates unchanged.
backend/tests/test_authorization.py (1)

5976-6027: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the duplicated derive-resource setup into a helper.

Lines 5978-6017 and lines 6169-6208 build the identical custody context, resource context, caller input, and project scope. Extract one module-level helper that returns the (caller_input, scope) pair for PROJECT_SUBMISSION_ARTIFACT_POLICY_DERIVE, then call it from both tests. This removes about 40 duplicated lines and keeps the two matrix tests in sync when the resource context gains fields in 12F2.

♻️ Proposed helper
def _submission_policy_derive_prepare_inputs() -> tuple[
    PreparedAuthorizationInput, PreparedAuthorityScope
]:
    """Build the exact derive caller input and project scope shared by matrix tests."""
    project_id, guide_id, snapshot_id, policy_id, operation_id = (uuid4() for _ in range(5))
    custody = ProjectSetupServiceCustodyContext(
        setup_run_id=uuid4(),
        scope_project_id=project_id,
        guide_id=guide_id,
        source_snapshot_id=snapshot_id,
        setup_generation=1,
        expected_step="submission_artifact_policy",
        task_id=uuid4(),
        correlation_id=uuid4(),
        stale_output_digest=DIGEST,
    )
    resource = ProjectSubmissionArtifactPolicyMutationResourceContext(
        resource_type="project_submission_artifact_policy_mutation",
        resource_id=policy_id,
        operation_id=operation_id,
        request_digest=DIGEST,
        scope_project_id=project_id,
        guide_id=guide_id,
        guide_version="1",
        source_snapshot_id=snapshot_id,
        source_snapshot_hash=DIGEST,
        target_kind="derive",
        execution_kind="setup_service",
        policy_id=policy_id,
        policy_version="1",
        policy_generation=1,
        setup_generation=1,
        stale_output_digest=DIGEST,
        setup_service_custody=custody,
    )
    return (
        PreparedAuthorizationInput(
            idempotency_key=uuid4(), request_value=resource.model_dump(mode="json")
        ),
        PreparedAuthorityScope(kind=PreparedAuthorityScopeKind.PROJECT, project_id=project_id),
    )

Then in both tests:

     caller_input = PreparedAuthorizationInput(idempotency_key=uuid4(), request_value={})
     scope = PreparedAuthorityScope(kind=PreparedAuthorityScopeKind.SYSTEM)
     if action_id is ActionId.PROJECT_SUBMISSION_ARTIFACT_POLICY_DERIVE:
-        project_id, guide_id, snapshot_id, policy_id, operation_id = (
-            uuid4() for _ in range(5)
-        )
-        ...
+        caller_input, scope = _submission_policy_derive_prepare_inputs()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/test_authorization.py` around lines 5976 - 6027, Extract the
duplicated derive setup into a module-level helper named
_submission_policy_derive_prepare_inputs that returns the
PreparedAuthorizationInput and PreparedAuthorityScope pair, preserving all
existing custody and resource fields. Replace the inline
PROJECT_SUBMISSION_ARTIFACT_POLICY_DERIVE setup in both matrix tests with calls
to this helper, while leaving other action setup unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
@.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-12F1-pr-trust-bundle.md:
- Around line 59-60: Update the checklist entries for “Historical null
provenance survives upgrade and empty roundtrip” and “Pending replay and
admitted audit evidence block downgrade” to remain pending until hosted Backend
PostgreSQL/Alembic migration and custody results for the exact pushed head are
available; link those results before marking them complete.

In `@backend/alembic/versions/0057_submission_policy_authority.py`:
- Around line 119-121: The constraint operations around
ck_submission_policy_creation_authority_shape use only the suffix instead of the
generated physical constraint name. Update the migration’s create and drop
operations to consistently use the naming convention’s full name, or drop each
constraint by its definition with ALTER TABLE; ensure every affected constraint
operation targets the actual physical name.
- Around line 418-421: The reservation lookup in the submission policy authority
migration can silently accept multiple committed rows because `select ... into
reservation` in the trigger path only picks the first match. Update the
`submission_policy_mutation_idempotency_records` handling around the committed
reservation check to fail closed by either adding a partial unique index on
`(committed_policy_id, action_id)` where `status='committed'` or switching the
`select ... into reservation` in the relevant trigger/function flow to `into
strict` so duplicate committed reservations cannot be bound.
- Around line 641-665: Update downgrade() to acquire SHARE ROW EXCLUSIVE locks
on submission_policy_mutation_idempotency_records, submission_artifact_policies,
effective_project_submission_artifact_policies, pre_submit_checker_policies, and
audit_events before reading replay_count, provenance_count, or audit_count.
Preserve the existing evidence guard, then remove the later redundant
audit_events lock after the destructive operations begin.

In `@backend/app/modules/authorization/prepared.py`:
- Around line 402-405: Add ActionId._SUBMISSION_POLICY_MUTATIONS and
ProjectSubmissionArtifactPolicyMutationResourceContext to the supported
prepared-denial check in AuthorizationService._complete_prepared_denial() within
kernel.py, so unsupported submit-policy mutations that fail binding validation
are recorded as prepared denials instead of raising TypeError.
- Around line 509-550: Update the authorization preparation validation block
around ProjectSubmissionArtifactPolicyMutationResourceContext.model_validate to
import and catch pydantic.ValidationError alongside the existing exceptions,
preserving the PreparedAuthorizationHandleInvalid conversion for malformed
request values. Apply the same ValidationError handling to every Pydantic
validation path in this block so failures remain fail-closed.

In `@backend/app/modules/projects/models.py`:
- Around line 1282-1312: Bind each scope-project column to the row’s project_id
instead of only checking non-null: update creation_scope_project_id in
ck_submission_policy_creation_authority_shape, approval_scope_project_id in
ck_submission_policy_approval_authority_shape, and creation_scope_project_id in
ck_effective_submission_policy_authority_shape and
ck_pre_submit_policy_authority_shape. Apply the corresponding predicate changes
in backend/alembic/versions/0057_submission_policy_authority.py lines 119-151
and the shared output_shape at lines 152-163; no direct changes are needed at
other sites.

In `@backend/tests/test_projects.py`:
- Line 9371: Replace the `_find_namespace` assignment on `repository` with a
callable stub for `GuideSufficiencyMutationReplayRepository.find`, ensuring
`reserve` receives the intended replay result and exercises its reserve path
against the fake Session.

---

Nitpick comments:
In `@backend/app/modules/projects/submission_policy_mutation_repository.py`:
- Around line 195-220: Introduce a shared _matches helper that returns a
null-safe equality predicate, then replace the five inline nullable conditional
expressions in the shown query with calls to it. Apply the same helper in
_find_namespace for idempotency_key, setup_task_id, and correlation_id, while
leaving non-nullable predicates unchanged.

In `@backend/app/modules/projects/submission_policy_mutation_service.py`:
- Around line 119-122: Add the appropriate typed-tuple return annotation to
SubmissionPolicyMutationService.reserve_replay, matching the return type of
SubmissionPolicyMutationReplayRepository.reserve, and add any required typing
imports. Preserve the existing delegation and transaction behavior.
- Around line 79-88: Update the service identity comparison in _replay_values()
for setup_service replays to use ServiceIdentity.PROJECT_SETUP instead of the
hard-coded "workstream.project.setup" string. Import and reuse ServiceIdentity
from app.modules.actors.service_identities, leaving the other custody
validations unchanged.

In `@backend/tests/test_authorization.py`:
- Around line 5976-6027: Extract the duplicated derive setup into a module-level
helper named _submission_policy_derive_prepare_inputs that returns the
PreparedAuthorizationInput and PreparedAuthorityScope pair, preserving all
existing custody and resource fields. Replace the inline
PROJECT_SUBMISSION_ARTIFACT_POLICY_DERIVE setup in both matrix tests with calls
to this helper, while leaving other action setup unchanged.

In `@backend/tests/test_projects.py`:
- Around line 9466-9474: Add tests in the existing
SubmissionPolicyMutationReplayRepository coverage to exercise the claimed
reserve integrity branch by setting session.scalar_result to a UUID and
session.get_result to None, then asserting ProjectRepositoryIntegrityError. Also
configure _find_namespace inputs with a service identity and setup_run_id,
setup_generation, setup_task_id, correlation_id, and action_id, and assert the
service namespace lookup path is used.
- Around line 9637-9651: Add assertions in the service replay reservation test
around `service.reserve_replay`: verify the unchanged `service_facts` succeeds
before testing mutated custody values, and add coverage for
`_require_root_transaction` when the root transaction exists but `is_active` is
false, preserving the existing `None` and nested transaction assertions.
- Around line 9743-9753: Update the competing insert setup in the reserve test
to remove the fixed asyncio.sleep and wait deterministically until the task
started by reserve_second is actually blocked on the database lock. Use the
existing async test flow around
SubmissionPolicyMutationReplayRepository(second).reserve and poll
pg_stat_activity for the competing backend’s lock wait state before calling
first.commit(), then keep the current assertions on second_result unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 28882d53-1960-42ce-935b-d1f97b1a5663

📥 Commits

Reviewing files that changed from the base of the PR and between 7f395d4 and 91f8aa0.

📒 Files selected for processing (18)
  • .agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md
  • .agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-12F1-submission-policy-authority-foundation.md
  • .agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-12F1-internal-review-evidence.md
  • .agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-12F1-pr-trust-bundle.md
  • .github/workflows/backend.yml
  • backend/alembic/versions/0057_submission_policy_authority.py
  • backend/app/modules/audit/schemas.py
  • backend/app/modules/authorization/kernel.py
  • backend/app/modules/authorization/prepared.py
  • backend/app/modules/authorization/runtime.py
  • backend/app/modules/projects/models.py
  • backend/app/modules/projects/submission_policy_mutation_repository.py
  • backend/app/modules/projects/submission_policy_mutation_service.py
  • backend/tests/test_alembic.py
  • backend/tests/test_authorization.py
  • backend/tests/test_projects.py
  • docs/operations_authorization_service.md
  • docs/spec_authorization_service.md

Comment thread backend/alembic/versions/0057_submission_policy_authority.py
Comment thread backend/alembic/versions/0057_submission_policy_authority.py
Comment thread backend/alembic/versions/0057_submission_policy_authority.py
Comment thread backend/app/modules/authorization/prepared.py
Comment on lines +509 to +550
if action_id in {
ActionId.PROJECT_SUBMISSION_ARTIFACT_POLICY_CREATE,
ActionId.PROJECT_SUBMISSION_ARTIFACT_POLICY_DERIVE,
ActionId.PROJECT_SUBMISSION_ARTIFACT_POLICY_UPDATE,
ActionId.PROJECT_SUBMISSION_ARTIFACT_POLICY_APPROVE,
}:
try:
value = dict(caller_input.request_value)
for field in (
"resource_id",
"operation_id",
"scope_project_id",
"guide_id",
"source_snapshot_id",
"policy_id",
):
value[field] = UUID(str(value[field]))
raw_custody = value.get("setup_service_custody")
if raw_custody is not None:
custody = dict(raw_custody)
for field in (
"setup_run_id",
"scope_project_id",
"guide_id",
"source_snapshot_id",
"task_id",
"correlation_id",
):
custody[field] = UUID(str(custody[field]))
value["setup_service_custody"] = custody
resource = ProjectSubmissionArtifactPolicyMutationResourceContext.model_validate(
value
)
except (KeyError, TypeError, ValueError) as exc:
raise PreparedAuthorizationHandleInvalid(
"invalid prepared authorization handle"
) from exc
expected_target = PROJECT_SUBMISSION_POLICY_TARGET_KIND_BY_ACTION[action_id]
if resource.target_kind != expected_target:
raise PreparedAuthorizationHandleInvalid("invalid prepared authorization handle")
submission_policy_context = resource.model_dump(mode="json")
submission_policy_resource_digest = authorization_resource_digest(resource)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm the pinned Pydantic version and check whether tests assert the mapped exception.
set -euo pipefail

# Find the declared pydantic constraint.
fd -t f 'pyproject.toml|requirements.*\.txt|constraints.*\.txt' backend . --exec rg -n 'pydantic' {} \;

# Check whether any test asserts PreparedAuthorizationHandleInvalid for malformed submission-policy input.
rg -n -C4 'PreparedAuthorizationHandleInvalid' backend/tests --glob '*.py' | rg -n -C4 -i 'submission'

Repository: Flow-Research/workstream

Length of output: 234


🌐 Web query:

Does pydantic v2 ValidationError inherit from ValueError?

💡 Result:

No, Pydantic v2's ValidationError does not inherit from ValueError [1]. It is a distinct exception class [1]. In Pydantic v2, ValidationError is implemented in Rust via pydantic-core, which makes it a separate type that does not follow the standard Python exception hierarchy of inheriting from Exception or ValueError in the way traditional Python classes do [1]. It is important to note that while ValidationError does not inherit from ValueError, you are encouraged to raise ValueError (or AssertionError) within your custom validators; Pydantic will catch these exceptions and automatically wrap them into a Pydantic ValidationError [2][3][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)(pyproject\.toml|uv\.lock|poetry\.lock|requirements.*\.txt|constraints.*\.txt)$|backend/app/modules/authorization/prepared\.py|Backend|pydantic'

echo "== pyproject snippets =="
fd -t f 'pyproject.toml|uv.lock|poetry.lock|requirements.*\.txt|constraints.*\.txt' . --exec sh -c '
  echo "--- {}"
  rg -n "^(include\s*=|packages\s*=|pyproject\.toml|pydantic|pydantic-settings|pydantic-core|# package metadata)" "{}" || true
' \;

echo "== local pydantic availability =="
python3 - <<'PY'
try:
    import pydantic
    print("pydantic", pydantic.__version__)
    print("ValidationError bases", pydantic.ValidationError.__bases__)
    print("isinstance check", isinstance(pydantic.ValidationError("x"), (KeyError, TypeError, ValueError)))
except Exception as e:
    print(type(e).__name__, e)
PY

echo "== relevant code =="
sed -n '480,570p' backend/app/modules/authorization/prepared.py

echo "== pydantic imports in prepared.py =="
rg -n "import pydantic|from pydantic|ValidationError" backend/app/modules/authorization/prepared.py

Repository: Flow-Research/workstream

Length of output: 4983


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== backend/pyproject.toml =="
cat -n backend/pyproject.toml

echo "== backend/uv.lock pydantic/core occurrences =="
rg -n "pydantic|pydantic-core|Name = \"pydantic\"|Name = \"pydantic-core\"" backend/uv.lock

echo "== mutation requirements =="
cat -n scripts/mutation-requirements.txt

echo "== prepared.py imports and exception definition =="
sed -n '1,80p' backend/app/modules/authorization/prepared.py

Repository: Flow-Research/workstream

Length of output: 50380


Catch pydantic.ValidationError explicitly.

model_validate raises pydantic.ValidationError, and backend/uv.lock pins Pydantic 2.13.4. This exception is not covered by the surrounding except (KeyError, TypeError, ValueError), so malformed request_value escapes instead of raising PreparedAuthorizationHandleInvalid. Import pydantic.ValidationError and add it to this fail-closed handler; apply the same catch to the Pydantic validation paths in this block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/modules/authorization/prepared.py` around lines 509 - 550, Update
the authorization preparation validation block around
ProjectSubmissionArtifactPolicyMutationResourceContext.model_validate to import
and catch pydantic.ValidationError alongside the existing exceptions, preserving
the PreparedAuthorizationHandleInvalid conversion for malformed request values.
Apply the same ValidationError handling to every Pydantic validation path in
this block so failures remain fail-closed.

Comment thread backend/app/modules/projects/models.py
Comment thread backend/tests/test_projects.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
backend/tests/test_authorization.py (1)

6179-6186: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test every canonical binding field.

This test changes only compilation.catalogue_version. A matcher that omits operation_id, lineage, output digests, or another compilation field can still pass.

Parameterize mutations for every bound top-level and nested field. Assert that each changed resource fails _submission_policy_binding_matches.

As per coding guidelines, “New or materially changed backend subsystems must maintain at least 90% test coverage.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/test_authorization.py` around lines 6179 - 6186, Expand the
test around _submission_policy_binding_matches by parameterizing resource
mutations for every canonical binding field, including operation_id, lineage,
output digests, and all relevant compilation fields. For each mutation, assert
the changed resource does not match the binding, while preserving the existing
catalogue_version case. Ensure the added coverage meets the required 90%
threshold.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/app/modules/projects/models.py`:
- Line 1295: Update the provenance CHECK constraints in
backend/app/modules/projects/models.py at lines 1295-1295, 1315-1315, 1425-1425,
and 1522-1522: require each corresponding scope project ID to be non-null before
evaluating its equality with project_id, using creation_scope_project_id or
approval_scope_project_id as applicable. Add a database-level test covering
otherwise complete provenance with a NULL scope project and assert that
insertion is rejected.

---

Nitpick comments:
In `@backend/tests/test_authorization.py`:
- Around line 6179-6186: Expand the test around
_submission_policy_binding_matches by parameterizing resource mutations for
every canonical binding field, including operation_id, lineage, output digests,
and all relevant compilation fields. For each mutation, assert the changed
resource does not match the binding, while preserving the existing
catalogue_version case. Ensure the added coverage meets the required 90%
threshold.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 68cda307-d23c-449c-bda9-9314c9d0029c

📥 Commits

Reviewing files that changed from the base of the PR and between 91f8aa0 and ed5f456.

📒 Files selected for processing (12)
  • .agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-12F1-submission-policy-authority-foundation.md
  • .agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-12F1-pr-trust-bundle.md
  • backend/alembic/versions/0057_submission_policy_authority.py
  • backend/app/modules/authorization/kernel.py
  • backend/app/modules/projects/models.py
  • backend/app/modules/projects/submission_policy_mutation_repository.py
  • backend/app/modules/projects/submission_policy_mutation_service.py
  • backend/tests/conftest.py
  • backend/tests/test_alembic.py
  • backend/tests/test_authorization.py
  • backend/tests/test_projects.py
  • backend/tests/test_review_lease_persistence.py
🚧 Files skipped from review as they are similar to previous changes (7)
  • .agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/reviews/WS-AUTH-001-12F1-pr-trust-bundle.md
  • backend/app/modules/projects/submission_policy_mutation_repository.py
  • backend/app/modules/projects/submission_policy_mutation_service.py
  • backend/tests/test_projects.py
  • backend/alembic/versions/0057_submission_policy_authority.py
  • .agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/chunks/WS-AUTH-001-12F1-submission-policy-authority-foundation.md
  • backend/app/modules/authorization/kernel.py

Comment thread backend/app/modules/projects/models.py
@abiorh-claw
abiorh-claw self-requested a review August 5, 2026 15:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
backend/tests/test_alembic.py (1)

536-555: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a match pattern to this pytest.raises(IntegrityError).

The block asserts only the exception type. Any unrelated integrity failure in the insert (for example a foreign-key violation on project_id) satisfies the assertion. Add the expected constraint fragment so the test proves the intended attribution rule.

♻️ Proposed change
-                with pytest.raises(IntegrityError):
+                with pytest.raises(
+                    IntegrityError,
+                    match="submission_policy_mutation_idempotency_records",
+                ):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/test_alembic.py` around lines 536 - 555, Update the
pytest.raises(IntegrityError) assertion in the nested transaction test to
include a match pattern for the intended idempotency attribution constraint
violation. Keep the existing insert and exception type check unchanged, and use
the constraint fragment that identifies the duplicate attribution rule rather
than accepting unrelated integrity errors such as foreign-key failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/alembic/versions/0057_submission_policy_authority.py`:
- Around line 617-628: Update submission_policy_mutation_provenance_custody
around the creation_action_id-null branch to enforce immutable linkage for
completed approval outputs. Ensure updates to submission_artifact_policies,
effective_project_submission_artifact_policies, and pre_submit_checker_policies
cannot replace or alter provenance after completion, rather than being routed
through production custody; preserve the existing partial-provenance validation
for non-approval rows.

---

Nitpick comments:
In `@backend/tests/test_alembic.py`:
- Around line 536-555: Update the pytest.raises(IntegrityError) assertion in the
nested transaction test to include a match pattern for the intended idempotency
attribution constraint violation. Keep the existing insert and exception type
check unchanged, and use the constraint fragment that identifies the duplicate
attribution rule rather than accepting unrelated integrity errors such as
foreign-key failures.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 602fb4c9-f612-43a7-bf9d-b057323c8074

📥 Commits

Reviewing files that changed from the base of the PR and between ed5f456 and 07f9227.

📒 Files selected for processing (5)
  • backend/alembic/versions/0057_submission_policy_authority.py
  • backend/app/modules/projects/models.py
  • backend/tests/conftest.py
  • backend/tests/test_alembic.py
  • backend/tests/test_authorization.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • backend/tests/conftest.py
  • backend/app/modules/projects/models.py
  • backend/tests/test_authorization.py

Comment on lines +617 to +628
if new.creation_action_id is null then
if new.created_by_actor_profile_id is not null
or new.created_via_identity_link_id is not null
or new.created_by_admin_role_grant_id is not null
or new.creation_scope_type is not null
or new.creation_scope_project_id is not null
or new.creation_decision_event_id is not null then
raise exception 'partial effective-policy provenance'
using errcode='23514';
end if;
return null;
end if;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the affected model definitions before inspecting constraints.
ast-grep outline backend/app/modules/projects/models.py --items all

# Find constraints, triggers, and tests that protect output linkage and hashes.
rg -n -C5 \
  'submission_artifact_policy_id|effective_policy_hash|compiled_bundle_hash|\
committed_effective_policy_id|committed_pre_submit_policy_id|\
protect_submission_policy_output_provenance|submission_policy.*custody' \
  backend/alembic/versions backend/app/modules/projects/models.py backend/tests

Repository: Flow-Research/workstream

Length of output: 10205


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the migration around the cited trigger lines.
sed -n '500,660p' backend/alembic/versions/0057_submission_policy_authority.py | cat -n

# Search without literal newline regex syntax.
rg -n -C5 'submission_artifact_policy_id|effective_policy_hash|compiled_bundle_hash|committed_effective_policy_id|committed_pre_submit_policy_id|protect_submission_policy_output_provenance|submission_policy.*custody|submit_state|committed_submission_artifact' backend/alembic/versions backend/app/modules/projects/models.py backend/tests || true

# Find references to the relevant trigger/function names or completed output fields in migrations/test model definitions.
rg -n -C3 'protect_submission_policy|submission_policy_authority|output_submission_artifact|submission_artifact_policy_id|pre_submit_bundle_hash|effective_policy_hash|compiled_bundle_hash' backend/alembic backend/app backend/tests || true

Repository: Flow-Research/workstream

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Extract the migration around the cited trigger conditions and the replay binding.
sed -n '500,640p' backend/alembic/versions/0057_submission_policy_authority.py | cat -n

# Search migration/test files for constraints, triggers, and tests around submitted setup-run fields.
rg -n -C5 'prevent_submission|output_submission_artifact|output_post_submit_checker|submission_artifact_policy_id|pre_submit_checker_bundle_hash|effective_policy_hash|compiled_bundle_hash|protect|immutable|UPDATE|ON UPDATE|trigger|create_trigger|execute' \
  backend/alembic/versions/0057_submission_policy_authority.py backend/tests/backend_alembic_schema_contract tests backend/tests || true

Repository: Flow-Research/workstream

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the migration around the cited replay branch without the broad sed output cap.
python3 - <<'PY'
from pathlib import Path
p = Path('backend/alembic/versions/0057_submission_policy_authority.py')
lines = p.read_text().splitlines()
start, end = 480, 650
for i in range(start, min(end, len(lines)) + 1):
    print(f"{i}: {lines[i-1]}")
PY

# Search exact relevant triggers/functions/tests and print only those files.
python3 - <<'PY'
from pathlib import Path
import re
terms = [
    'output_submission_artifact_policy_id',
    'output_post_submit_checker_policy_id',
    'pre_submit_checker_bundle_hash',
    'compiled_bundle_hash',
    'effective_policy_hash',
    'post_submit_checker_policy_hash',
    'committed_effective_policy_id',
    'prevent_submission',
    'submission_policy_custody',
    'protect_submission_policy',
    'immutable',
    'UPDATE',
]
root = Path('backend')
for pat in ['**/*.py']:
    matches = []
    for p in root.glob(pat):
        try:
            text = p.read_text()
        except UnicodeDecodeError:
            continue
        if any(t.lower() in text.lower() for t in terms):
            lines = text.splitlines()
            hits = []
            for j,l in enumerate(lines, 1):
                if any(t.lower() in l.lower() for t in terms):
                    hits.append((j,l))
            if hits:
                matches.append((str(p), hits[:40]))
    print('\n'.join(f"{m[0]}" for m in matches))
    print("---", sum(len(m[1]) for m in matches), "hits")
PY

Repository: Flow-Research/workstream

Length of output: 19899


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the effective-policy replay branch continuation and the trigger creation.
python3 - <<'PY'
from pathlib import Path
p = Path('backend/alembic/versions/0057_submission_policy_authority.py')
lines = p.read_text().splitlines()
start, end = 615, 690
for i in range(start, min(end, len(lines)) + 1):
    print(f"{i}: {lines[i-1]}")
PY

# Search for project_setup_runs completion-output constraints/triggers in the migration.
python3 - <<'PY'
from pathlib import Path
p = Path('backend/alembic/versions/0057_submission_policy_authority.py')
text = p.read_text()
terms = ["project_setup_runs", "output_submission_artifact_policy_id",
         "output_post_submit_checker_policy_id", "effective_policy_hash",
         "pre_submit_checker_bundle_hash", "compiled_bundle_hash",
         "post_submit_checker_policy_hash", "update", "update on"]
for i, line in enumerate(text.splitlines(), 1):
    if any(t.lower() in line.lower() for t in terms):
        print(f"{i}: {line}")
PY

# Search schema tests for project_setup_runs contract fields and triggers.
python3 - <<'PY'
from pathlib import Path
p = Path('backend/tests/test_alembic.py')
text = p.read_text().splitlines()
for i in range(50, 220):
    if 0 < i <= len(text):
        print(f"{i}: {text[i-1]}")
PY

Repository: Flow-Research/workstream

Length of output: 13631


Enforce immutable output linkage for completed approval outputs.

The current submission_policy_mutation_provenance_custody only validates approval outputs on the initial approval_action_id; subsequent updates to submission_artifact_policies, effective_project_submission_artifact_policies, or pre_submit_checker_policies use production custody instead. Add a completed-output immutality check in this branch or a separate trigger/constraint for completed outputs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/alembic/versions/0057_submission_policy_authority.py` around lines
617 - 628, Update submission_policy_mutation_provenance_custody around the
creation_action_id-null branch to enforce immutable linkage for completed
approval outputs. Ensure updates to submission_artifact_policies,
effective_project_submission_artifact_policies, and pre_submit_checker_policies
cannot replace or alter provenance after completion, rather than being routed
through production custody; preserve the existing partial-provenance validation
for non-approval rows.

@abiorh-claw
abiorh-claw merged commit 5a4186c into main Aug 5, 2026
11 of 12 checks passed
@abiorh-claw
abiorh-claw deleted the codex/ws-auth-001-12f1-submission-policy-foundation branch August 6, 2026 03:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants