Skip to content

Commit d7c2acf

Browse files
ravygayushozha
andcommitted
feat(types): integrate risk_payload into mandates and add CREDENTIAL_CHECK condition
Incorporates two contributions from @ayushozha's work on PR google-agentic-commerce#187 and their analysis on Issue google-agentic-commerce#163: 1. Wire risk_payload as an optional field into IntentMandate, CartMandate, and PaymentMandateContents, so risk signals travel with the mandate chain rather than as a separate DataPart. This closes the spec-implementation gap identified in Section 7.4 (lines 298-299, 321-322). 2. Add CREDENTIAL_CHECK as a new TripConditionType for static identity verification (e.g., on-chain wallet attestations, KYB credentials). This addresses the Section 7.4 gap between behavioral risk (what the agent does) and identity risk (what the agent is). Includes Python tests, Go type updates, and documentation for both changes. Refs: google-agentic-commerce#163, google-agentic-commerce#187 Co-Authored-By: ayushozha <7945279+ayushozha@users.noreply.github.com>
1 parent c827070 commit d7c2acf

6 files changed

Lines changed: 183 additions & 0 deletions

File tree

docs/topics/fiduciary-circuit-breaker.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,32 @@ Trip conditions are predicate functions that evaluate agent behavior:
9898
| `TIME_BASED` | Action during restricted period | Trade outside market hours |
9999
| `DEVIATION` | Significant departure from baseline | Price 30% below historical average |
100100
| `VENDOR_TRUST` | Untrusted counterparty | New vendor in high-risk region |
101+
| `CREDENTIAL_CHECK` | Static identity/credential verification | Agent wallet holds KYB attestation |
102+
103+
## Identity vs. Behavioral Risk
104+
105+
AP2 Section 7.4 identifies two complementary dimensions of risk in agent transactions:
106+
107+
### Behavioral Risk (Runtime)
108+
109+
Most trip conditions (`VALUE_THRESHOLD`, `VELOCITY`, `ANOMALY`, etc.) evaluate **what the agent does** — its runtime behavior against predefined thresholds. These are dynamic checks that may change with every transaction.
110+
111+
### Identity Risk (Static)
112+
113+
The `CREDENTIAL_CHECK` trip condition evaluates **what the agent is** — its identity, credentials, and trust signals. This addresses the spec's call-out that "the Shopping Agent's ID becomes synonymous with a bot's identity, which requires new methods of verification and trust."
114+
115+
Examples of credential checks:
116+
117+
- Agent wallet holds governance tokens from a recognized DAO
118+
- Agent has a KYB (Know Your Business) attestation via [EAS](https://attest.org/) on Base
119+
- Agent presents a W3C Verifiable Credential from a trusted issuer
120+
- Agent identity is registered in a trust registry
121+
122+
Credential check results use the standard `TripConditionResult` structure. The `message` field carries human-readable verification details, while implementation-specific data (attestation UIDs, chain references, credential schemas) should flow through `RiskPayload.custom_signals` to keep the core types ecosystem-agnostic.
123+
124+
!!! note
125+
126+
Future versions may separate identity evaluation into its own dimension within `RiskPayload` (e.g., `identity_evaluation`) to provide richer type support for credential data shapes that differ from behavioral thresholds.
101127

102128
## Usage in AP2 Messages
103129

@@ -279,6 +305,12 @@ riskPayload.AgentID = &agentID
279305
- Confidence that agents operate within guardrails
280306
- Human oversight for exceptional cases
281307

308+
## Risk Payload in Mandates
309+
310+
As of v0.1-alpha, `RiskPayload` can be included directly on `IntentMandate`, `CartMandate`, and `PaymentMandateContents` via the optional `risk_payload` field. This allows risk signals to travel with the mandate chain, giving networks and issuers structured visibility into the agent's governance state at mandate-signing time.
311+
312+
The `risk_payload` field is fully optional — mandates without it remain valid, ensuring backward compatibility.
313+
282314
## References
283315

284316
- [AP2 Specification Section 7.4: Risk Signals](../specification.md#74-risk-signals)

samples/go/pkg/ap2/types/risk.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ const (
6262
// TripConditionVendorTrust - Transaction with untrusted counterparty.
6363
TripConditionVendorTrust TripConditionType = "VENDOR_TRUST"
6464

65+
// TripConditionCredentialCheck - Static identity/credential verification (e.g., wallet attestation, KYB check).
66+
TripConditionCredentialCheck TripConditionType = "CREDENTIAL_CHECK"
67+
6568
// TripConditionCustom - Implementation-specific trip condition.
6669
TripConditionCustom TripConditionType = "CUSTOM"
6770
)

samples/go/pkg/ap2/types/risk_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func TestTripConditionTypes(t *testing.T) {
2929
TripConditionTimeBased,
3030
TripConditionDeviation,
3131
TripConditionVendorTrust,
32+
TripConditionCredentialCheck,
3233
TripConditionCustom,
3334
}
3435

@@ -45,6 +46,9 @@ func TestTripConditionTypes(t *testing.T) {
4546
if TripConditionCumulativeThreshold != "CUMULATIVE_THRESHOLD" {
4647
t.Errorf("Expected CUMULATIVE_THRESHOLD, got %s", TripConditionCumulativeThreshold)
4748
}
49+
if TripConditionCredentialCheck != "CREDENTIAL_CHECK" {
50+
t.Errorf("Expected CREDENTIAL_CHECK, got %s", TripConditionCredentialCheck)
51+
}
4852
}
4953

5054
func TestFCBStates(t *testing.T) {

src/ap2/types/mandate.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from ap2.types.payment_request import PaymentItem
2222
from ap2.types.payment_request import PaymentRequest
2323
from ap2.types.payment_request import PaymentResponse
24+
from ap2.types.risk import RiskPayload
2425
from pydantic import BaseModel
2526
from pydantic import Field
2627

@@ -74,6 +75,13 @@ class IntentMandate(BaseModel):
7475
...,
7576
description="When the intent mandate expires, in ISO 8601 format.",
7677
)
78+
risk_payload: Optional[RiskPayload] = Field(
79+
None,
80+
description=(
81+
"Optional structured risk payload containing the current risk"
82+
" assessment state as defined in Section 7.4."
83+
),
84+
)
7785

7886

7987
class CartContents(BaseModel):
@@ -111,6 +119,13 @@ class CartMandate(BaseModel):
111119
"""
112120

113121
contents: CartContents = Field(..., description="The contents of the cart.")
122+
risk_payload: Optional[RiskPayload] = Field(
123+
None,
124+
description=(
125+
"Optional structured risk payload containing the current risk"
126+
" assessment state as defined in Section 7.4."
127+
),
128+
)
114129
merchant_authorization: Optional[str] = Field(
115130
None,
116131
description=(""" A base64url-encoded JSON Web Token (JWT) that digitally
@@ -154,6 +169,13 @@ class PaymentMandateContents(BaseModel):
154169
),
155170
)
156171
merchant_agent: str = Field(..., description="Identifier for the merchant.")
172+
risk_payload: Optional[RiskPayload] = Field(
173+
None,
174+
description=(
175+
"Optional structured risk payload containing the current risk"
176+
" assessment state as defined in Section 7.4."
177+
),
178+
)
157179
timestamp: str = Field(
158180
description=(
159181
"The date and time the mandate was created, in ISO 8601 format."

src/ap2/types/risk.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ class TripConditionType(str, Enum):
7474
VENDOR_TRUST = "VENDOR_TRUST"
7575
"""Transaction with unverified or untrusted counterparty."""
7676

77+
CREDENTIAL_CHECK = "CREDENTIAL_CHECK"
78+
"""Static identity/credential verification (e.g., wallet attestation, KYB check)."""
79+
7780
CUSTOM = "CUSTOM"
7881
"""Implementation-specific trip condition."""
7982

tests/test_risk.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def test_all_types_exist(self):
4747
"TIME_BASED",
4848
"DEVIATION",
4949
"VENDOR_TRUST",
50+
"CREDENTIAL_CHECK",
5051
"CUSTOM",
5152
]
5253
actual = [e.value for e in TripConditionType]
@@ -57,6 +58,18 @@ def test_string_values(self):
5758
assert TripConditionType.VALUE_THRESHOLD == "VALUE_THRESHOLD"
5859
assert TripConditionType.CUMULATIVE_THRESHOLD == "CUMULATIVE_THRESHOLD"
5960
assert TripConditionType.VELOCITY == "VELOCITY"
61+
assert TripConditionType.CREDENTIAL_CHECK == "CREDENTIAL_CHECK"
62+
63+
def test_credential_check_in_trip_result(self):
64+
"""Test CREDENTIAL_CHECK can be used in a TripConditionResult."""
65+
result = TripConditionResult(
66+
condition_type=TripConditionType.CREDENTIAL_CHECK,
67+
status=TripConditionStatus.PASS,
68+
message="Agent wallet holds valid KYB attestation via EAS on Base",
69+
suggestion=None,
70+
)
71+
assert result.condition_type == TripConditionType.CREDENTIAL_CHECK
72+
assert result.status == TripConditionStatus.PASS
6073

6174

6275
class TestTripConditionStatus:
@@ -460,3 +473,109 @@ def test_b2b_quote_scenario(self):
460473
json_output = json.loads(payload.model_dump_json())
461474
assert json_output["fcb_evaluation"]["fcb_state"] == "HALF_OPEN"
462475
assert json_output["custom_signals"]["negotiation_rounds"] == 3
476+
477+
478+
class TestMandateRiskPayloadIntegration:
479+
"""Tests for risk_payload integration into mandate types."""
480+
481+
def test_intent_mandate_with_risk_payload(self):
482+
"""Test IntentMandate accepts optional risk_payload."""
483+
from ap2.types.mandate import IntentMandate
484+
485+
payload = RiskPayload(
486+
agent_modality=AgentModality.HUMAN_NOT_PRESENT,
487+
agent_id="shopping-agent-001",
488+
)
489+
mandate = IntentMandate(
490+
natural_language_description="Red basketball shoes under $200",
491+
intent_expiry="2026-03-01T00:00:00Z",
492+
risk_payload=payload,
493+
)
494+
assert mandate.risk_payload is not None
495+
assert mandate.risk_payload.agent_id == "shopping-agent-001"
496+
497+
def test_intent_mandate_without_risk_payload(self):
498+
"""Test IntentMandate works without risk_payload (backward compat)."""
499+
from ap2.types.mandate import IntentMandate
500+
501+
mandate = IntentMandate(
502+
natural_language_description="Red basketball shoes",
503+
intent_expiry="2026-03-01T00:00:00Z",
504+
)
505+
assert mandate.risk_payload is None
506+
507+
def test_cart_mandate_with_risk_payload(self):
508+
"""Test CartMandate accepts optional risk_payload."""
509+
from ap2.types.mandate import CartContents, CartMandate
510+
from ap2.types.payment_request import (
511+
PaymentCurrencyAmount,
512+
PaymentDetailsInit,
513+
PaymentItem,
514+
PaymentRequest,
515+
)
516+
517+
payload = RiskPayload(
518+
fcb_evaluation=FCBEvaluation(
519+
fcb_state=FCBState.CLOSED,
520+
trips_evaluated=3,
521+
trips_triggered=0,
522+
),
523+
agent_modality=AgentModality.HUMAN_NOT_PRESENT,
524+
)
525+
cart_contents = CartContents(
526+
id="cart-001",
527+
user_cart_confirmation_required=True,
528+
payment_request=PaymentRequest(
529+
method_data=[],
530+
details=PaymentDetailsInit(
531+
id="details-001",
532+
display_items=[],
533+
total=PaymentItem(
534+
label="Total",
535+
amount=PaymentCurrencyAmount(
536+
currency="USD", value="100.00"
537+
),
538+
),
539+
),
540+
),
541+
cart_expiry="2026-03-01T00:00:00Z",
542+
merchant_name="Test Merchant",
543+
)
544+
mandate = CartMandate(
545+
contents=cart_contents,
546+
risk_payload=payload,
547+
)
548+
assert mandate.risk_payload is not None
549+
assert mandate.risk_payload.fcb_evaluation.fcb_state == FCBState.CLOSED
550+
551+
def test_payment_mandate_contents_with_risk_payload(self):
552+
"""Test PaymentMandateContents accepts optional risk_payload."""
553+
from ap2.types.mandate import PaymentMandateContents
554+
from ap2.types.payment_request import (
555+
PaymentCurrencyAmount,
556+
PaymentItem,
557+
PaymentResponse,
558+
)
559+
560+
payload = RiskPayload(
561+
agent_modality=AgentModality.HUMAN_NOT_PRESENT,
562+
agent_id="b2b-agent-001",
563+
cumulative_session_value=235000.0,
564+
transaction_count_today=4,
565+
)
566+
contents = PaymentMandateContents(
567+
payment_mandate_id="pm-001",
568+
payment_details_id="pd-001",
569+
payment_details_total=PaymentItem(
570+
label="Total",
571+
amount=PaymentCurrencyAmount(currency="USD", value="85000.00"),
572+
),
573+
payment_response=PaymentResponse(
574+
request_id="req-001",
575+
method_name="basic-card",
576+
),
577+
merchant_agent="merchant-001",
578+
risk_payload=payload,
579+
)
580+
assert contents.risk_payload is not None
581+
assert contents.risk_payload.cumulative_session_value == 235000.0

0 commit comments

Comments
 (0)