Skip to content
Open
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
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"caseSensitive": true,
"useGitignore": true,
"ignorePaths": [
".ruff.toml",
".github/**",
".cspell/**",
".gemini/**",
Expand Down
3 changes: 2 additions & 1 deletion .github/linters/.markdownlint.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
},
"MD033": false,
"MD046": false,
"MD024": false
"MD024": false,
"MD060": false
}
3 changes: 3 additions & 0 deletions .github/workflows/linter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ jobs:
VALIDATE_PYTHON_PYLINT: false
VALIDATE_CHECKOV: false
VALIDATE_NATURAL_LANGUAGE: false
# Filename only; super-linter loads it from LINTER_RULES_PATH (default:
# `.github/linters`). A repo-root `.markdownlint.json` is not used.
LINTER_RULES_PATH: ".github/linters"
MARKDOWN_CONFIG_FILE: ".markdownlint.json"
VALIDATE_MARKDOWN_PRETTIER: false
VALIDATE_JAVASCRIPT_PRETTIER: false
Expand Down
1 change: 1 addition & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ target-version = "py312" # Minimum Python version

[lint]
ignore = [
"UP017", # datetime.UTC is 3.11+; project supports Python 3.10 (use timezone.utc).
"COM812",
"FBT001",
"FBT002",
Expand Down
5 changes: 3 additions & 2 deletions docs/a2a-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,12 @@ The following listing shows the JSON rendering of an IntentMandate Message.
"kind": "data",
"data": {
"ap2.mandates.IntentMandate": {
"user_cart_confirmation_required": false,
"id": "550e8400-e29b-41d4-a716-446655440000",
"user_cart_confirmation_required": true,
"natural_language_description": "I'd like some cool red shoes in my size",
"merchants": null,
"skus": null,
"required_refundability": true,
"requires_refundability": true,
"intent_expiry": "2025-09-16T15:00:00Z"
}
}
Expand Down
14 changes: 9 additions & 5 deletions docs/specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,11 +282,15 @@ Mandate, the Intent Mandate and the Payment Mandate.
The Cart Mandate is the foundational credential that captures the user's
authorization for a purchase when the human is present at the time of purchase
([see illustrative user journey](#51-human-present-transaction)). It is
generated by the Merchant based on the user's request and is cryptographically
signed by the user, typically using a hardware-backed key on their device with
in-session authentication. This signature binds the user's identity and
authorization to their intent. The Cart Mandate is a structured object
containing critical parameters that define the scope of the transaction.
generated by the Merchant based on the user's request. The merchant first signs
the Cart Mandate (see Step 10 in [Section 7.1](#71-illustrative-transaction-flow))
to guarantee fulfillment. The user then approves the merchant-signed Cart Mandate by
including its hash in their Payment Mandate signature, typically using a
hardware-backed key on their device with in-session authentication. That
Payment Mandate binding (not a separate field on the Cart Mandate object)
anchors the user's approval to the merchant-signed cart. The Cart Mandate is a
structured object containing critical parameters that define the scope of the
transaction.

A Cart Mandate contains the following bound information:

Expand Down
4 changes: 4 additions & 0 deletions samples/go/pkg/ap2/types/mandate.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ const (
)

type IntentMandate struct {
ID string `json:"id,omitempty"`
UserCartConfirmationRequired *bool `json:"user_cart_confirmation_required,omitempty"`
NaturalLanguageDescription string `json:"natural_language_description"`
Merchants []string `json:"merchants,omitempty"`
SKUs []string `json:"skus,omitempty"`
RequiresRefundability *bool `json:"requires_refundability,omitempty"`
IntentExpiry string `json:"intent_expiry"`
UserAuthorization *string `json:"user_authorization,omitempty"`
}

func NewIntentMandate() *IntentMandate {
Expand Down Expand Up @@ -75,6 +77,8 @@ type PaymentMandateContents struct {
PaymentDetailsTotal PaymentItem `json:"payment_details_total"`
PaymentResponse PaymentResponse `json:"payment_response"`
MerchantAgent string `json:"merchant_agent"`
IntentMandateID *string `json:"intent_mandate_id,omitempty"`
TransactionModality *string `json:"transaction_modality,omitempty"`
Timestamp string `json:"timestamp,omitempty"`
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,23 @@
shopping and purchasing process.
"""

from datetime import datetime
from datetime import timedelta
from datetime import timezone
from datetime import datetime, timedelta, timezone

from a2a.types import Artifact
from google.adk.tools.tool_context import ToolContext

from ap2.types.mandate import CART_MANDATE_DATA_KEY
from ap2.types.mandate import CartMandate
from ap2.types.mandate import INTENT_MANDATE_DATA_KEY
from ap2.types.mandate import IntentMandate
from common.a2a_message_builder import A2aMessageBuilder
from common.artifact_utils import find_canonical_objects
from google.adk.tools.tool_context import ToolContext
from roles.shopping_agent.remote_agents import merchant_agent_client

from ap2.types.mandate import (
CART_MANDATE_DATA_KEY,
INTENT_MANDATE_DATA_KEY,
CartMandate,
IntentMandate,
)


def create_intent_mandate(
def create_intent_mandate( # noqa: PLR0913
natural_language_description: str,
user_cart_confirmation_required: bool,
merchants: list[str],
Expand All @@ -55,6 +55,12 @@ def create_intent_mandate(
Returns:
An IntentMandate object valid for 1 day.
"""
# Sample placeholder JWT when the user allows agent checkout without
# per-cart confirmation; production must use a real hardware-backed signature.
user_authorization = None
if not user_cart_confirmation_required:
# Sample placeholder; production must use a real hardware-backed JWT.
user_authorization = "dev.placeholder.intent_mandate_user_authorization_jwt"
intent_mandate = IntentMandate(
natural_language_description=natural_language_description,
user_cart_confirmation_required=user_cart_confirmation_required,
Expand All @@ -64,6 +70,7 @@ def create_intent_mandate(
intent_expiry=(
datetime.now(timezone.utc) + timedelta(days=1)
).isoformat(),
user_authorization=user_authorization,
)
tool_context.state["intent_mandate"] = intent_mandate
return intent_mandate
Expand Down
91 changes: 68 additions & 23 deletions samples/python/src/roles/shopping_agent/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,32 @@
shopping and purchasing process, such as updating a cart or initiating payment.
"""

from datetime import datetime
from datetime import timezone
import os
import uuid

from datetime import datetime, timezone

from a2a.types import Artifact
from common import artifact_utils
from common.a2a_message_builder import A2aMessageBuilder
from google.adk.tools.tool_context import ToolContext
from roles.shopping_agent.remote_agents import (
credentials_provider_client,
merchant_agent_client,
)

from .remote_agents import credentials_provider_client
from .remote_agents import merchant_agent_client
from ap2.types.contact_picker import ContactAddress
from ap2.types.mandate import CART_MANDATE_DATA_KEY
from ap2.types.mandate import CartMandate
from ap2.types.mandate import PAYMENT_MANDATE_DATA_KEY
from ap2.types.mandate import PaymentMandate
from ap2.types.mandate import PaymentMandateContents
from ap2.types.payment_receipt import PAYMENT_RECEIPT_DATA_KEY
from ap2.types.payment_receipt import PaymentReceipt
from ap2.types.mandate import (
CART_MANDATE_DATA_KEY,
PAYMENT_MANDATE_DATA_KEY,
CartMandate,
IntentMandate,
PaymentMandate,
PaymentMandateContents,
TransactionModality,
)
from ap2.types.payment_receipt import PAYMENT_RECEIPT_DATA_KEY, PaymentReceipt
from ap2.types.payment_request import PaymentResponse
from common import artifact_utils
from common.a2a_message_builder import A2aMessageBuilder


async def update_cart(
Expand Down Expand Up @@ -118,10 +123,10 @@ async def initiate_payment(tool_context: ToolContext, debug_mode: bool = False):
async def initiate_payment_with_otp(
challenge_response: str, tool_context: ToolContext, debug_mode: bool = False
):
"""Initiates a payment using the payment mandate from state and a
"""Initiates payment with the mandate from state and an OTP challenge.

challenge response. In our sample, the challenge response is a one-time
password (OTP) sent to the user.
In our sample, the challenge response is a one-time password (OTP) sent to
the user.

Args:
challenge_response: The challenge response.
Expand Down Expand Up @@ -204,6 +209,18 @@ def create_payment_mandate(
payer_email=user_email,
)

intent_mandate: IntentMandate | None = tool_context.state.get(
"intent_mandate",
)
intent_mandate_id: str | None = None
transaction_modality: TransactionModality | None = None
if intent_mandate:
if intent_mandate.user_authorization:
intent_mandate_id = intent_mandate.id
transaction_modality = TransactionModality.HUMAN_NOT_PRESENT
else:
transaction_modality = TransactionModality.HUMAN_PRESENT

payment_mandate = PaymentMandate(
payment_mandate_contents=PaymentMandateContents(
payment_mandate_id=uuid.uuid4().hex,
Expand All @@ -212,6 +229,8 @@ def create_payment_mandate(
payment_details_total=payment_request.details.total,
payment_response=payment_response,
merchant_agent=cart_mandate.contents.merchant_name,
intent_mandate_id=intent_mandate_id,
transaction_modality=transaction_modality,
),
)

Expand All @@ -232,24 +251,30 @@ def sign_mandates_on_user_device(tool_context: ToolContext) -> str:
concatenating the mandate hashes.

Args:
tool_context: The context object used for state management. It is expected
to contain the `payment_mandate` and `cart_mandate`.
tool_context: The context object used for state management. Expected to
contain the `payment_mandate` and `cart_mandate`.

Returns:
A string representing the simulated user authorization signature (JWT).
"""
payment_mandate: PaymentMandate = tool_context.state["payment_mandate"]
cart_mandate: CartMandate = tool_context.state["cart_mandate"]
intent_mandate: IntentMandate | None = tool_context.state.get(
"intent_mandate",
)
cart_mandate_hash = _generate_cart_mandate_hash(cart_mandate)
payment_mandate_hash = _generate_payment_mandate_hash(
payment_mandate.payment_mandate_contents
)
# A JWT containing the user's digital signature to authorize the transaction.
# The payload uses hashes to bind the signature to the specific cart and
# A JWT containing the user's digital signature to authorize the
# transaction. The payload uses hashes to bind the signature to the cart and
# payment details, and includes a nonce to prevent replay attacks.
payment_mandate.user_authorization = (
cart_mandate_hash + "_" + payment_mandate_hash
)
hashes = [cart_mandate_hash, payment_mandate_hash]
# When the intent mandate is user-signed (user_authorization set), bind the
# payment authorization to that intent for Human Not Present scenarios.
if intent_mandate and intent_mandate.user_authorization:
hashes.append(_generate_intent_mandate_hash(intent_mandate))
payment_mandate.user_authorization = "_".join(hashes)
tool_context.state["signed_payment_mandate"] = payment_mandate
return payment_mandate.user_authorization

Expand Down Expand Up @@ -302,6 +327,26 @@ def _generate_cart_mandate_hash(cart_mandate: CartMandate) -> str:
return "fake_cart_mandate_hash_" + cart_mandate.contents.id


def _generate_intent_mandate_hash(intent_mandate: IntentMandate) -> str:
"""Generates a cryptographic hash of the IntentMandate.

This hash binds the user's payment authorization to the specific
user-signed Intent Mandate, ensuring that Human Not Present transactions
can be traced back to a verified user intent.

Note: This is a placeholder implementation for development. A real
implementation must use a secure hashing algorithm (e.g., SHA-256) on the
canonical representation of the IntentMandate object.

Args:
intent_mandate: The IntentMandate object to hash.

Returns:
A string representing the hash of the intent mandate.
"""
return "fake_intent_mandate_hash_" + intent_mandate.id


def _generate_payment_mandate_hash(
payment_mandate_contents: PaymentMandateContents,
) -> str:
Expand Down
Loading
Loading