From d5152e3530d9d2369f98db63895dddb9159275b9 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Fri, 14 Aug 2026 00:12:54 +0100 Subject: [PATCH 1/8] feat(auth): register adapter binding actions --- .agent-loop/CURRENT_STATE.md | 3 +- .../CHUNK_MAP.md | 2 +- .../STATUS.md | 8 +- ...CP01A-auth-adapter-binding-registration.md | 9 +- .../ACTIVATION_CUSTODY.md | 4 +- .../CHUNK_MAP.md | 2 +- .../STATUS.md | 4 +- .../STATUS.md | 6 +- .../app/modules/authorization/api/__init__.py | 12 ++ .../authorization/api/adapter_bindings.py | 122 ++++++++++++++++++ .../app/modules/authorization/catalogue.py | 27 +++- .../test_adapter_binding_registration.py | 109 ++++++++++++++++ backend/tests/test_authorization.py | 22 +++- docs/operations_authorization_service.md | 7 +- docs/roadmap_status.md | 7 +- docs/spec_authorization_service.md | 4 + docs/spec_contribution_compensation.md | 68 +++++----- 17 files changed, 366 insertions(+), 50 deletions(-) create mode 100644 backend/app/modules/authorization/api/adapter_bindings.py create mode 100644 backend/tests/authorization/test_adapter_binding_registration.py diff --git a/.agent-loop/CURRENT_STATE.md b/.agent-loop/CURRENT_STATE.md index 6cde2e612..ef7c9563e 100644 --- a/.agent-loop/CURRENT_STATE.md +++ b/.agent-loop/CURRENT_STATE.md @@ -103,7 +103,8 @@ non-executable until PLAN3 merges and its current-main contract is expanded. - WS-ARCH-001-PLAN3 is proposed planning only. - WS-ARCH-001-CP01 is a planned split and non-executable. -- WS-ARCH-001-CP01A is proposed for unavailable adapter-binding registration. +- WS-ARCH-001-CP01A is complete on merge with adapter-binding actions registered + but unavailable. - WS-ARCH-001-CP01B is proposed after CP01A for unavailable ContributionPolicy registration. - WS-ARCH-001-CP02 is proposed and non-executable. diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/CHUNK_MAP.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/CHUNK_MAP.md index 17e2f7c2a..56a3af2bb 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/CHUNK_MAP.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/CHUNK_MAP.md @@ -17,7 +17,7 @@ | `WS-ARCH-001-PLAN2` | Current-main reconciliation around canonical Submission-to-`allow_review` | L1 | Planned; planning contract lands on merge | | `WS-ARCH-001-PLAN3` | ContributionPolicy registration, behavior, activation, guide/task lineage and clean legacy-removal sequencing | L1 | Proposed planning correction; no runtime | | `WS-ARCH-001-CP01` | Combined AUTH registration planning parent | L1 | Planned split into CP01A/CP01B; non-executable | -| `WS-ARCH-001-CP01A` | AUTH adapter-binding unavailable registration | L1 | Proposed executable contract after PLAN3 | +| `WS-ARCH-001-CP01A` | AUTH adapter-binding unavailable registration | L1 | Complete on merge; four actions remain planned/unavailable | | `WS-ARCH-001-CP01B` | AUTH ContributionPolicy unavailable registration | L1 | Proposed executable contract after CP01A | | `WS-ARCH-001-CP02` | CON hidden adapter-binding behavior | L1 | Proposed skeleton after CP01B | | `WS-ARCH-001-CP03` | AUTH exact adapter-binding activation | L1 | Proposed skeleton after CP02 evidence | diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/STATUS.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/STATUS.md index 5c3862b0c..21ffc24fe 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/STATUS.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/STATUS.md @@ -51,15 +51,17 @@ - PLAN3 corrects PLAN2's first implementation dependency. CON-05A is not a valid direct start: missing AUTH registration/activation and hidden CON behavior must precede owner-separated guide/task lineage. CP01 is the - non-executable split parent; CP01A and CP01B are current-main proposed - executable contracts, while CP02-CP09 remain non-executable skeletons. + non-executable split parent; CP01A is complete on merge with its four actions + registered/unavailable, CP01B is the remaining proposed executable contract, + and CP02-CP09 remain non-executable skeletons. ## Proposed PLAN3 children - WS-ARCH-001-PLAN3 is proposed planning only; runtime behavior is unchanged. - WS-ARCH-001-CP01 is a planned split and non-executable. Current-main discovery proved adapter binding and ContributionPolicy are distinct resource/action families. -- WS-ARCH-001-CP01A is proposed: exact unavailable adapter-binding registration. +- WS-ARCH-001-CP01A is complete on merge: exact adapter-binding identifiers, + mappings, typed facts, and digests are registered while unavailable. - WS-ARCH-001-CP01B is proposed after CP01A: exact unavailable ContributionPolicy registration. - WS-ARCH-001-CP02 is proposed: hidden adapter-binding behavior. diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md index 0de0833ab..3840d915d 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md @@ -38,6 +38,13 @@ backend/app/modules/authorization/api/__init__.py backend/app/modules/authorization/api/action_ids.py backend/app/modules/authorization/api/adapter_bindings.py backend/tests/authorization/test_adapter_binding_registration.py +backend/tests/test_authorization.py (closed catalogue/action-owner parity only) +docs/operations_authorization_service.md (catalogue parity only) +docs/spec_authorization_service.md (catalogue parity only) +.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/ACTIVATION_CUSTODY.md (catalogue parity only) +docs/spec_contribution_compensation.md (CP01A registration parity only) +.agent-loop/initiatives/WS-CON-001-contribution-compensation-boundary/STATUS.md (CP01A state only) +docs/roadmap_status.md (CP01A capability state only) .agent-loop/CURRENT_STATE.md .agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/{CHUNK_MAP.md,STATUS.md} .agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md @@ -119,4 +126,4 @@ identity, migration, or product import; update and re-review the contract first. ## Merge state -- Outcome on merge: `planned` +- Outcome on merge: `complete` diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/ACTIVATION_CUSTODY.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/ACTIVATION_CUSTODY.md index c498f3080..0b8c59a61 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/ACTIVATION_CUSTODY.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/ACTIVATION_CUSTODY.md @@ -69,8 +69,10 @@ remains planned and cannot be activated by read/status proof. The historical transfer added no migration because owner and availability are typed metadata. WS-XINT-002-01 reconciles PostgreSQL parity through migration `0036`. The current catalogue -has 73 PermissionIds, 102 ActionIds, 57 active actions, and 45 planned actions, +has 73 PermissionIds, 106 ActionIds, 57 active actions, and 49 planned actions, with fourteen fixed-service identities and twenty-two matrix memberships. +CP01A contributes the four planned/unavailable adapter-binding actions only; +it changes no fixed-service identity or matrix membership. CP01B remains next. ## REV custody transfer 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 cde3b3449..79dd48e67 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 @@ -25,7 +25,7 @@ sequence. | Chunk | Title | Risk | Status | |---|---|---:|---| | `WS-ARCH-001-CP01` | Combined registration planning parent | L1 | Planned split into CP01A/CP01B; non-executable | -| `WS-ARCH-001-CP01A` | Adapter-binding unavailable registration | L1 | Proposed; excludes retirement, callback/fulfillment, identity, evaluator, and activation | +| `WS-ARCH-001-CP01A` | Adapter-binding unavailable registration | L1 | Complete on merge; excludes retirement, callback/fulfillment, identity, evaluator, and activation | | `WS-ARCH-001-CP01B` | ContributionPolicy unavailable registration | L1 | Proposed after CP01A; excludes binding behavior, evaluator, and activation | | `WS-ARCH-001-CP03` | Exact adapter-binding activation | L1 | Proposed after merged CP02 hidden proof | | `WS-ARCH-001-CP05` | Exact ContributionPolicy activation | L1 | Proposed after merged CP04 hidden proof | diff --git a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md index 937c813ad..7010cd284 100644 --- a/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md +++ b/.agent-loop/initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md @@ -42,8 +42,8 @@ not current start requirements. - ART, REV, and CON feature actions remain unavailable until their exact hidden owner behavior and typed manifests are merged. Their activation order lives in the matching XINT and feature-owner records. -- PLAN3 proposes the missing contribution-policy sequence: CP01A registers exact - adapter-binding actions while unavailable and CP01B separately registers +- PLAN3 defines the missing contribution-policy sequence: WS-ARCH-001-CP01A is + complete on merge with exact adapter-binding actions still unavailable; CP01B separately registers policy actions while unavailable; CP03 and CP05 activate only their respective merged hidden behavior. Fulfillment callback authority remains separate and cannot be bundled into adapter-binding registration. diff --git a/.agent-loop/initiatives/WS-CON-001-contribution-compensation-boundary/STATUS.md b/.agent-loop/initiatives/WS-CON-001-contribution-compensation-boundary/STATUS.md index 173c79751..37a4cf23e 100644 --- a/.agent-loop/initiatives/WS-CON-001-contribution-compensation-boundary/STATUS.md +++ b/.agent-loop/initiatives/WS-CON-001-contribution-compensation-boundary/STATUS.md @@ -78,8 +78,10 @@ historical-row backfill or compatibility behavior. - CON-02B: missing `outbox.dispatch`, `workstream.outbox.dispatcher`, exact matrix/context/PREP support, and AUTH activation plan. -- CP01A/CP01B: exact AUTH adapter-binding and ContributionPolicy manifests are not yet - registered; callback/fulfillment authority must remain separate. +- CP01A is complete on merge with four exact adapter-binding actions registered + but unavailable. CP01B ContributionPolicy registration remains missing; + callback/fulfillment authority stays separate and CP02 cannot start before + CP01B merges. - CP02-CP05: hidden behavior and exact activation have not merged. - CP06-CP09: validation, owner schema lineage, and clean legacy removal wait for the preceding public behavior. No historical-row classification is required diff --git a/backend/app/modules/authorization/api/__init__.py b/backend/app/modules/authorization/api/__init__.py index a0061b013..651a60436 100644 --- a/backend/app/modules/authorization/api/__init__.py +++ b/backend/app/modules/authorization/api/__init__.py @@ -1,6 +1,13 @@ """The sole dependency-free public interface of the authorization module.""" from .action_ids import ActionId, PermissionId, action_id, permission_id +from .adapter_bindings import ( + AdapterBindingCreateFacts, + AdapterBindingReadFacts, + AdapterBindingResumeFacts, + AdapterBindingSuspendFacts, + adapter_binding_resource_digest, +) from .decisions import AuthorizationDecision, DecisionOutcome from .errors import ( AuthorizationBoundaryError, @@ -23,6 +30,10 @@ __all__ = ( "ActionId", + "AdapterBindingCreateFacts", + "AdapterBindingReadFacts", + "AdapterBindingResumeFacts", + "AdapterBindingSuspendFacts", "ActorIdentityFacts", "ActorKind", "AuthorizationBoundaryError", @@ -47,5 +58,6 @@ "ResourceFacts", "ResourceValue", "action_id", + "adapter_binding_resource_digest", "permission_id", ) diff --git a/backend/app/modules/authorization/api/adapter_bindings.py b/backend/app/modules/authorization/api/adapter_bindings.py new file mode 100644 index 000000000..9351e2a22 --- /dev/null +++ b/backend/app/modules/authorization/api/adapter_bindings.py @@ -0,0 +1,122 @@ +"""Public AUTH facts for planned compensation adapter-binding actions.""" + +from __future__ import annotations + +from dataclasses import asdict, dataclass +import hashlib +import json +import re +from typing import TypeAlias +from uuid import UUID + +from .action_ids import ActionId + +_TOKEN = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,159}\Z") + + +def _require_token(name: str, value: str) -> None: + if not isinstance(value, str) or not _TOKEN.fullmatch(value): + raise ValueError(f"{name} must be a bounded canonical token") + + +def _require_uuid(name: str, value: UUID) -> None: + if not isinstance(value, UUID): + raise ValueError(f"{name} must be a UUID") + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AdapterBindingReadFacts: + """Exact project and binding identity for a planned binding read.""" + + project_id: UUID + adapter_binding_id: UUID + + def __post_init__(self) -> None: + _require_uuid("project_id", self.project_id) + _require_uuid("adapter_binding_id", self.adapter_binding_id) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AdapterBindingCreateFacts: + """Server-owned facts for one planned binding creation.""" + + project_id: UUID + instrument: str + unit: str + adapter_actor_id: UUID + route_key: str + + def __post_init__(self) -> None: + _require_uuid("project_id", self.project_id) + _require_uuid("adapter_actor_id", self.adapter_actor_id) + _require_token("instrument", self.instrument) + _require_token("unit", self.unit) + _require_token("route_key", self.route_key) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AdapterBindingSuspendFacts: + """Exact active binding targeted by a planned suspension.""" + + project_id: UUID + adapter_binding_id: UUID + expected_status: str = "active" + + def __post_init__(self) -> None: + _require_uuid("project_id", self.project_id) + _require_uuid("adapter_binding_id", self.adapter_binding_id) + if self.expected_status != "active": + raise ValueError("suspension requires active binding status") + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AdapterBindingResumeFacts: + """Exact suspended binding targeted by a planned resumption.""" + + project_id: UUID + adapter_binding_id: UUID + expected_status: str = "suspended" + + def __post_init__(self) -> None: + _require_uuid("project_id", self.project_id) + _require_uuid("adapter_binding_id", self.adapter_binding_id) + if self.expected_status != "suspended": + raise ValueError("resumption requires suspended binding status") + + +AdapterBindingFacts: TypeAlias = ( + AdapterBindingReadFacts + | AdapterBindingCreateFacts + | AdapterBindingSuspendFacts + | AdapterBindingResumeFacts +) + +_FACT_TYPE_BY_ACTION = { + "compensation.adapter_binding.read": AdapterBindingReadFacts, + "compensation.adapter_binding.create": AdapterBindingCreateFacts, + "compensation.adapter_binding.suspend": AdapterBindingSuspendFacts, + "compensation.adapter_binding.resume": AdapterBindingResumeFacts, +} + + +def adapter_binding_resource_digest(action_id: ActionId, facts: AdapterBindingFacts) -> str: + """Hash one exact action and its immutable adapter-binding facts.""" + action = str(action_id) + expected_type = _FACT_TYPE_BY_ACTION.get(action) + if expected_type is None or type(facts) is not expected_type: + raise ValueError("adapter-binding action does not match resource facts") + canonical = json.dumps( + { + "domain": "workstream.authorization.adapter_binding.v1", + "action_id": action, + "resource_type": "compensation_adapter_binding", + "facts": { + key: str(value) if isinstance(value, UUID) else value + for key, value in asdict(facts).items() + }, + }, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + ).encode() + return "sha256:" + hashlib.sha256(canonical).hexdigest() diff --git a/backend/app/modules/authorization/catalogue.py b/backend/app/modules/authorization/catalogue.py index ae3528865..15c936151 100644 --- a/backend/app/modules/authorization/catalogue.py +++ b/backend/app/modules/authorization/catalogue.py @@ -202,6 +202,10 @@ class ActionId(StrEnum): ARTIFACT_CHECKER_OUTPUT_WRITE = "artifact.checker_output.write" ARTIFACT_REVIEW_PACKET_MATERIALIZE = "artifact.review_packet.materialize" ARTIFACT_REVIEW_EVIDENCE_BINDING_CREATE = "artifact.review_evidence.binding.create" + COMPENSATION_ADAPTER_BINDING_READ = "compensation.adapter_binding.read" + COMPENSATION_ADAPTER_BINDING_CREATE = "compensation.adapter_binding.create" + COMPENSATION_ADAPTER_BINDING_SUSPEND = "compensation.adapter_binding.suspend" + COMPENSATION_ADAPTER_BINDING_RESUME = "compensation.adapter_binding.resume" @unique @@ -251,6 +255,7 @@ class ActionOwner(StrEnum): XINT_002_07 = "WS-XINT-002-07" XINT_003_08A = "WS-XINT-003-08A" XINT_003_08B = "WS-XINT-003-08B" + ARCH_CP01A = "WS-ARCH-001-CP01A" @unique @@ -764,6 +769,26 @@ def _active( PermissionId.ARTIFACT_BINDING_CREATE, ActionOwner.XINT_002_07, ), + _planned( + ActionId.COMPENSATION_ADAPTER_BINDING_READ, + PermissionId.COMPENSATION_ADAPTER_BINDING_MANAGE, + ActionOwner.ARCH_CP01A, + ), + _planned( + ActionId.COMPENSATION_ADAPTER_BINDING_CREATE, + PermissionId.COMPENSATION_ADAPTER_BINDING_MANAGE, + ActionOwner.ARCH_CP01A, + ), + _planned( + ActionId.COMPENSATION_ADAPTER_BINDING_SUSPEND, + PermissionId.COMPENSATION_ADAPTER_BINDING_MANAGE, + ActionOwner.ARCH_CP01A, + ), + _planned( + ActionId.COMPENSATION_ADAPTER_BINDING_RESUME, + PermissionId.COMPENSATION_ADAPTER_BINDING_MANAGE, + ActionOwner.ARCH_CP01A, + ), ) PERMISSION_IDS = frozenset(PermissionId) @@ -818,7 +843,7 @@ def _index_actions( ): raise RuntimeError("authorization action catalogue contains an invalid row") indexed = {definition.action_id: definition for definition in definitions} - if len(PERMISSION_IDS) != 73 or len(ACTION_IDS) != 102: + if len(PERMISSION_IDS) != 73 or len(ACTION_IDS) != 106: raise RuntimeError("authorization catalogue count mismatch") if len(indexed) != len(definitions) or set(indexed) != ACTION_IDS: raise RuntimeError("authorization action catalogue is incomplete") diff --git a/backend/tests/authorization/test_adapter_binding_registration.py b/backend/tests/authorization/test_adapter_binding_registration.py new file mode 100644 index 000000000..a993eb095 --- /dev/null +++ b/backend/tests/authorization/test_adapter_binding_registration.py @@ -0,0 +1,109 @@ +"""CP01A proof for planned adapter-binding AUTH registration.""" + +from __future__ import annotations + +from dataclasses import FrozenInstanceError +from uuid import uuid4 + +import pytest + +from app.modules.authorization.api import ( + AdapterBindingCreateFacts, + AdapterBindingReadFacts, + AdapterBindingResumeFacts, + AdapterBindingSuspendFacts, + action_id, + adapter_binding_resource_digest, +) +from app.modules.authorization.catalogue import ( + ACTION_BY_ID, + SERVICE_ACTIONS_BY_IDENTITY, + ActionAvailability, + ActionId, + ActionOwner, + PermissionId, + resolve_executable_action, +) + +_ACTIONS = { + ActionId.COMPENSATION_ADAPTER_BINDING_READ, + ActionId.COMPENSATION_ADAPTER_BINDING_CREATE, + ActionId.COMPENSATION_ADAPTER_BINDING_SUSPEND, + ActionId.COMPENSATION_ADAPTER_BINDING_RESUME, +} + + +def test_cp01a_registers_only_exact_planned_binding_actions() -> None: + for action in _ACTIONS: + definition = ACTION_BY_ID[action] + assert definition.permission_id is PermissionId.COMPENSATION_ADAPTER_BINDING_MANAGE + assert definition.owner is ActionOwner.ARCH_CP01A + assert definition.availability is ActionAvailability.PLANNED + with pytest.raises(ValueError, match="authorization action is not active"): + resolve_executable_action(action) + + assert "compensation.adapter_binding.retire" not in {item.value for item in ActionId} + assert all(_ACTIONS.isdisjoint(actions) for actions in SERVICE_ACTIONS_BY_IDENTITY.values()) + + +def test_cp01a_public_facts_are_immutable_and_validate_lifecycle_state() -> None: + project_id = uuid4() + binding_id = uuid4() + read = AdapterBindingReadFacts(project_id=project_id, adapter_binding_id=binding_id) + with pytest.raises(FrozenInstanceError): + read.project_id = uuid4() # type: ignore[misc] + with pytest.raises(ValueError, match="suspension requires active"): + AdapterBindingSuspendFacts( + project_id=project_id, + adapter_binding_id=binding_id, + expected_status="suspended", + ) + with pytest.raises(ValueError, match="resumption requires suspended"): + AdapterBindingResumeFacts( + project_id=project_id, + adapter_binding_id=binding_id, + expected_status="active", + ) + with pytest.raises(ValueError, match="route_key must be"): + AdapterBindingCreateFacts( + project_id=project_id, + instrument="money", + unit="USD", + adapter_actor_id=uuid4(), + route_key="secret route with spaces", + ) + + +def test_cp01a_digest_is_deterministic_and_action_domain_separated() -> None: + project_id = uuid4() + binding_id = uuid4() + read = AdapterBindingReadFacts(project_id=project_id, adapter_binding_id=binding_id) + read_action = action_id("compensation.adapter_binding.read") + first = adapter_binding_resource_digest(read_action, read) + assert first == adapter_binding_resource_digest(read_action, read) + assert first.startswith("sha256:") and len(first) == 71 + + suspend = AdapterBindingSuspendFacts( + project_id=project_id, + adapter_binding_id=binding_id, + ) + suspend_digest = adapter_binding_resource_digest( + action_id("compensation.adapter_binding.suspend"), suspend + ) + assert suspend_digest != first + with pytest.raises(ValueError, match="does not match"): + adapter_binding_resource_digest(read_action, suspend) + + +def test_cp01a_public_api_exports_exact_registration_surface() -> None: + import app.modules.authorization.api as authorization_api + + expected = { + "AdapterBindingCreateFacts", + "AdapterBindingReadFacts", + "AdapterBindingResumeFacts", + "AdapterBindingSuspendFacts", + "adapter_binding_resource_digest", + } + assert expected <= set(authorization_api.__all__) + assert all(hasattr(authorization_api, name) for name in expected) diff --git a/backend/tests/test_authorization.py b/backend/tests/test_authorization.py index 14803237d..c7ef27aea 100644 --- a/backend/tests/test_authorization.py +++ b/backend/tests/test_authorization.py @@ -2085,11 +2085,27 @@ def test_closed_permission_and_action_catalogue_is_exact_and_non_executable() -> ), "project.setup_run.update": ("project.guide.manage", "WS-AUTH-001-12B2"), "project.guide.activate": ("project.guide.manage", "WS-AUTH-001-12H"), + "compensation.adapter_binding.read": ( + "compensation.adapter_binding.manage", + "WS-ARCH-001-CP01A", + ), + "compensation.adapter_binding.create": ( + "compensation.adapter_binding.manage", + "WS-ARCH-001-CP01A", + ), + "compensation.adapter_binding.suspend": ( + "compensation.adapter_binding.manage", + "WS-ARCH-001-CP01A", + ), + "compensation.adapter_binding.resume": ( + "compensation.adapter_binding.manage", + "WS-ARCH-001-CP01A", + ), } assert {item.value for item in HISTORICAL_PERMISSION_IDS} == historical_permissions assert {item.value for item in NEW_PERMISSION_IDS} == new_permissions assert {item.value for item in PERMISSION_IDS} == historical_permissions | new_permissions - assert len(ACTION_IDS) == len(ACTION_DEFINITIONS) == len(ACTION_BY_ID) == 102 + assert len(ACTION_IDS) == len(ACTION_DEFINITIONS) == len(ACTION_BY_ID) == 106 assert set(ACTION_BY_ID) == ACTION_IDS assert {definition.owner for definition in ACTION_DEFINITIONS} == set(ActionOwner) assert { @@ -2234,7 +2250,7 @@ def test_closed_permission_and_action_catalogue_is_exact_and_non_executable() -> sum( definition.availability is ActionAvailability.PLANNED for definition in ACTION_DEFINITIONS - ) == 45 + ) == 49 ) assert resolve_executable_action(ActionId.ACTOR_PROFILE_READ_SELF).permission_id is PermissionId.ACTOR_PROFILE_READ_SELF with pytest.raises(ValueError, match="not active"): @@ -2900,7 +2916,7 @@ def test_art_custody_documentation_matches_the_independent_activation_fixture() assert "does not grant Operator" in operations assert "verification retry remains independently gated" in operations assert ( - "73 PermissionIds, 102 ActionIds, 57 active actions, and\n45 planned actions" in operations + "73 PermissionIds, 106 ActionIds, 57 active actions, and\n49 planned actions" in operations ) def test_rev_custody_documentation_matches_the_independent_catalogue_fixture() -> None: diff --git a/docs/operations_authorization_service.md b/docs/operations_authorization_service.md index f79615b7c..3e6b7b061 100644 --- a/docs/operations_authorization_service.md +++ b/docs/operations_authorization_service.md @@ -494,8 +494,11 @@ v0.1 baseline. The REV transfer adds no migration. The ART transfer does not grant Operator authority; its `OPERATOR` suffix denotes only future activation custody, and verification retry remains independently gated from read/status actions. -Catalogue totals are 73 PermissionIds, 102 ActionIds, 57 active actions, and -45 planned actions. AUTH-12I adds and activates only the unified compilation +Catalogue totals are 73 PermissionIds, 106 ActionIds, 57 active actions, and +49 planned actions. CP01A adds four unavailable adapter-binding actions under +`WS-ARCH-001-CP01A` custody; it adds no evaluator, identity, grant, service +matrix row, route, or activation. CP01B is the next registration gate. +AUTH-12I adds and activates only the unified compilation request/execute pair. AUTH-11C2 activates three current effective-policy and active-guide reads in addition to AUTH-11C1's six diagnostic reads. The exact route mapping is in `docs/spec_authorization_service.md`. WS-XINT-002-04A diff --git a/docs/roadmap_status.md b/docs/roadmap_status.md index 942362a56..1d685907e 100644 --- a/docs/roadmap_status.md +++ b/docs/roadmap_status.md @@ -133,9 +133,10 @@ review. Their presence does not change the implemented-on-`main` list above. recovery, provider proof, and the later public cutover. Hidden durable admission, Submission creation, and final binding are already merged through WS-ARCH-001-02H. -2. Register exact adapter-binding authority through CP01A and exact - ContributionPolicy authority through CP01B while both remain unavailable; implement - and separately activate adapter-binding and ContributionPolicy behavior; +2. CP01A has registered exact adapter-binding authority while keeping all four + actions unavailable. Register exact ContributionPolicy authority through + CP01B while unavailable; then implement and separately activate + adapter-binding and ContributionPolicy behavior; expose CON validation; then bind one exact published, complete, binding-valid ContributionPolicyVersion at guide activation, and lock it on each task before that task becomes claimable. TaskAssignment diff --git a/docs/spec_authorization_service.md b/docs/spec_authorization_service.md index f4baa1502..90b4ec4d4 100644 --- a/docs/spec_authorization_service.md +++ b/docs/spec_authorization_service.md @@ -261,6 +261,10 @@ planned rows without adding an action or permission. WS-ARCH-001-02H activates the existing human `submission.create` and fixed-service `artifact.submission.binding.create` rows, producing 57 active and 45 planned rows without adding an action or permission. +WS-ARCH-001-CP01A then adds four planned/unavailable adapter-binding actions, +producing 106 rows: 57 active and 49 planned. They map only to +`compensation.adapter_binding.manage`; no retirement, evaluator, identity, +service-matrix row, route, or activation is added. CP01B remains next. AUTH-10A added five project-role read/manage rows; AUTH-10B owns and activates the three reads, while AUTH-10C owns and activates the two reason-bound, idempotent project-role mutations. AUTH-11A adds eleven diff --git a/docs/spec_contribution_compensation.md b/docs/spec_contribution_compensation.md index 405006a5e..01b6e7824 100644 --- a/docs/spec_contribution_compensation.md +++ b/docs/spec_contribution_compensation.md @@ -10,9 +10,10 @@ The files under `docs/reference_specs/` are immutable archival inputs. Older chunk specifications that describe the currently implemented guide-bound payment fields remain useful runtime history, but they do not control the target contribution or compensation architecture. The clean cut from those -fields is implemented only by the approved `WS-CON-001-05A` and -`WS-CON-001-05B` chunks; this specification does not claim that migration has -already occurred. +fields follows CP06/CP07/CP08, replacement activation through +WS-ARCH-001-03A/03B/03C, and CP09 removal. The former WS-CON-001-05A/05B path +is historical and non-executable; this specification does not claim that the +replacement or removal has already occurred. No route, model, action, service identity, or runtime behavior described here exists merely because it appears in this document. Each behavior remains @@ -753,8 +754,12 @@ AUTH-09E admission, evaluator integration, and exact action activation. ### Proposed surface mappings -The following 22 mappings are product proposals, not registered or executable -identifiers. Existing PermissionIds remain stable. Policy ActionIds use the +The table contains both registered-unavailable and future proposed mappings. +CP01A registers the four adapter-binding read/create/suspend/resume ActionIds +under `WS-ARCH-001-CP01A`; they remain unavailable and have no evaluator, +identity, service-matrix row, route, or activation. All other rows remain +proposals, including dependency-aware adapter-binding retirement. Existing +PermissionIds remain stable. Policy ActionIds use the canonical `contribution.policy.*` namespace while mapping to the existing `compensation.policy.manage` PermissionId. AUTH must approve exact identifiers, principals, contexts, mappings, custodians, and activation in its own chunks. @@ -762,10 +767,10 @@ principals, contexts, mappings, custodians, and activation in its own chunks. | Proposed ActionId | PermissionId | Principal / target | Protocol | Feature owner | |---|---|---|---:|---| | `outbox.dispatch` | proposed `outbox.dispatch` | fixed dispatcher / claimed event | T | CON-02B | -| `compensation.adapter_binding.read` | `compensation.adapter_binding.manage` | Finance / binding | Q | CON-04A | -| `compensation.adapter_binding.create` | `compensation.adapter_binding.manage` | Finance / binding collection | T | CON-04A | -| `compensation.adapter_binding.suspend` | `compensation.adapter_binding.manage` | Finance / active binding | T | CON-04A | -| `compensation.adapter_binding.resume` | `compensation.adapter_binding.manage` | Finance / suspended binding | T | CON-04A | +| `compensation.adapter_binding.read` | `compensation.adapter_binding.manage` | Finance / binding | Q | WS-ARCH-001-CP01A (registered, unavailable) | +| `compensation.adapter_binding.create` | `compensation.adapter_binding.manage` | Finance / binding collection | T | WS-ARCH-001-CP01A (registered, unavailable) | +| `compensation.adapter_binding.suspend` | `compensation.adapter_binding.manage` | Finance / active binding | T | WS-ARCH-001-CP01A (registered, unavailable) | +| `compensation.adapter_binding.resume` | `compensation.adapter_binding.manage` | Finance / suspended binding | T | WS-ARCH-001-CP01A (registered, unavailable) | | `compensation.adapter_binding.retire` | `compensation.adapter_binding.manage` | Finance / dependency-free binding | T | CON-10B | | `contribution.policy.read` | `compensation.policy.manage` | Finance / policy version | Q | CON-04B | | `contribution.policy.create_draft` | `compensation.policy.manage` | Finance / policy collection | T | CON-04B | @@ -793,8 +798,8 @@ AUTH must approve an exact ServiceIdentity/ActionId/static-row design or an explicitly closed dual-principal evaluator. Discovery candidate strings are not canonical catalogue identifiers and MUST NOT be registered by CON. -`task.claim` currently has a stable PermissionId but no ActionId. CON-05A and -task-owned readiness/inheritance composition must merge before AUTH registers +`task.claim` currently has a stable PermissionId but no ActionId. CP06-CP08 and +task-owned ARCH-03A/03B readiness/inheritance composition must merge before AUTH registers or activates that future action. `review.claim` and `review.decision` remain planned until the inherited task-policy lineage, CON-07, and complete REV composition merge. AUTH must transfer the complete @@ -918,27 +923,29 @@ Missing provisioned service rows keep readiness false but do not block startup or administrative provisioning. Optional evidence and ART are not readiness dependencies. -## Legacy Economic Schema Cutover +## Retired Economic Schema Cutover `ContributionPolicy` is the sole target award-eligibility policy. The existing guide-bound economic-policy aggregate, its locked version fields, copied task economic terms, and every semantic consumer are retired. -The clean cut is mandatory and split: +The clean cut is mandatory and owned by the current sequence: -1. `WS-CON-001-05A` introduces assignment freezing, removes all semantic - consumers of the legacy model, migrates or deterministically rebuilds only - the explicitly classified pre-production data, and proves zero fallback. -2. `WS-CON-001-05B` proves zero remaining consumers and physically removes the - dead schema with reviewed upgrade and downgrade behavior. +1. CP06 validates ContributionPolicy state, CP07 binds the guide, and CP08 + installs TASK-owned attempt-lineage persistence. +2. WS-ARCH-001-03A/03B implements the replacement readiness/claim path and + WS-ARCH-001-03C activates it. +3. CP09 proves zero remaining semantic consumers and removes the retired + schema from the consolidated v0.1 baseline. No alias, dual read, dual write, automatic conversion, compatibility response, or historical executable fallback may remain. Until those chunks merge, old runtime-oriented chunk specifications describe current implementation only and are subordinate to this target contract. -The human-owned legacy-row classification MUST be recorded before CON-05A -starts. Ambiguous rows cause the migration to fail closed. +No legacy-row classification, data conversion, or compatibility migration is +required for the consolidated v0.1 baseline. Discovery must stop and require a +fresh reviewed migration contract if real deployed data is found. ## Human And AUTH Gates @@ -948,8 +955,9 @@ answers: 1. D11 must select the exact AdminRole candidates for project award detail, delivery recovery, and CON audit read/export before CON-10A/10B or related AUTH registration starts. -2. The human must classify all pre-production legacy economic rows for - deterministic rebuild or explicit migration before CON-05A/05B. +2. The consolidated v0.1 baseline requires no legacy-row classification or + compatibility migration; CP09 removes the retired economic path only after + its replacement is active. 3. The human and AUTH must approve exact fixed-service identity, action, static row, context, and evaluator contracts for dispatcher mechanics, outbound delivery, reconciliation, projection rebuild, and callback execution before @@ -969,12 +977,13 @@ CON-01 -> CON-02A CON-03A -> CON-03B -> REV lease/policy-freeze persistence CON-02C -> REV Review/FinalAcceptance transaction foundation REV-04B runtime Review/ReviewLease/FinalAcceptance -> CON-03C -> CON-03D -CON-03A -> CON-04A -CON-03B + CON-04A -> CON-04B -> CON-05A -> CON-05B -stable REV revision lineage + CON-03C/03D + CON-05A -> CON-07 -> REV-10 +WS-ARCH-001-CP01A -> CP01B -> CP02 -> CP03 -> CP04 -> CP05 +CP05 -> CP06 -> CP07 -> CP08 +CP08 -> WS-ARCH-001-03A/03B -> WS-ARCH-001-03C -> CP09 +stable REV revision lineage + CON-03C/03D + CP06/CP08 -> CON-07 -> REV-10 AUTH dispatcher contract -> CON-02B -CON-02B + CON-03D + CON-04A/B + CON-07 -> CON-08A -> CON-08R -> CON-08B -CON-08B -> CON-10A -> CON-10B +CON-02B + CON-03D + CP02/CP04 + CON-07 -> fresh delivery contracts +fresh delivery contracts -> CON-10A -> CON-10B CON-02B + CON-10B -> CON-10C all required hidden behavior and cross-initiative gates -> CON-11 ``` @@ -987,8 +996,9 @@ not a prerequisite for CON-02C, CON-03A, or CON-03B. Cross-initiative interleaving is mandatory: ```text -CON-05A guide-activation validation/persistence contract - -> PROJECT guide binding -> TASK readiness lock -> assignment inheritance +CP06 validation -> CP07 PROJECT guide binding -> CP08 TASK lineage persistence + -> ARCH-03A/03B readiness and assignment inheritance -> ARCH-03C activation + -> CP09 retired-path removal CON-03B ContributionPolicyVersion persistence -> REV-03 ReviewLease foreign key From 9d2c84c86665664b23d2a13d6d7e27a7ed0b37b6 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Fri, 14 Aug 2026 00:33:43 +0100 Subject: [PATCH 2/8] fix(auth): reconcile CP01A verification evidence --- ...CP01A-auth-adapter-binding-registration.md | 1 + .../TEST_STRUCTURE_DEBT.json | 80 +++++++++---------- backend/tests/test_authorization.py | 41 +++------- docs/spec_contribution_compensation.md | 4 +- 4 files changed, 55 insertions(+), 71 deletions(-) diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md index 3840d915d..731eb5c70 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md @@ -45,6 +45,7 @@ docs/spec_authorization_service.md (catalogue parity only) docs/spec_contribution_compensation.md (CP01A registration parity only) .agent-loop/initiatives/WS-CON-001-contribution-compensation-boundary/STATUS.md (CP01A state only) docs/roadmap_status.md (CP01A capability state only) +.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json (generated parity only) .agent-loop/CURRENT_STATE.md .agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/{CHUNK_MAP.md,STATUS.md} .agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md diff --git a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json index 65ad448a3..93a6676d5 100644 --- a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json +++ b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json @@ -39,14 +39,14 @@ { "capability": "unassigned_legacy_auth", "content_sha256": "0a1e990ef039786066276283bc519669a9274aceb2949e1d320f93e75203926e", - "end_line": 1124, + "end_line": 1149, "hard_limit": 100, "kind": "production_function", "observed_lines": 120, "path": "backend/app/modules/authorization/catalogue.py", "qualified_symbol": "_index_service_actions", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1005 + "start_line": 1030 }, { "capability": "unassigned_legacy_auth", @@ -218,11 +218,11 @@ }, { "capability": "unassigned_legacy_auth", - "content_sha256": "b208f0ad84feb415e88445c96cd7341b09432b04e39b23bddef0c772221640bf", - "end_line": 13407, + "content_sha256": "dd8d424d97a68ff9080aa05c16feb073f4f219eb63e0d5dd55defdfad00d28b9", + "end_line": 13406, "hard_limit": 1200, "kind": "test_file", - "observed_lines": 13407, + "observed_lines": 13406, "path": "backend/tests/test_authorization.py", "qualified_symbol": null, "removal_chunk": "WS-AUTH-003-CLOSE", @@ -531,58 +531,58 @@ { "capability": "unassigned_legacy_auth", "content_sha256": "951b44cc07e36002118fe93b7974e8d65851b0e2c3cec89a3031cbe42b014e2d", - "end_line": 8513, + "end_line": 8512, "hard_limit": 120, "kind": "test_function", "observed_lines": 140, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_actor_lifecycle_service_applies_success_and_guards_conflicts", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 8374 + "start_line": 8373 }, { "capability": "unassigned_legacy_auth", "content_sha256": "569084d6de89ff9eae71d526fc6c157aea7638f55ca93ad128b721fbda339be6", - "end_line": 9003, + "end_line": 9002, "hard_limit": 120, "kind": "test_function", "observed_lines": 122, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_admin_resource_digest_alone_rejects_substituted_role_and_disposition", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 8882 + "start_line": 8881 }, { "capability": "unassigned_legacy_auth", "content_sha256": "90b8b670b4d277209617cc1aa794188b4fb3cd9d894b69d5dc3d0adfc885f6ed", - "end_line": 9231, + "end_line": 9230, "hard_limit": 120, "kind": "test_function", "observed_lines": 132, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_admin_revoke_stages_complete_state_and_evidence", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 9100 + "start_line": 9099 }, { "capability": "unassigned_legacy_auth", "content_sha256": "78d9bf3df08e5633e9a75720b1e4bf5b7d7b24019bc392b4cb7496a9ac5e5e8e", - "end_line": 11059, + "end_line": 11058, "hard_limit": 120, "kind": "test_function", "observed_lines": 122, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_authorization_locks_refresh_cached_actor_lifecycle_state", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 10938 + "start_line": 10937 }, { "capability": "unassigned_legacy_auth", - "content_sha256": "e4d94ac97842b84c9b64d46a0a86801294a90eeca7b074de5b281c8014a8fcb5", - "end_line": 2243, + "content_sha256": "9f874fde0ed7cfe54e4cd9c3fc75d18305ec48edfb6b8696e305116b3828afea", + "end_line": 2242, "hard_limit": 120, "kind": "test_function", - "observed_lines": 322, + "observed_lines": 321, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_closed_permission_and_action_catalogue_is_exact_and_non_executable", "removal_chunk": "WS-AUTH-003-CLOSE", @@ -591,122 +591,122 @@ { "capability": "unassigned_legacy_auth", "content_sha256": "df1df6ec2ba6aa55445e21cfcf4e4e3ad9664c9504313484a495b6a6df1e9047", - "end_line": 3473, + "end_line": 3472, "hard_limit": 120, "kind": "test_function", "observed_lines": 126, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_identity_link_lifecycle_route_preserves_outcome_transaction_contract", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 3348 + "start_line": 3347 }, { "capability": "unassigned_legacy_auth", "content_sha256": "05d1b020ecff0f9bc0a0567adc07f5b31a2f9dfb7828ae3ad34d4e1e7757797c", - "end_line": 8666, + "end_line": 8665, "hard_limit": 120, "kind": "test_function", "observed_lines": 151, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_identity_link_lifecycle_service_applies_success_and_guards_conflicts", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 8516 + "start_line": 8515 }, { "capability": "unassigned_legacy_auth", "content_sha256": "c5d6d0d480ced964354915614f15202c0159bfbb281658da0d44186dffd17848", - "end_line": 7461, + "end_line": 7460, "hard_limit": 120, "kind": "test_function", "observed_lines": 142, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_prepared_actor_authority_crossed_mutations_complete_in_both_orders", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 7320 + "start_line": 7319 }, { "capability": "unassigned_legacy_auth", "content_sha256": "ebb76e63671195aa4d806ac602bfe58cb22bf82d00c8a70ab186785f3acd7f9b", - "end_line": 7802, + "end_line": 7801, "hard_limit": 120, "kind": "test_function", "observed_lines": 336, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_prepared_crosses_real_lifecycle_service_transactions", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 7467 + "start_line": 7466 }, { "capability": "unassigned_legacy_auth", "content_sha256": "9ea4fc0ddbab4c7262a43bc3f498ee1afea3318e9a0b6c93c87763f9f22aae9c", - "end_line": 7292, + "end_line": 7291, "hard_limit": 120, "kind": "test_function", "observed_lines": 518, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_prepared_postgresql_failure_and_cancellation_are_atomic", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 6775 + "start_line": 6774 }, { "capability": "unassigned_legacy_auth", "content_sha256": "46e0b031394da6fee856e48615732a17ff8d2276d9cedfb0ecaa3a6879410adb", - "end_line": 4678, + "end_line": 4677, "hard_limit": 120, "kind": "test_function", "observed_lines": 157, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_project_11c2_reads_require_exact_admin_context_and_role_allowlist", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 4522 + "start_line": 4521 }, { "capability": "unassigned_legacy_auth", "content_sha256": "e95d2a03e3829df89c43210a9de92efca31a25905c9648c3179d17b078386b17", - "end_line": 2644, + "end_line": 2643, "hard_limit": 120, "kind": "test_function", "observed_lines": 397, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_project_mutation_resources_and_prepared_scopes_are_closed", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 2248 + "start_line": 2247 }, { "capability": "unassigned_legacy_auth", "content_sha256": "105667302ed6e8f2fd16ea7e95d642e72e41514673e1152503536e0520f9c362", - "end_line": 10934, + "end_line": 10933, "hard_limit": 120, "kind": "test_function", "observed_lines": 204, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_project_read_permissions_have_postgresql_role_scope_matrix", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 10731 + "start_line": 10730 }, { "capability": "unassigned_legacy_auth", "content_sha256": "756b7f99f9a743284934b4d85ce263617710d9b8119792ccb4526a1de04070a1", - "end_line": 12436, + "end_line": 12435, "hard_limit": 120, "kind": "test_function", "observed_lines": 233, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_project_role_and_all_operation_mappings_commit_one_linked_pair", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 12204 + "start_line": 12203 }, { "capability": "unassigned_legacy_auth", "content_sha256": "2e1d7db74ddb955b90a0ee12e4fb72b651a0f85d2746c76e09079c05b0b85252", - "end_line": 13407, + "end_line": 13406, "hard_limit": 120, "kind": "test_function", "observed_lines": 681, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_project_role_issue_postgresql_prep_binds_target_role_and_scope", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 12727 + "start_line": 12726 }, { "capability": "unassigned_legacy_auth", @@ -735,14 +735,14 @@ { "capability": "unassigned_legacy_auth", "content_sha256": "05621e885ed2f88d0ba1a072c1f263fc923939f7ce755a514e9872112aa830a1", - "end_line": 11953, + "end_line": 11952, "hard_limit": 120, "kind": "test_function", "observed_lines": 163, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_service_actor_replay_fails_closed_on_committed_state_drift", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 11791 + "start_line": 11790 }, { "capability": "unassigned_legacy_auth", @@ -1023,14 +1023,14 @@ { "capability": "unassigned_legacy_auth", "content_sha256": "1a0a9f3e2be6965e29f76aa74272ccfa2fe4b99e4e0c3bde5ec8ba2c374b369b", - "end_line": 11267, + "end_line": 11266, "hard_limit": 100, "kind": "test_helper", "observed_lines": 169, "path": "backend/tests/test_authorization.py", "qualified_symbol": "_operation_success", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 11099 + "start_line": 11098 }, { "capability": "unassigned_legacy_auth", diff --git a/backend/tests/test_authorization.py b/backend/tests/test_authorization.py index c7ef27aea..c760c6881 100644 --- a/backend/tests/test_authorization.py +++ b/backend/tests/test_authorization.py @@ -2083,24 +2083,8 @@ def test_closed_permission_and_action_catalogue_is_exact_and_non_executable() -> "project.effective_policy.manage", "WS-AUTH-001-12G", ), - "project.setup_run.update": ("project.guide.manage", "WS-AUTH-001-12B2"), - "project.guide.activate": ("project.guide.manage", "WS-AUTH-001-12H"), - "compensation.adapter_binding.read": ( - "compensation.adapter_binding.manage", - "WS-ARCH-001-CP01A", - ), - "compensation.adapter_binding.create": ( - "compensation.adapter_binding.manage", - "WS-ARCH-001-CP01A", - ), - "compensation.adapter_binding.suspend": ( - "compensation.adapter_binding.manage", - "WS-ARCH-001-CP01A", - ), - "compensation.adapter_binding.resume": ( - "compensation.adapter_binding.manage", - "WS-ARCH-001-CP01A", - ), + "project.setup_run.update": ("project.guide.manage", "WS-AUTH-001-12B2"), "project.guide.activate": ("project.guide.manage", "WS-AUTH-001-12H"), # noqa: E501 + **dict.fromkeys((f"compensation.adapter_binding.{op}" for op in ("read", "create", "suspend", "resume")), ("compensation.adapter_binding.manage", "WS-ARCH-001-CP01A")), # noqa: E501 } assert {item.value for item in HISTORICAL_PERMISSION_IDS} == historical_permissions assert {item.value for item in NEW_PERMISSION_IDS} == new_permissions @@ -2240,18 +2224,17 @@ def test_closed_permission_and_action_catalogue_is_exact_and_non_executable() -> ActionOwner.XINT_003_08B: 1, } assert all(not owner.value.startswith("WS-REV-") for owner in ActionOwner) - assert ( - sum( - definition.availability is ActionAvailability.ACTIVE - for definition in ACTION_DEFINITIONS - ) == 57 - ) - assert ( - sum( - definition.availability is ActionAvailability.PLANNED + availability_counts = { + availability: sum( + definition.availability is availability for definition in ACTION_DEFINITIONS - ) == 49 - ) + ) + for availability in ActionAvailability + } + assert availability_counts == { + ActionAvailability.ACTIVE: 57, + ActionAvailability.PLANNED: 49, + } assert resolve_executable_action(ActionId.ACTOR_PROFILE_READ_SELF).permission_id is PermissionId.ACTOR_PROFILE_READ_SELF with pytest.raises(ValueError, match="not active"): resolve_executable_action(ActionId.REVIEW_QUEUE_READ) diff --git a/docs/spec_contribution_compensation.md b/docs/spec_contribution_compensation.md index 01b6e7824..47771e9d1 100644 --- a/docs/spec_contribution_compensation.md +++ b/docs/spec_contribution_compensation.md @@ -752,7 +752,7 @@ or rebuild identity therefore still requires its own human-approved feature manifest followed by AUTH-owned identity/matrix registration, provisioning, AUTH-09E admission, evaluator integration, and exact action activation. -### Proposed surface mappings +### Surface mappings The table contains both registered-unavailable and future proposed mappings. CP01A registers the four adapter-binding read/create/suspend/resume ActionIds @@ -764,7 +764,7 @@ canonical `contribution.policy.*` namespace while mapping to the existing `compensation.policy.manage` PermissionId. AUTH must approve exact identifiers, principals, contexts, mappings, custodians, and activation in its own chunks. -| Proposed ActionId | PermissionId | Principal / target | Protocol | Feature owner | +| ActionId | PermissionId | Principal / target | Protocol | Feature owner | |---|---|---|---:|---| | `outbox.dispatch` | proposed `outbox.dispatch` | fixed dispatcher / claimed event | T | CON-02B | | `compensation.adapter_binding.read` | `compensation.adapter_binding.manage` | Finance / binding | Q | WS-ARCH-001-CP01A (registered, unavailable) | From 4051a90bfc7b92e4c4766ec0aab842850fcaec14 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Fri, 14 Aug 2026 00:38:12 +0100 Subject: [PATCH 3/8] fix(ci): register CP01A behavior ownership --- .../WS-ARCH-001-CP01A-auth-adapter-binding-registration.md | 3 +++ .ci/behavior-ownership/partition.v1.json | 6 +++++- backend/scripts/behavior_ownership.py | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md index 731eb5c70..97bebc244 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md @@ -37,6 +37,9 @@ backend/app/modules/authorization/catalogue.py backend/app/modules/authorization/api/__init__.py backend/app/modules/authorization/api/action_ids.py backend/app/modules/authorization/api/adapter_bindings.py +.ci/behavior-ownership/partition.v1.json (additive AUTH target parity only) +backend/scripts/behavior_ownership.py (exact CP01A public-API target only) +backend/tests/test_behavior_ownership.py (exact additive-transition parity only) backend/tests/authorization/test_adapter_binding_registration.py backend/tests/test_authorization.py (closed catalogue/action-owner parity only) docs/operations_authorization_service.md (catalogue parity only) diff --git a/.ci/behavior-ownership/partition.v1.json b/.ci/behavior-ownership/partition.v1.json index 800245d0c..b61751369 100644 --- a/.ci/behavior-ownership/partition.v1.json +++ b/.ci/behavior-ownership/partition.v1.json @@ -336,6 +336,10 @@ "group": "auth", "target": "backend/app/modules/authorization/api/action_ids.py" }, + { + "group": "auth", + "target": "backend/app/modules/authorization/api/adapter_bindings.py" + }, { "group": "auth", "target": "backend/app/modules/authorization/api/decisions.py" @@ -845,7 +849,7 @@ "target": "backend/scripts/week2_api_e2e.py" } ], - "authority_digest": "8ee0aa0c7ab30e28e3f7a665b66bcfd981d220197f783e5fb72a000bd2948cb4", + "authority_digest": "2bee80de4e315abab9e5821d572e37e22e986cb26ee33e78644ea5b97dad04fe", "protected_base_commit": "7676ce4347db0c9694962a9b587a20765e16eac6", "schema": "workstream.behavior-ownership-partition.v1" } diff --git a/backend/scripts/behavior_ownership.py b/backend/scripts/behavior_ownership.py index 126a95bf3..bf772fc44 100644 --- a/backend/scripts/behavior_ownership.py +++ b/backend/scripts/behavior_ownership.py @@ -62,6 +62,7 @@ ) AUTH_BOUNDARY_FOUNDATION_TARGETS = frozenset( { + "backend/app/modules/authorization/api/adapter_bindings.py", "backend/app/modules/authorization/api/action_ids.py", "backend/app/modules/authorization/api/decisions.py", "backend/app/modules/authorization/api/errors.py", From 7d5cdc5df6e2346eca5e467df04e8056827f7bab Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Fri, 14 Aug 2026 00:42:12 +0100 Subject: [PATCH 4/8] test(auth): bind CP01A behavior ownership --- ...CP01A-auth-adapter-binding-registration.md | 1 + .../auth/adapter-binding-facts.json | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 .ci/behavior-ownership/auth/adapter-binding-facts.json diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md index 97bebc244..de44e65a2 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md @@ -38,6 +38,7 @@ backend/app/modules/authorization/api/__init__.py backend/app/modules/authorization/api/action_ids.py backend/app/modules/authorization/api/adapter_bindings.py .ci/behavior-ownership/partition.v1.json (additive AUTH target parity only) +.ci/behavior-ownership/auth/adapter-binding-facts.json backend/scripts/behavior_ownership.py (exact CP01A public-API target only) backend/tests/test_behavior_ownership.py (exact additive-transition parity only) backend/tests/authorization/test_adapter_binding_registration.py diff --git a/.ci/behavior-ownership/auth/adapter-binding-facts.json b/.ci/behavior-ownership/auth/adapter-binding-facts.json new file mode 100644 index 000000000..f2becd85b --- /dev/null +++ b/.ci/behavior-ownership/auth/adapter-binding-facts.json @@ -0,0 +1,28 @@ +{ + "behavior_id": "auth.adapter_binding.facts", + "boundaries": [], + "callables": [ + "app.modules.authorization.api.adapter_bindings.AdapterBindingCreateFacts.__post_init__", + "app.modules.authorization.api.adapter_bindings.AdapterBindingReadFacts.__post_init__", + "app.modules.authorization.api.adapter_bindings.AdapterBindingResumeFacts.__post_init__", + "app.modules.authorization.api.adapter_bindings.AdapterBindingSuspendFacts.__post_init__", + "app.modules.authorization.api.adapter_bindings._require_token", + "app.modules.authorization.api.adapter_bindings._require_uuid", + "app.modules.authorization.api.adapter_bindings.adapter_binding_resource_digest" + ], + "group": "auth", + "outcomes": [ + "return", + "mapped_error" + ], + "reviewed_by": [ + "WS-ARCH-001-CP01A required reviewers" + ], + "schema": "workstream.behavior-ownership.v1", + "status": "reviewed", + "target": "backend/app/modules/authorization/api/adapter_bindings.py", + "tests": [ + "backend/tests/authorization/test_adapter_binding_registration.py::test_cp01a_public_facts_are_immutable_and_validate_lifecycle_state", + "backend/tests/authorization/test_adapter_binding_registration.py::test_cp01a_digest_is_deterministic_and_action_domain_separated" + ] +} From 15d9a97eb2fb02dc3e11d1c640c555581fd0e4b8 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Fri, 14 Aug 2026 00:46:55 +0100 Subject: [PATCH 5/8] docs(auth): document CP01A validators --- backend/app/modules/authorization/api/adapter_bindings.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/app/modules/authorization/api/adapter_bindings.py b/backend/app/modules/authorization/api/adapter_bindings.py index 9351e2a22..5b65fdd8b 100644 --- a/backend/app/modules/authorization/api/adapter_bindings.py +++ b/backend/app/modules/authorization/api/adapter_bindings.py @@ -15,11 +15,13 @@ def _require_token(name: str, value: str) -> None: + """Reject values outside the bounded canonical-token grammar.""" if not isinstance(value, str) or not _TOKEN.fullmatch(value): raise ValueError(f"{name} must be a bounded canonical token") def _require_uuid(name: str, value: UUID) -> None: + """Reject identifiers that are not already parsed UUID values.""" if not isinstance(value, UUID): raise ValueError(f"{name} must be a UUID") @@ -32,6 +34,7 @@ class AdapterBindingReadFacts: adapter_binding_id: UUID def __post_init__(self) -> None: + """Validate the exact project and binding identifiers.""" _require_uuid("project_id", self.project_id) _require_uuid("adapter_binding_id", self.adapter_binding_id) @@ -47,6 +50,7 @@ class AdapterBindingCreateFacts: route_key: str def __post_init__(self) -> None: + """Validate the binding creation identity and routing facts.""" _require_uuid("project_id", self.project_id) _require_uuid("adapter_actor_id", self.adapter_actor_id) _require_token("instrument", self.instrument) @@ -63,6 +67,7 @@ class AdapterBindingSuspendFacts: expected_status: str = "active" def __post_init__(self) -> None: + """Require an exact active binding as the suspension target.""" _require_uuid("project_id", self.project_id) _require_uuid("adapter_binding_id", self.adapter_binding_id) if self.expected_status != "active": @@ -78,6 +83,7 @@ class AdapterBindingResumeFacts: expected_status: str = "suspended" def __post_init__(self) -> None: + """Require an exact suspended binding as the resumption target.""" _require_uuid("project_id", self.project_id) _require_uuid("adapter_binding_id", self.adapter_binding_id) if self.expected_status != "suspended": From c61cc11fec6b622efae8ab4652e8fcaae0bf4dcd Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Fri, 14 Aug 2026 00:51:14 +0100 Subject: [PATCH 6/8] fix(ci): assign CP01A test lane custody --- .../WS-ARCH-001-CP01A-auth-adapter-binding-registration.md | 1 + backend/scripts/run_test_lanes.py | 1 + 2 files changed, 2 insertions(+) diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md index de44e65a2..f0e4799bb 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-CP01A-auth-adapter-binding-registration.md @@ -40,6 +40,7 @@ backend/app/modules/authorization/api/adapter_bindings.py .ci/behavior-ownership/partition.v1.json (additive AUTH target parity only) .ci/behavior-ownership/auth/adapter-binding-facts.json backend/scripts/behavior_ownership.py (exact CP01A public-API target only) +backend/scripts/run_test_lanes.py (CP01A test custody only) backend/tests/test_behavior_ownership.py (exact additive-transition parity only) backend/tests/authorization/test_adapter_binding_registration.py backend/tests/test_authorization.py (closed catalogue/action-owner parity only) diff --git a/backend/scripts/run_test_lanes.py b/backend/scripts/run_test_lanes.py index 94c2046c8..3bc28ef52 100644 --- a/backend/scripts/run_test_lanes.py +++ b/backend/scripts/run_test_lanes.py @@ -140,6 +140,7 @@ class TestLane: "tests/authorization/guide_compilation/test_domain_contract.py", "tests/authorization/guide_compilation/test_migration_contract.py", "tests/authorization/test_fixed_service_action_context.py", + "tests/authorization/test_adapter_binding_registration.py", "tests/test_behavior_ownership.py", "tests/test_artifact_admission.py", "tests/test_submission_bundle_admission.py", From cd2a40fff4ad2b2fbe6745314e42a372dd8c4e5b Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Fri, 14 Aug 2026 01:09:58 +0100 Subject: [PATCH 7/8] docs(con): narrow CP01A specification parity --- docs/spec_contribution_compensation.md | 52 ++++++++++++-------------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/docs/spec_contribution_compensation.md b/docs/spec_contribution_compensation.md index 47771e9d1..24a66be6a 100644 --- a/docs/spec_contribution_compensation.md +++ b/docs/spec_contribution_compensation.md @@ -10,10 +10,9 @@ The files under `docs/reference_specs/` are immutable archival inputs. Older chunk specifications that describe the currently implemented guide-bound payment fields remain useful runtime history, but they do not control the target contribution or compensation architecture. The clean cut from those -fields follows CP06/CP07/CP08, replacement activation through -WS-ARCH-001-03A/03B/03C, and CP09 removal. The former WS-CON-001-05A/05B path -is historical and non-executable; this specification does not claim that the -replacement or removal has already occurred. +fields is implemented only by the approved `WS-CON-001-05A` and +`WS-CON-001-05B` chunks; this specification does not claim that migration has +already occurred. No route, model, action, service identity, or runtime behavior described here exists merely because it appears in this document. Each behavior remains @@ -798,8 +797,8 @@ AUTH must approve an exact ServiceIdentity/ActionId/static-row design or an explicitly closed dual-principal evaluator. Discovery candidate strings are not canonical catalogue identifiers and MUST NOT be registered by CON. -`task.claim` currently has a stable PermissionId but no ActionId. CP06-CP08 and -task-owned ARCH-03A/03B readiness/inheritance composition must merge before AUTH registers +`task.claim` currently has a stable PermissionId but no ActionId. CON-05A and +task-owned readiness/inheritance composition must merge before AUTH registers or activates that future action. `review.claim` and `review.decision` remain planned until the inherited task-policy lineage, CON-07, and complete REV composition merge. AUTH must transfer the complete @@ -923,29 +922,27 @@ Missing provisioned service rows keep readiness false but do not block startup or administrative provisioning. Optional evidence and ART are not readiness dependencies. -## Retired Economic Schema Cutover +## Legacy Economic Schema Cutover `ContributionPolicy` is the sole target award-eligibility policy. The existing guide-bound economic-policy aggregate, its locked version fields, copied task economic terms, and every semantic consumer are retired. -The clean cut is mandatory and owned by the current sequence: +The clean cut is mandatory and split: -1. CP06 validates ContributionPolicy state, CP07 binds the guide, and CP08 - installs TASK-owned attempt-lineage persistence. -2. WS-ARCH-001-03A/03B implements the replacement readiness/claim path and - WS-ARCH-001-03C activates it. -3. CP09 proves zero remaining semantic consumers and removes the retired - schema from the consolidated v0.1 baseline. +1. `WS-CON-001-05A` introduces assignment freezing, removes all semantic + consumers of the legacy model, migrates or deterministically rebuilds only + the explicitly classified pre-production data, and proves zero fallback. +2. `WS-CON-001-05B` proves zero remaining consumers and physically removes the + dead schema with reviewed upgrade and downgrade behavior. No alias, dual read, dual write, automatic conversion, compatibility response, or historical executable fallback may remain. Until those chunks merge, old runtime-oriented chunk specifications describe current implementation only and are subordinate to this target contract. -No legacy-row classification, data conversion, or compatibility migration is -required for the consolidated v0.1 baseline. Discovery must stop and require a -fresh reviewed migration contract if real deployed data is found. +The human-owned legacy-row classification MUST be recorded before CON-05A +starts. Ambiguous rows cause the migration to fail closed. ## Human And AUTH Gates @@ -955,9 +952,8 @@ answers: 1. D11 must select the exact AdminRole candidates for project award detail, delivery recovery, and CON audit read/export before CON-10A/10B or related AUTH registration starts. -2. The consolidated v0.1 baseline requires no legacy-row classification or - compatibility migration; CP09 removes the retired economic path only after - its replacement is active. +2. The human must classify all pre-production legacy economic rows for + deterministic rebuild or explicit migration before CON-05A/05B. 3. The human and AUTH must approve exact fixed-service identity, action, static row, context, and evaluator contracts for dispatcher mechanics, outbound delivery, reconciliation, projection rebuild, and callback execution before @@ -977,13 +973,12 @@ CON-01 -> CON-02A CON-03A -> CON-03B -> REV lease/policy-freeze persistence CON-02C -> REV Review/FinalAcceptance transaction foundation REV-04B runtime Review/ReviewLease/FinalAcceptance -> CON-03C -> CON-03D -WS-ARCH-001-CP01A -> CP01B -> CP02 -> CP03 -> CP04 -> CP05 -CP05 -> CP06 -> CP07 -> CP08 -CP08 -> WS-ARCH-001-03A/03B -> WS-ARCH-001-03C -> CP09 -stable REV revision lineage + CON-03C/03D + CP06/CP08 -> CON-07 -> REV-10 +CON-03A -> CON-04A +CON-03B + CON-04A -> CON-04B -> CON-05A -> CON-05B +stable REV revision lineage + CON-03C/03D + CON-05A -> CON-07 -> REV-10 AUTH dispatcher contract -> CON-02B -CON-02B + CON-03D + CP02/CP04 + CON-07 -> fresh delivery contracts -fresh delivery contracts -> CON-10A -> CON-10B +CON-02B + CON-03D + CON-04A/B + CON-07 -> CON-08A -> CON-08R -> CON-08B +CON-08B -> CON-10A -> CON-10B CON-02B + CON-10B -> CON-10C all required hidden behavior and cross-initiative gates -> CON-11 ``` @@ -996,9 +991,8 @@ not a prerequisite for CON-02C, CON-03A, or CON-03B. Cross-initiative interleaving is mandatory: ```text -CP06 validation -> CP07 PROJECT guide binding -> CP08 TASK lineage persistence - -> ARCH-03A/03B readiness and assignment inheritance -> ARCH-03C activation - -> CP09 retired-path removal +CON-05A guide-activation validation/persistence contract + -> PROJECT guide binding -> TASK readiness lock -> assignment inheritance CON-03B ContributionPolicyVersion persistence -> REV-03 ReviewLease foreign key From c538c250cc31d1204ed7cd3a25a57ea14e272242 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Fri, 14 Aug 2026 06:46:28 +0100 Subject: [PATCH 8/8] fix(auth): align adapter route key contract --- .../STATUS.md | 6 ++--- .../auth/adapter-binding-facts.json | 1 + .../authorization/api/adapter_bindings.py | 9 ++++++- .../test_adapter_binding_registration.py | 25 +++++++++++++------ 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/.agent-loop/initiatives/WS-CON-001-contribution-compensation-boundary/STATUS.md b/.agent-loop/initiatives/WS-CON-001-contribution-compensation-boundary/STATUS.md index 37a4cf23e..4fca14b58 100644 --- a/.agent-loop/initiatives/WS-CON-001-contribution-compensation-boundary/STATUS.md +++ b/.agent-loop/initiatives/WS-CON-001-contribution-compensation-boundary/STATUS.md @@ -114,7 +114,7 @@ CP08/ARCH-03B replacement path. ## Immediate next action CON-02C merged through PR #277. REV-04B may consume its shared lifecycle-audit -participant after REV's earlier gates merge. The first proposed policy-cutover -implementation begins with CP01A then CP01B unavailable AUTH registration; no -product behavior may start until both current-main contracts merge. +participant after REV's earlier gates merge. CP01B unavailable ContributionPolicy +AUTH registration is the next policy-cutover gate; no product behavior may start +until that current-main contract merges. Open pull requests determine transient CON work. diff --git a/.ci/behavior-ownership/auth/adapter-binding-facts.json b/.ci/behavior-ownership/auth/adapter-binding-facts.json index f2becd85b..4bcba51c9 100644 --- a/.ci/behavior-ownership/auth/adapter-binding-facts.json +++ b/.ci/behavior-ownership/auth/adapter-binding-facts.json @@ -7,6 +7,7 @@ "app.modules.authorization.api.adapter_bindings.AdapterBindingResumeFacts.__post_init__", "app.modules.authorization.api.adapter_bindings.AdapterBindingSuspendFacts.__post_init__", "app.modules.authorization.api.adapter_bindings._require_token", + "app.modules.authorization.api.adapter_bindings._require_route_key", "app.modules.authorization.api.adapter_bindings._require_uuid", "app.modules.authorization.api.adapter_bindings.adapter_binding_resource_digest" ], diff --git a/backend/app/modules/authorization/api/adapter_bindings.py b/backend/app/modules/authorization/api/adapter_bindings.py index 5b65fdd8b..67eaa7660 100644 --- a/backend/app/modules/authorization/api/adapter_bindings.py +++ b/backend/app/modules/authorization/api/adapter_bindings.py @@ -12,6 +12,7 @@ from .action_ids import ActionId _TOKEN = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,159}\Z") +_ROUTE_KEY = re.compile(r"[A-Za-z][A-Za-z0-9._:-]{0,119}\Z") def _require_token(name: str, value: str) -> None: @@ -26,6 +27,12 @@ def _require_uuid(name: str, value: UUID) -> None: raise ValueError(f"{name} must be a UUID") +def _require_route_key(value: str) -> None: + """Enforce the canonical compensation adapter route-key grammar.""" + if not isinstance(value, str) or not _ROUTE_KEY.fullmatch(value) or ".." in value: + raise ValueError("route_key must be canonical and traversal-free") + + @dataclass(frozen=True, slots=True, kw_only=True) class AdapterBindingReadFacts: """Exact project and binding identity for a planned binding read.""" @@ -55,7 +62,7 @@ def __post_init__(self) -> None: _require_uuid("adapter_actor_id", self.adapter_actor_id) _require_token("instrument", self.instrument) _require_token("unit", self.unit) - _require_token("route_key", self.route_key) + _require_route_key(self.route_key) @dataclass(frozen=True, slots=True, kw_only=True) diff --git a/backend/tests/authorization/test_adapter_binding_registration.py b/backend/tests/authorization/test_adapter_binding_registration.py index a993eb095..5e4b406f0 100644 --- a/backend/tests/authorization/test_adapter_binding_registration.py +++ b/backend/tests/authorization/test_adapter_binding_registration.py @@ -64,14 +64,23 @@ def test_cp01a_public_facts_are_immutable_and_validate_lifecycle_state() -> None adapter_binding_id=binding_id, expected_status="active", ) - with pytest.raises(ValueError, match="route_key must be"): - AdapterBindingCreateFacts( - project_id=project_id, - instrument="money", - unit="USD", - adapter_actor_id=uuid4(), - route_key="secret route with spaces", - ) + for route_key in ("1adapter", "adapter..secret", "a" * 121, "secret route"): + with pytest.raises(ValueError, match="route_key must be canonical"): + AdapterBindingCreateFacts( + project_id=project_id, + instrument="money", + unit="USD", + adapter_actor_id=uuid4(), + route_key=route_key, + ) + + assert AdapterBindingCreateFacts( + project_id=project_id, + instrument="money", + unit="USD", + adapter_actor_id=uuid4(), + route_key="a" * 120, + ).route_key == "a" * 120 def test_cp01a_digest_is_deterministic_and_action_domain_separated() -> None: