diff --git a/tests/conformance/README.md b/tests/conformance/README.md index cf03ec8..1228ffe 100644 --- a/tests/conformance/README.md +++ b/tests/conformance/README.md @@ -102,3 +102,6 @@ Spec: [trace-a2a-profile.md](../../docs/spec/trace-a2a-profile.md), [provenance- | ACTION-006 | MUST | A valid delegated action denied by local policy is classified as authorization-invalid, not malformed provenance. | `SCOPE_NOT_PERMITTED`. | | ACTION-007 | MUST | A valid delegated action whose controller outcome is negative remains valid evidence of a negative outcome. | `valid_negative_outcome`. | | ACTION-008 | MUST | An action whose delegation chain contains a credential with an invalid signature is rejected as provenance-invalid before authorization or outcome handling. | `INVALID_CREDENTIAL`. | +| ACTION-009 | MUST | A delegated action with a strictly attenuating multi-hop credential chain verifies. | `verified`. | +| ACTION-010 | MUST | An action evidence chain with scope widening at an intermediate hop is rejected as provenance-invalid. | `SCOPE_ESCALATION`. | +| ACTION-011 | MUST | Action evidence whose delegatee differs from the subject of the referenced credential is rejected as provenance-invalid. | `PROVENANCE_LINK_BROKEN`. | diff --git a/tests/conformance/test_profile_conformance.py b/tests/conformance/test_profile_conformance.py index 09fd14a..b6c7f39 100644 --- a/tests/conformance/test_profile_conformance.py +++ b/tests/conformance/test_profile_conformance.py @@ -435,3 +435,57 @@ def test_action_008_invalid_delegation_signature_is_provenance_invalid() -> None assert _verify_action_evidence(bad_chain, records, evidence, permissive_policy) == ( _ActionEvidenceResult("provenance_invalid", "INVALID_CREDENTIAL") ) +def test_action_009_multi_hop_attenuation_verifies() -> None: + chain = build_chain( + [ + frozenset({"robot.move", "robot.inspect", "robot.stop"}), + frozenset({"robot.move", "robot.inspect"}), + frozenset({"robot.move"}), + ] + ) + records = _records(chain) + result = _verify_action_evidence( + chain, + records, + _action_evidence(records), + LocalPolicy.of(["robot.move", "robot.inspect"]), + ) + assert result == _ActionEvidenceResult("verified", "ACCEPTED") + + +def test_action_010_intermediate_scope_widening_is_provenance_invalid() -> None: + chain = build_chain( + [ + frozenset({"robot.move"}), + frozenset({"robot.move", "robot.inspect"}), + frozenset({"robot.move"}), + ] + ) + records = _records(chain) + result = _verify_action_evidence( + chain, + records, + _action_evidence(records), + LocalPolicy.of(["robot.move", "robot.inspect"]), + ) + assert result == _ActionEvidenceResult("provenance_invalid", "SCOPE_ESCALATION") + + +def test_action_011_delegatee_mismatch_is_provenance_invalid() -> None: + chain = _action_chain() + records = _records(chain) + leaf = records[-1] + records[-1] = DelegationRecord( + leaf.record_id, + leaf.credential_id, + subject="different-delegatee", + scope=leaf.scope, + parent_record_hash=leaf.parent_record_hash, + ) + result = _verify_action_evidence( + chain, + records, + _action_evidence(records), + LocalPolicy.of(["robot.move"]), + ) + assert result == _ActionEvidenceResult("provenance_invalid", "PROVENANCE_LINK_BROKEN")