diff --git a/integrations/aeoess-aps/README.md b/integrations/aeoess-aps/README.md index 6a8780e..72842cb 100644 --- a/integrations/aeoess-aps/README.md +++ b/integrations/aeoess-aps/README.md @@ -57,13 +57,25 @@ permanently unmappable. No network access and no credentials are needed. | `policy.version` | `floorVersion` | | `runtime.platform` | `software-only` | | `runtime.measurement` | sha256 over the APS canonical bytes of the full signed decision | -| `appraisal.status` | `permit` to `affirming`, `narrow` to `warning`, `deny` to `contraindicated` | +| `appraisal.status` | constant `none` | | `appraisal.verifier` | `urn:aps:evaluator:` | | `appraisal.policy_ref` | `urn:aps:floor:` | | `appraisal.timestamp` | `iat` | | `transparency` | `urn:aps:transparency:none` | -An APS verdict this mapper does not know is refused rather than appraised. +An APS verdict this mapper does not know is refused rather than transcribed. + +`appraisal.status` is `none` and is not a parameter. The +`agentrust-trace-adapters` convention that landed on `main` in commit `e1aa231` +(2026-08-08) sets it that way for any record assembled from evidence another +system produced: "Nobody appraised the evidence. Transcribing is not +appraising", and "A vendor's bare ALLOW/DENY result is still a policy decision, +not an appraisal of the evidence behind that decision." An APS verdict is +exactly such a policy decision. Our mapping was reviewed as defensible on +2026-08-03. A clearer adapter convention landed on 2026-08-09 that separates +policy decisions from evidence appraisal, and the exporter aligns to that +convention here. The verdict is still carried, as `policy.enforcement_mode` and +`policy.version`. ## What is verified diff --git a/integrations/aeoess-aps/aps_trace.py b/integrations/aeoess-aps/aps_trace.py index fe9df5a..84eda8e 100644 --- a/integrations/aeoess-aps/aps_trace.py +++ b/integrations/aeoess-aps/aps_trace.py @@ -37,7 +37,7 @@ runtime.platform "software-only" runtime.measurement sha256 over the canonical bytes of the full signed decision -appraisal.status ``verdict`` through VERDICT_TO_APPRAISAL +appraisal.status constant "none": nothing here appraised the evidence appraisal.verifier urn:aps:evaluator: appraisal.policy_ref urn:aps:floor: (omitted when empty) appraisal.timestamp iat @@ -74,15 +74,22 @@ #: SPIFFE trust domain for APS workload identities. TRUST_DOMAIN = "agent-passport.org" -#: APS verdict to EAR appraisal status (draft-ietf-rats-ar4si). -#: permit authorizes the action, narrow authorizes it under added constraints, -#: deny withholds authorization. TRACE has no "narrow", and "warning" is the -#: EAR status for an appraisal that affirms with reservations. -VERDICT_TO_APPRAISAL = { - "permit": "affirming", - "narrow": "warning", - "deny": "contraindicated", -} +#: APS verdicts this mapper accepts. permit authorizes the action, narrow +#: authorizes it under added constraints, deny withholds authorization. The +#: verdict is checked against this set so an unknown one is refused rather than +#: transcribed, but it no longer selects an appraisal status: see APPRAISAL_STATUS. +KNOWN_VERDICTS = frozenset({"permit", "narrow", "deny"}) + +#: appraisal.status is a constant, not a parameter. +#: The agentrust-trace-adapters convention (integrations packages/agentrust- +#: trace-adapters/README.md, commit e1aa231, 2026-08-08) sets appraisal.status +#: to "none" for a record assembled from evidence another system produced, on +#: the grounds that "Nobody appraised the evidence. Transcribing is not +#: appraising", and that "A vendor's bare ALLOW/DENY result is still a policy +#: decision, not an appraisal of the evidence behind that decision." +#: An APS verdict is exactly such a policy decision. It stays in the record as +#: policy.enforcement_mode and policy.version; it is not an evidence appraisal. +APPRAISAL_STATUS = "none" #: Keys a signed APS policy decision must carry. Checked before signature #: verification so a malformed input fails with a precise message. @@ -129,7 +136,7 @@ def build_trace_record(decision: dict[str, Any], *, trace_jwk: dict[str, str]) - iat = _unix_seconds(decision["evaluatedAt"]) appraisal: dict[str, Any] = { - "status": VERDICT_TO_APPRAISAL[decision["verdict"]], + "status": APPRAISAL_STATUS, "timestamp": iat, "verifier": f"urn:aps:evaluator:{quote(str(evaluator_id), safe='')}", } @@ -190,10 +197,10 @@ def _validate_decision(decision: dict[str, Any]) -> None: ) verdict = decision["verdict"] - if verdict not in VERDICT_TO_APPRAISAL: + if verdict not in KNOWN_VERDICTS: raise ValueError( f"unknown APS verdict {verdict!r}; expected one of " - f"{sorted(VERDICT_TO_APPRAISAL)}" + f"{sorted(KNOWN_VERDICTS)}" ) diff --git a/integrations/aeoess-aps/integration.yaml b/integrations/aeoess-aps/integration.yaml index eb6259e..1b52401 100644 --- a/integrations/aeoess-aps/integration.yaml +++ b/integrations/aeoess-aps/integration.yaml @@ -23,4 +23,4 @@ trace_conformance_level: 0 trace_roles: - record-producer tested_against: - agentrust-trace: "0.5.1" + agentrust-trace: "0.9.0" diff --git a/integrations/aeoess-aps/tests/test_mapping.py b/integrations/aeoess-aps/tests/test_mapping.py index d504a19..f255ebf 100644 --- a/integrations/aeoess-aps/tests/test_mapping.py +++ b/integrations/aeoess-aps/tests/test_mapping.py @@ -18,7 +18,7 @@ from agent_passport.crypto import generate_key_pair from agent_passport.policy import FloorValidatorV1, create_action_intent, evaluate_intent -from aps_trace import EAT_PROFILE, TRUST_DOMAIN, VERDICT_TO_APPRAISAL, build_trace_record +from aps_trace import APPRAISAL_STATUS, EAT_PROFILE, KNOWN_VERDICTS, TRUST_DOMAIN, build_trace_record FLOOR_VERSION = "floor-1.0" @@ -153,28 +153,29 @@ def test_record_carries_no_aps_signature(record): # --- verdict to appraisal ---------------------------------------------------- -def test_permit_maps_to_affirming(record): - assert record["appraisal"]["status"] == "affirming" +def test_permit_does_not_appraise(record): + """The verdict is a policy decision, so it does not set an appraisal status.""" + assert record["appraisal"]["status"] == "none" -def test_narrow_maps_to_warning(): +def test_narrow_does_not_appraise(): decision = _decide( {"scopeRequired": "repo:read", "spend": {"amount": 100, "currency": "USD"}}, _context(scope=["repo:read"], spend_limit=50, spent=0), ) assert decision["verdict"] == "narrow" - assert build_trace_record(decision, trace_jwk=_jwk())["appraisal"]["status"] == "warning" + assert build_trace_record(decision, trace_jwk=_jwk())["appraisal"]["status"] == "none" -def test_deny_maps_to_contraindicated(): +def test_deny_does_not_appraise(): decision = _decide({"scopeRequired": "repo:write"}, _context(scope=["repo:read"])) assert decision["verdict"] == "deny" - assert build_trace_record(decision, trace_jwk=_jwk())["appraisal"]["status"] == "contraindicated" + assert build_trace_record(decision, trace_jwk=_jwk())["appraisal"]["status"] == "none" def test_every_known_verdict_maps_to_a_valid_ear_status(): valid = set(agentrust_trace.SCHEMA["properties"]["appraisal"]["properties"]["status"]["enum"]) - assert set(VERDICT_TO_APPRAISAL.values()) <= valid + assert APPRAISAL_STATUS in valid def test_enforcement_mode_is_enforce_when_a_principle_is_inline(record, permit_decision): @@ -256,11 +257,23 @@ def test_sign_verify_roundtrip(record): def test_tampered_signed_record_fails_verification(record): key = agentrust_trace.generate_key() signed = agentrust_trace.sign_record(dict(record), key) - signed["appraisal"]["status"] = "affirming" if signed["appraisal"]["status"] != "affirming" else "denying" + signed["appraisal"]["status"] = "affirming" # any value != the emitted constant with pytest.raises(Exception): agentrust_trace.verify_record(signed, allow_embedded_key=True, max_age_seconds=None) +def test_appraisal_status_is_a_constant_not_a_parameter(): + """Every accepted verdict emits the same appraisal status. + + The agentrust-trace-adapters convention (commit e1aa231, 2026-08-08) makes + appraisal.status a constant for records assembled from someone else's + evidence. If a future edit reintroduces a verdict-to-status mapping, this + fails. + """ + assert APPRAISAL_STATUS == "none" + assert KNOWN_VERDICTS == {"permit", "narrow", "deny"} + + # --- conformance ------------------------------------------------------------- def test_absent_schema_required_fields_are_exactly_the_documented_set(record):