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..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 @@ -37,7 +37,20 @@ 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) +.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) +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/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 @@ -119,4 +132,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-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/.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..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 @@ -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 @@ -112,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 new file mode 100644 index 000000000..4bcba51c9 --- /dev/null +++ b/.ci/behavior-ownership/auth/adapter-binding-facts.json @@ -0,0 +1,29 @@ +{ + "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_route_key", + "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" + ] +} 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/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..67eaa7660 --- /dev/null +++ b/backend/app/modules/authorization/api/adapter_bindings.py @@ -0,0 +1,135 @@ +"""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") +_ROUTE_KEY = re.compile(r"[A-Za-z][A-Za-z0-9._:-]{0,119}\Z") + + +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") + + +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.""" + + project_id: UUID + 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) + + +@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: + """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) + _require_token("unit", self.unit) + _require_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 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": + 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 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": + 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/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", 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", 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..5e4b406f0 --- /dev/null +++ b/backend/tests/authorization/test_adapter_binding_registration.py @@ -0,0 +1,118 @@ +"""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", + ) + 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: + 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..c760c6881 100644 --- a/backend/tests/test_authorization.py +++ b/backend/tests/test_authorization.py @@ -2083,13 +2083,13 @@ 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"), + "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 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 { @@ -2224,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 - ) == 45 - ) + ) + 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) @@ -2900,7 +2899,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..24a66be6a 100644 --- a/docs/spec_contribution_compensation.md +++ b/docs/spec_contribution_compensation.md @@ -751,21 +751,25 @@ 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 - -The following 22 mappings are product proposals, not registered or executable -identifiers. Existing PermissionIds remain stable. Policy ActionIds use the +### Surface mappings + +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. -| 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 | 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 |