Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
1f0f7de
plan supported operator checker recovery
Oct 16, 2026
e0fa067
tighten checker recovery project custody plan
Oct 16, 2026
bdcbecf
expose supported operator checker recovery
Oct 16, 2026
a3eae54
tighten operator recovery proof
Oct 16, 2026
6cca76b
repair recovery boundary and audit integration
Oct 16, 2026
7f7f467
repair operator recovery denial boundary
Oct 16, 2026
cbdb97c
restore public recovery fixture registration
Oct 16, 2026
1129b04
surface sanitized recovery API failures
Oct 16, 2026
e4a339c
fix checker recovery read evidence boundary
Oct 16, 2026
2a42983
fix: admit operational read audit evidence
Oct 16, 2026
c3a7a46
test checker recovery audit construction
Oct 16, 2026
fc3dc8a
fix(db): bound operational audit revision id
Oct 16, 2026
86d30c2
docs record canonical audit migration order
Oct 16, 2026
0c20a73
test(db): refresh operational audit schema fingerprint
Oct 16, 2026
d890e6a
docs bind recovery to schema fingerprint owner
Oct 16, 2026
596b3c3
merge canonical checker registry prerequisite
Oct 16, 2026
1aeea46
chore reconcile combined authorization snapshot
Oct 16, 2026
19a2c50
test: inspect labeled checker recovery projection
Oct 16, 2026
5c77cae
test(api): bind registry rate-control routes
Oct 16, 2026
cf033f3
fix: preserve checker recovery failure boundaries
Oct 16, 2026
dbada3b
merge current checker registry prerequisite
Oct 16, 2026
7af59ae
merge final checker registry prerequisite record
Oct 16, 2026
308a712
merge canonical all-route rate inventory repair
Oct 16, 2026
bf75545
fix: close checker recovery boundary gaps
Oct 16, 2026
2e2df61
merge canonical protected-route rate inventory
Oct 16, 2026
d1960d5
test: keep recovery HTTP proof structurally bounded
Oct 16, 2026
a2b2f45
test: register recovery HTTP proof lane
Oct 16, 2026
909660c
Merge commit 'fd215ebd969247f7aac42acd9f5bbc3f947a3fab' into codex/mv…
Oct 16, 2026
f4327d8
fix: preserve recovery denial translation
Oct 16, 2026
ea92136
test: repair recovery lane ownership
Oct 17, 2026
9629f7d
Merge remote-tracking branch 'origin/codex/mvp-checker-registration-5…
Oct 17, 2026
6f85c7b
test: expose bounded recovery failure frames
Oct 17, 2026
6faab0a
test: bound recovery diagnostic capture
Oct 17, 2026
b4ec6f5
test: bind stale retry to source denial
Oct 17, 2026
69a2bf9
fix: start public checker retry transaction
Oct 17, 2026
1c05f84
Merge commit '0e55558a2c2f4a55a0c730865282b7f946f1e12d' into codex/mv…
Oct 17, 2026
fd6471e
docs: bind recovery auth contract provenance
Oct 17, 2026
4ab8751
fix: combine recovery and contributor read dispatch
Oct 17, 2026
020d3d5
Merge commit '2aa661bb9bb19e0970b12f05527454de77d30163' into codex/mv…
Oct 17, 2026
ee4a46f
docs: reconcile recovery evidence with canonical base
Oct 17, 2026
ad9c261
fix: retain exact retry prepare denials
Oct 17, 2026
c675781
fix: bind checker retry prepare denial
Oct 17, 2026
aa9b9f9
Merge registry prerequisite migration repair
Oct 17, 2026
5dea340
fix: bound public checker retry response
Oct 17, 2026
1be2381
test: close checker retry public projection
Oct 17, 2026
667e5a0
test: preserve retry replay audit identity
Oct 17, 2026
f9196be
docs: record checker recovery proof
Oct 17, 2026
e898db6
Merge canonical rate inventory reconciliation
Oct 17, 2026
b29783c
docs: bind final recovery checks
Oct 17, 2026
f936e41
docs: close recovery proof contradictions
Oct 17, 2026
f6dbc4d
test: keep recovery proof structurally bounded
Oct 17, 2026
7a99673
Merge commit '317fe89a12960ccf3cd781955575d43fce26efd1' into codex/mv…
Oct 17, 2026
62950ac
docs(commitrail): reconcile recovery registry base
Oct 17, 2026
0464524
fix(checkers): conceal superseded retry sources
Oct 17, 2026
f717826
docs(commitrail): record stale-source retry proof
Oct 17, 2026
b85953a
Merge remote-tracking branch 'origin/main' into codex/mvp-operator-re…
Oct 17, 2026
5cbf4f3
docs(commitrail): reconcile merged registry prerequisite
Oct 17, 2026
4a25843
docs(commitrail): complete operator recovery record
Oct 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .ci/behavior-ownership/partition.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@
"group": "shared",
"target": "backend/app/api/deps/authorization.py"
},
{
"group": "artifacts",
"target": "backend/app/api/deps/checker_recovery.py"
},
{
"group": "artifacts",
"target": "backend/app/api/deps/checker_registry.py"
Expand Down Expand Up @@ -592,6 +596,10 @@
"group": "auth",
"target": "backend/app/modules/authorization/checker_retry_authorization.py"
},
{
"group": "auth",
"target": "backend/app/modules/authorization/checker_retry_read.py"
},
{
"group": "auth",
"target": "backend/app/modules/authorization/contribution_policy_authorization.py"
Expand Down Expand Up @@ -636,6 +644,10 @@
"group": "auth",
"target": "backend/app/modules/authorization/domain/checker_retry.py"
},
{
"group": "auth",
"target": "backend/app/modules/authorization/domain/checker_retry_reads.py"
},
{
"group": "auth",
"target": "backend/app/modules/authorization/domain/contribution_policies.py"
Expand Down Expand Up @@ -2105,7 +2117,7 @@
"target": "backend/scripts/validate_test_lane_evidence.py"
}
],
"authority_digest": "72ede56f55522908d9102eaad07c05349bc6485c314d8fa9c57631b5e797641b",
"authority_digest": "63bd7c5bf88c7ee20bbec0dbcc08359c5e77d7acc79dea93363cf61f9b108c73",
"protected_base_commit": "7676ce4347db0c9694962a9b587a20765e16eac6",
"schema": "workstream.behavior-ownership-partition.v1"
}
241 changes: 241 additions & 0 deletions .commitrail/changes/mvp-operator-checker-recovery.md

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions backend/app/adapters/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,22 @@ def task_queue_authorization(kernel, secret):
return TaskQueueReadAuthorization(kernel, secret)


def checker_retry_discovery_authorization(session, context, secret):
"""Compose exact Operator retry-source reads through the shared AUTH kernel."""
from app.modules.authorization.checker_retry_read import (
CheckerRetryDiscoveryAuthorization,
)
from app.modules.authorization.kernel import AuthorizationService

repository = AdminAuthorizationRepository(session)
return CheckerRetryDiscoveryAuthorization(
AuthorizationService(
session, context, admin_repository=repository,
),
secret,
)


def contributor_read_authorization(kernel, session: AsyncSession, secret):
"""Compose the caller's own-work AUTH adapter in the shared decision transaction."""
from app.modules.authorization.contributor_read import ContributorReadAuthorization
Expand Down
11 changes: 8 additions & 3 deletions backend/app/adapters/checkers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,10 +281,12 @@ def evaluation_coordinator(session) -> EvaluationCoordinationPort:
return EvaluationCoordinator(session, tasks=evaluation_task_guard(session))


def checker_retry_service(session, context):
"""Compose hidden Operator retry from existing owner operations only."""
def checker_retry_service(session, context, *, cursor_secret=None):
"""Compose supported Operator recovery from existing owner operations."""
from app.adapters.audit import checker_retry_audit
from app.adapters.auth import checker_retry_authorization
from app.adapters.auth import (
checker_retry_authorization, checker_retry_discovery_authorization,
)
from app.adapters.outbox import outbox_append
from app.adapters.tasks import evaluation_task_guard
from app.modules.checkers.checker_retry import CheckerRetryService
Expand All @@ -295,6 +297,9 @@ def checker_retry_service(session, context):
session, actor_profile_id=context.actor_profile_id,
tasks=tasks, evaluations=EvaluationCoordinator(session, tasks=tasks),
authorization=checker_retry_authorization(session, context),
discovery=checker_retry_discovery_authorization(
session, context, cursor_secret,
),
events=outbox_append(session), audit=checker_retry_audit(session),
)

Expand Down
214 changes: 214 additions & 0 deletions backend/app/api/deps/checker_recovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
"""HTTP transaction roots for exact Operator checker recovery."""

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Annotated
from uuid import UUID

from fastapi import Depends, Request
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession

from app.adapters.checkers import checker_retry_service
from app.api.deps.auth import get_auth_verification_result
from app.api.deps.authorization import (
AuthorizationDenied,
AuthorizationEvidenceUnavailable,
ResolvedActor,
_authorization_context,
get_authorization_actor,
)
from app.core.api_controls import StructuredHTTPException, parse_idempotency_key, request_ids
from app.db.session import get_db_session
from app.modules.checkers.api.execution import (
CheckerExecutionUnavailable,
CheckerRequestConflict,
CheckerRetryAuthorityDenied,
CheckerRetryCommand,
CheckerRetryCursorInvalid,
CheckerRetryOperationUnavailable,
CheckerRetryRequest,
CheckerRetryResult,
CheckerRetrySourceUnavailable,
CheckerRetrySourcePage,
CheckerRetrySourceQuery,
)
from app.schemas.auth import AuthVerificationResult


def require_checker_retry_key(request: Request) -> UUID:
"""Reject absent, duplicate or malformed retry keys before product mutation."""
values = request.headers.getlist("Idempotency-Key")
return parse_idempotency_key(values[0] if len(values) == 1 else "")


async def require_checker_retry_human(
key: Annotated[UUID, Depends(require_checker_retry_key)],
result: Annotated[AuthVerificationResult, Depends(get_auth_verification_result)],
) -> None:
"""Conceal nonhuman retry callers before resolving product dependencies."""
del key
if result.token.subject_kind != "human":
raise _concealed()


def _concealed() -> StructuredHTTPException:
return StructuredHTTPException(
status_code=404,
detail="Checker recovery source not found",
error_code="checker_recovery_source_not_found",
error_message="Checker recovery source not found",
)


def _unavailable() -> StructuredHTTPException:
return StructuredHTTPException(
status_code=503,
detail="Checker recovery unavailable",
error_code="checker_recovery_unavailable",
error_message="Checker recovery unavailable",
retryable=True,
)


class CheckerRetryCommands:
"""Bind the public selector/body to CHECKERS's existing atomic writer."""

def __init__(self, session: AsyncSession, service, key: UUID) -> None:
self._session, self._service, self._key = session, service, key

async def retry(
self, project_id: UUID, source_run_id: UUID, request: CheckerRetryRequest,
) -> CheckerRetryResult:
result = await self._service.retry(CheckerRetryCommand(
project_id=project_id,
source_run_id=source_run_id,
request_key=self._key,
reason=request.reason,
))
await self._session.commit()
return result


class CheckerRetryReads:
"""Authorize, project and commit one exact discovery page."""

def __init__(self, session: AsyncSession, service) -> None:
self._session, self._service = session, service

async def discover(
self, project_id: UUID, *, limit: int, cursor: str | None,
) -> CheckerRetrySourcePage:
result = await self._service.discover(CheckerRetrySourceQuery(
project_id=project_id, limit=limit, cursor=cursor,
))
await self._session.commit()
return result


@asynccontextmanager
async def _checker_retry_commands(
request: Request,
key: UUID,
resolved: ResolvedActor,
session: AsyncSession,
) -> AsyncIterator[CheckerRetryCommands]:
"""Own mutation commit, canonical denial evidence and fail-closed rollback."""
context = _authorization_context(resolved, key, key)
await session.rollback()
service = checker_retry_service(
session,
context,
cursor_secret=request.app.state.settings.pagination_cursor_hmac_secret,
)
try:
await session.begin()
yield CheckerRetryCommands(session, service, key)
except CheckerRetryAuthorityDenied as exc:
await session.rollback()
try:
if await service.restage_denial(exc):
await session.commit()
except (AuthorizationEvidenceUnavailable, SQLAlchemyError) as evidence_error:
await session.rollback()
raise _unavailable() from evidence_error
raise _concealed() from exc
except CheckerRequestConflict as exc:
await session.rollback()
raise StructuredHTTPException(
status_code=409,
detail="Checker retry request conflicts with retained facts",
error_code="checker_retry_request_conflict",
error_message="Checker retry request conflicts with retained facts",
) from exc
except CheckerRetrySourceUnavailable as exc:
await session.rollback()
raise _concealed() from exc
except (
CheckerRetryOperationUnavailable,
AuthorizationEvidenceUnavailable,
SQLAlchemyError,
) as exc:
await session.rollback()
raise _unavailable() from exc
except BaseException:
await session.rollback()
raise
finally:
if session.in_transaction():
await session.rollback()


async def get_checker_retry_commands(
request: Request,
key: Annotated[UUID, Depends(require_checker_retry_key)],
resolved: Annotated[ResolvedActor, Depends(get_authorization_actor)],
session: Annotated[AsyncSession, Depends(get_db_session)],
) -> AsyncIterator[CheckerRetryCommands]:
"""Forward endpoint failures through the mutation transaction owner."""
async with _checker_retry_commands(request, key, resolved, session) as commands:
yield commands


async def get_checker_retry_reads(
request: Request,
resolved: Annotated[ResolvedActor, Depends(get_authorization_actor)],
session: Annotated[AsyncSession, Depends(get_db_session)],
) -> AsyncIterator[CheckerRetryReads]:
"""Own read decision commit and canonical concealed denial evidence."""
request_id, correlation_id = (UUID(value) for value in request_ids(request))
context = _authorization_context(resolved, request_id, correlation_id)
await session.rollback()
service = checker_retry_service(
session,
context,
cursor_secret=request.app.state.settings.pagination_cursor_hmac_secret,
)
try:
yield CheckerRetryReads(session, service)
except AuthorizationDenied as exc:
await session.rollback()
try:
if await service.restage_discovery_denial(exc):
await session.commit()
except (AuthorizationEvidenceUnavailable, SQLAlchemyError) as evidence_error:
await session.rollback()
raise _unavailable() from evidence_error
raise _concealed() from exc
except CheckerRetryCursorInvalid as exc:
await session.rollback()
raise StructuredHTTPException(
status_code=422,
detail="Invalid checker recovery cursor",
error_code="validation_error",
error_message="Invalid checker recovery cursor",
) from exc
except (CheckerExecutionUnavailable, AuthorizationEvidenceUnavailable, SQLAlchemyError) as exc:
await session.rollback()
raise _unavailable() from exc
except BaseException:
await session.rollback()
raise
finally:
if session.in_transaction():
await session.rollback()
9 changes: 8 additions & 1 deletion backend/app/modules/authorization/catalogue.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ class ActionId(StrEnum):
OPERATIONS_TASK_START_OVERRIDE = "operations.task.start_override"
OPERATIONS_SUBMISSION_GATE_REPAIR = "operations.submission_gate.repair"
OPERATIONS_CHECKER_RETRY = "operations.checker.retry"
OPERATIONS_CHECKER_RETRY_SOURCE_LIST = "operations.checker_retry_source.list"
SUBMISSION_CREATE = "submission.create"
REVIEW_QUEUE_READ = "review.queue.read"
REVIEW_QUEUE_INSPECT = "review.queue.inspect"
Expand Down Expand Up @@ -621,6 +622,11 @@ def _active(
ActionOwner.AUTH_14,
),
_active(ActionId.OPERATIONS_CHECKER_RETRY, PermissionId.OPERATIONS_CHECKER_RETRY, ActionOwner.AUTH_14),
_active(
ActionId.OPERATIONS_CHECKER_RETRY_SOURCE_LIST,
PermissionId.OPERATIONS_STATUS_READ,
ActionOwner.AUTH_14,
),
_active(
ActionId.CHECKER_REGISTRY_READ,
PermissionId.OPERATIONS_STATUS_READ,
Expand Down Expand Up @@ -823,7 +829,7 @@ def _active(

def _require_catalogue_counts() -> None:
"""Keep the closed action inventory and permission boundary exact."""
if len(PERMISSION_IDS) != 82 or len(ACTION_IDS) != 163:
if len(PERMISSION_IDS) != 82 or len(ACTION_IDS) != 164:
raise RuntimeError("authorization catalogue count mismatch")
if len(HISTORICAL_PERMISSION_IDS) != 49 or len(NEW_PERMISSION_IDS) != 33:
raise RuntimeError("authorization permission boundary mismatch")
Expand Down Expand Up @@ -973,6 +979,7 @@ def _index_actions(definitions: tuple[ActionDefinition, ...]) -> MappingProxyTyp
ActionId.TASK_READ, ActionId.TASK_SUBMISSION_REQUIREMENTS_READ,
ActionId.PROJECT_TASK_READ, ActionId.PROJECT_TASK_SUBMISSION_REQUIREMENTS_READ,
ActionId.AUDIT_TASK_EVIDENCE_READ, ActionId.OPERATIONS_CHECKER_RETRY,
ActionId.OPERATIONS_CHECKER_RETRY_SOURCE_LIST,
} | TASK_LOCKED_CONTEXT_READ_ACTIONS | HISTORY_READ_ACTIONS:
raise RuntimeError("authorization active action boundary mismatch")
_validate_action_definitions(definitions)
Expand Down
Loading
Loading