Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions integrations/aeoess-aps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:<evaluatorId>` |
| `appraisal.policy_ref` | `urn:aps:floor:<floorVersion>` |
| `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

Expand Down
33 changes: 20 additions & 13 deletions integrations/aeoess-aps/aps_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:<evaluatorId>
appraisal.policy_ref urn:aps:floor:<floorVersion> (omitted when empty)
appraisal.timestamp iat
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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='')}",
}
Expand Down Expand Up @@ -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)}"
)


Expand Down
2 changes: 1 addition & 1 deletion integrations/aeoess-aps/integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ trace_conformance_level: 0
trace_roles:
- record-producer
tested_against:
agentrust-trace: "0.5.1"
agentrust-trace: "0.9.0"
31 changes: 22 additions & 9 deletions integrations/aeoess-aps/tests/test_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
Loading