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
22 changes: 11 additions & 11 deletions CLAUDE.md

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pip install 'agentscore-commerce[fastapi,x402,coinbase]'

| Submodule | What it provides |
|---|---|
| `agentscore_commerce` (top-level) | `Checkout` orchestrator + `CheckoutContext` + `CheckoutGateConfig` + `CheckoutValidationError` + `DiscoveryProbeConfig` + `SettleOutcome` + `MppxComposeOutcome` + `PricingResult` (the 2.0 high-level surface: one config object, hooks for pre_validate/compute_pricing/on_settled/mint_recipients/compose_mppx, auto-derived x402+mppx servers, per-framework adapters `handle_fastapi`/`handle_flask`/`handle_django`/`handle_aiohttp`/`handle_sanic`, signed UCP routes via `mount_ucp_routes_{fastapi,flask,django,aiohttp,sanic}`); `create_result_cache` (neutral keyed JSON-value cache with a stable body-hash key builder; cache a probe-leg result, e.g. a paid upstream call made in `pre_validate`, and replay it on the settle leg) plus the compute-first-shaped `create_quote_cache` built on it; `pricing_result` (factory: cents-denominated → typed `PricingResult` with embedded `PricingBlock`); `validation_response_{fastapi,flask,django,aiohttp,sanic}` (per-framework 4xx envelope wrappers); `make_mppx_compose_hook` (canonical pympp compose adapter). |
| `agentscore_commerce` (top-level) | `Checkout` orchestrator + `CheckoutContext` + `CheckoutGateConfig` + `CheckoutValidationError` + `DiscoveryProbeConfig` + `SettleOutcome` + `MppxComposeOutcome` + `PricingResult` (one config object, hooks for pre_validate/compute_pricing/on_settled/mint_recipients/compose_mppx, auto-derived x402+mppx servers, per-framework adapters `handle_fastapi`/`handle_flask`/`handle_django`/`handle_aiohttp`/`handle_sanic`, signed UCP routes via `mount_ucp_routes_{fastapi,flask,django,aiohttp,sanic}`); `create_result_cache` (neutral keyed JSON-value cache with a stable body-hash key builder; cache a probe-leg result, e.g. a paid upstream call made in `pre_validate`, and replay it on the settle leg) plus the compute-first-shaped `create_quote_cache` built on it; `pricing_result` (factory: cents-denominated → typed `PricingResult` with embedded `PricingBlock`); `validation_response_{fastapi,flask,django,aiohttp,sanic}` (per-framework 4xx envelope wrappers); `make_mppx_compose_hook` (canonical pympp compose adapter). |
| `agentscore_commerce.identity.{fastapi,flask,django,aiohttp,sanic,middleware}` | Trust gate middleware: KYC, sanctions (account name + signer wallet), age, jurisdiction. `AgentScoreGate(...)` (or `agentscore_gate(app, ...)` on Flask/Sanic), `get_agentscore_data(...)`, `capture_wallet(...)`, `get_signer_verdict(...)`. The gate extracts the payment signer pre-evaluate and passes it to `/v1/assess`, so the API composes both wallet-binding (`signer_match`) and OFAC SDN wallet-address (`signer_sanctions`) verdicts on one round trip. |
| `agentscore_commerce.identity` (package level) | Re-exports the denial helpers: `denial_reason_status`, `denial_reason_to_body`, `build_signer_mismatch_body`, `build_contact_support_next_steps`, `verification_agent_instructions`, `is_fixable_denial`, `FIXABLE_DENIAL_REASONS`. The per-framework adapter modules also expose `get_gate_quota_info(request)` for surfacing X-RateLimit info from gate state. Also re-exports the per-product policy helpers: `PolicyBlock`, `GateResult`, `EnforcementMode`, `IdentityStatus`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed`, `validate_shipping_against_policy` (one-call country+state validator that raises `CheckoutValidationError` with the canonical envelope on miss), for multi-product merchants where each product carries its own compliance config: hard gate vs soft vs none, per-product shipping allowlists. Key + token helpers: `load_ucp_signing_key_from_env` (cached env-driven loader for the UCP signing key, reads `UCP_SIGNING_KEY_JWK_PRIVATE` JSON JWK, detects alg from shape, falls back to ephemeral when unset, sanitizes errors so key bytes never reach logs, concurrent-safe via `threading.Lock`; env-var names and `default_kid` / `default_alg` are overridable as kwargs); `hash_operator_token` (sha256 hex of plaintext `opc_...`, for merchants persisting `operator_token_id` to their own DB without ever storing the plaintext); `extract_owner_scope(headers) -> OwnerScope` (canonical owner-identity extractor for caller-scoped resource queries, reads `X-Wallet-Address` / `X-Operator-Token`, hashes the token so plaintext never leaves the request); `default_read_only_on_denied(reason)` (canonical `on_denied` for read-only resource gates: 401 + `Cache-Control: no-store` while still spreading `denial_reason_to_body`. Returns a `DefaultOnDeniedResult(body, status, headers)`; FastAPI / Flask / aiohttp / Sanic `on_denied` callbacks accept an optional 3-tuple `(body, status, headers)` to carry headers through; wrap with `lambda req, reason: (lambda r: (r.body, r.status, r.headers or {}))(default_read_only_on_denied(reason))`). |
| `agentscore_commerce.payment` | `networks`, `USDC`, `rails` registries; `payment_directive`, `build_payment_directive`, `www_authenticate_header`, `payment_required_header`, `alias_amount_fields` (opt-in v1↔v2 amount-field shim that adds both `amount` and `maxAmountRequired` to an entry. The 402 builders do NOT apply it by default, strict x402 v2 settlement matches the agent's echoed requirement by exact comparison, so an extra field the server's rebuilt requirement lacks breaks settle; use only when you know a client is hardcoded to read `maxAmountRequired`), `settlement_override_header`, `dispatch_settlement_by_network`, `extract_payment_signer` (accepts positional `x402_payment_header` AND/OR `authorization_header=` kwarg; recovers signer from x402 EIP-3009 `payload.authorization.from` OR MPP `Authorization: Payment <base64>` `did:pkh:eip155:<chain>:<addr>` / `did:pkh:solana:<genesis>:<addr>` source DID), `detect_rail_from_headers` (returns `"x402"` / `"mpp"` / `None` from inbound headers), `register_x402_schemes_v1_v2`; drop-in x402 helpers: `validate_x402_network_config` (boot-time guard), `verify_x402_request` (parse + validate inbound X-Payment), `process_x402_settle` (verify-then-settle with one call), `classify_x402_settle_result` (maps the tagged settle result to a recommended HTTP status / code / next_steps so merchants get a controlled envelope without coupling to facilitator-specific error text), `classify_orchestration_error` (same `ClassifiedX402Error` shape but for uncaught exceptions thrown elsewhere in the orchestration; returns `None` for unknown errors so merchants rethrow instead of swallowing); `zero_amount_carve_out` (skip CDP / pympp upstream verify+settle for $0 settles where the upstream rejects value=0 payloads, including credentials signed against a nonzero quote the merchant re-priced to $0 at settle; parses the credential, lifts signer + network, returns a `ZeroSettleResult` shaped identically to the success path so callers branch on rail, not on result shape), `malformed_payment_credential` (wire-shape gate: payment headers that are not base64 JSON and not token-shaped are rejected before merchant hooks run; `Checkout` applies it by default, opt out with `credential_pre_check=False`); `usd_to_atomic` (Decimal-based USD → atomic int, ROUND_HALF_UP, for Tempo / Solana / Base USDC amount construction). |
Expand Down Expand Up @@ -93,7 +93,7 @@ async def purchase(request: Request, assess=Depends(get_agentscore_data)):
return {"ok": True}
```

## Checkout orchestrator (the 2.0 high-level surface)
## Checkout orchestrator

`Checkout` is the canonical merchant surface: one config object, hooks for the merchant-specific pieces, and the SDK handles 402 emit, identity gating, x402 verify+settle, mppx compose, $0 carve-out, and the per-framework adapter. Most merchants reach for `Checkout` first and drop to lower-level helpers only when they need custom flows.

Expand Down Expand Up @@ -315,12 +315,12 @@ from agentscore_commerce.identity import (
ucp_a2a_extension,
)

# Google A2A v1.0 Signed Agent Card. Publish at /.well-known/agent-card.json.
# A2A v1.0 Agent Card. Publish at /.well-known/agent-card.json.
# Per UCP §A2A binding the card MUST declare the canonical UCP extension URI in
# `capabilities.extensions[]`; pass `ucp_a2a_extension()` with empty capabilities
# until you bind formal UCP capabilities (dev.ucp.shopping.checkout, etc.).
# Skills are top-level AgentSkill objects; identity claims live in a separate
# AgentCardSignature (RFC 7515 JWS) wrapping the serialized card.
# Skills are top-level AgentSkill objects. The card is returned unsigned: to sign it,
# compute RFC 7515 JWS entries over the card body and pass them as `signatures`.
card = build_a2a_agent_card(
name="My Service",
description="Buy products via agent payments.",
Expand All @@ -340,7 +340,7 @@ card = build_a2a_agent_card(
# Google Universal Commerce Protocol. Publish at /.well-known/ucp.
# Output shape: {"ucp": {"version", "services", "capabilities",
# "payment_handlers", "name?", "supported_versions?"}, "keys": [...]}
# , services / capabilities / payment_handlers are MAPS keyed by reverse-DNS
# services / capabilities / payment_handlers are MAPS keyed by reverse-DNS
# service / capability / handler name (UCP spec §3 + §6).
profile = build_ucp_profile(
name="My Service",
Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ We will acknowledge receipt within 48 hours and aim to release a fix within 7 da

| Version | Supported |
|---------|-----------|
| 2.x | ✅ |
| < 2.0 | ❌ |
| 3.x | ✅ |
| < 3.0 | ❌ |
4 changes: 2 additions & 2 deletions agentscore_commerce/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Agentic commerce SDK — identity middleware + payment helpers + 402 builders + discovery + Stripe multichain.
"""Agentic commerce SDK: identity middleware + payment helpers + 402 builders + discovery + Stripe multichain.

Submodules:
agentscore_commerce.identity - per-framework gate adapters
Expand Down Expand Up @@ -163,7 +163,7 @@
try:
__version__ = _pkg_version("agentscore-commerce")
except PackageNotFoundError:
# Editable install or pre-build state — fall back to a sentinel so consumers
# Editable install or pre-build state: fall back to a sentinel so consumers
# don't crash on a missing dist-info dir. Real version always comes from
# pyproject.toml at install time.
__version__ = "0.0.0+local"
Expand Down
2 changes: 1 addition & 1 deletion agentscore_commerce/_headers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Internal header helpers — case-normalization for HTTP headers.
"""Internal header helpers: case-normalization for HTTP headers.

Replaces hand-rolled ``{k.lower(): v for k, v in headers.items()}`` loops in
``checkout``, ``signer`` and ``challenge.respond_402``.
Expand Down
6 changes: 3 additions & 3 deletions agentscore_commerce/_mppx_receipt.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ def extract_mppx_receipt_header_from_raw(raw: Any) -> str | None:

Covers three shapes hand-rolled hooks commonly return:

* ``raw.receipt_header`` — pympp's current direct-attribute shape.
* ``raw.to_payment_receipt()`` — pympp's older Receipt return-method shape
* ``raw.receipt_header``: pympp's current direct-attribute shape.
* ``raw.to_payment_receipt()``: pympp's older Receipt return-method shape
(also reached when ``raw`` is a ``(credential, receipt)`` tuple OR a
dict/object carrying ``.receipt``).
* ``raw.with_receipt(response) -> Response`` — a shape that wraps an
* ``raw.with_receipt(response) -> Response``: a shape that wraps an
outgoing Response and attaches the header.

Returns ``None`` when none match or the underlying call raises.
Expand Down
4 changes: 2 additions & 2 deletions agentscore_commerce/_redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
``stripe_multichain.pi_cache``, and ``middleware._core`` so they don't drift
on logging posture, TLS handling, or connect-error semantics.

``redis`` is an optional peer dep — callers pass ``redis_url`` (or rely on
``redis`` is an optional peer dep: callers pass ``redis_url`` (or rely on
``REDIS_URL`` env); when unset or the lazy import fails, this returns ``None``
and the caller falls back to its in-process dict.

Expand Down Expand Up @@ -80,7 +80,7 @@ def memoized_redis(*, url: str | None, label: str) -> Callable[[], Awaitable[Any
First call constructs the client; later calls return the same client
(or the same ``None``).

Pairs with the per-caller ``redis_url`` opt — when ``url`` is ``None`` AND
Pairs with the per-caller ``redis_url`` opt: when ``url`` is ``None`` AND
``REDIS_URL`` is unset, the getter resolves to ``None`` once and remains so
for the lifetime of the caller.
"""
Expand Down
2 changes: 1 addition & 1 deletion agentscore_commerce/_warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def warn_missing_api_key_once(label: str) -> None:
return
_warned_no_api_key = True
logging.getLogger(__name__).warning(
f"[{label}] AGENTSCORE_API_KEY is not set — wallet OFAC SDN sanctions are NOT being enforced. "
f"[{label}] AGENTSCORE_API_KEY is not set: wallet OFAC SDN sanctions are NOT being enforced. "
"Set the env var to enable strict-liability protection on settle."
)

Expand Down
2 changes: 1 addition & 1 deletion agentscore_commerce/aip/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""AIP (Agentic Identity Protocol) — AIT verification (verifier role) + RFC 9421 signing.
"""AIP (Agentic Identity Protocol): AIT verification (verifier role) + RFC 9421 signing.

This package is the AgentScore verifier for Agent Identity Tokens (AITs): a merchant gate
hands a parsed request plus a trusted-issuer :class:`JwksCache` to the orchestrator and gets
Expand Down
26 changes: 13 additions & 13 deletions agentscore_commerce/aip/gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

``verify_ait_request`` is the one call a framework adapter makes: hand it a parsed request
plus a :class:`~agentscore_commerce.aip.jwks.JwksCache`, and it returns the verified AIT claims
or a typed failure. The helpers here also map that failure onto the AIP wire contract — HTTP
status + error code + an RFC 9457 problem-details body — so every adapter renders denials
or a typed failure. The helpers here also map that failure onto the AIP wire contract: HTTP
status + error code + an RFC 9457 problem-details body: so every adapter renders denials
identically.

This layer does identity *verification* only (is this a real, key-bound AIT from a trusted
IdP?). Policy enrichment — sanctions, jurisdiction, cross-merchant graph — happens when the
IdP?). Policy enrichment (sanctions, jurisdiction, cross-merchant graph) happens when the
merchant additionally feeds the verified claims to ``/v1/assess``; that's the gate's choice,
not something this module forces.
"""
Expand Down Expand Up @@ -41,14 +41,14 @@ class AipGateOptions:
jwks: JwksCache
now: float | None = None
max_skew_seconds: float | None = None
# Minimum ``trust_level`` (autonomous < human_present < human_confirmed) the AIT must assert —
# Minimum ``trust_level`` (autonomous < human_present < human_confirmed) the AIT must assert:
# the spec's human-presence gate. Insufficient -> 403 weak_auth with ``required_trust_level``.
# Enforced by :func:`evaluate_aip_request` / :func:`evaluate_aip_parts`. Unset = any trust level.
require_trust_level: TrustLevel | None = None
# Acceptable ``auth.amr`` methods (RFC 8176); the AIT must carry >=1. Insufficient -> 403
# weak_auth with ``required_amr``. Unset = not enforced.
require_amr: list[str] | None = None
# Identity claims the endpoint needs — surfaced as ``required_claims`` on insufficient_claims
# Identity claims the endpoint needs: surfaced as ``required_claims`` on insufficient_claims
# denials so the agent can self-correct. Advisory only (enforce by feeding the verified claims
# to your own policy / ``/v1/assess``; this gate does identity + trust_level/amr).
required_claims: list[str] | None = None
Expand Down Expand Up @@ -104,7 +104,7 @@ def aip_error_code(failure: VerifyAitFailure) -> str:
if failure == "invalid_claims":
return "insufficient_claims"
if failure == "key_unavailable":
# The IdP's JWKS could not be fetched/resolved — our infra couldn't reach a trusted
# The IdP's JWKS could not be fetched/resolved: our infra couldn't reach a trusted
# issuer, not a client-side auth failure. Distinct code so agents back off + retry
# rather than uselessly re-signing.
return "idp_unavailable"
Expand Down Expand Up @@ -173,8 +173,8 @@ def build_aip_error_body(
"""Build an RFC 9457 problem-details body for an AIP verify failure.

Adapters serialize this as ``application/problem+json`` with :func:`aip_error_status`.
Optionally carries the merchant's requirements — ``trusted_issuers`` on untrusted_issuer;
``required_claims`` / ``required_trust_level`` / ``required_amr`` on insufficient_claims —
Optionally carries the merchant's requirements: ``trusted_issuers`` on untrusted_issuer;
``required_claims`` / ``required_trust_level`` / ``required_amr`` on insufficient_claims:
so the agent learns what would satisfy the gate.
"""
code = aip_error_code(failure)
Expand Down Expand Up @@ -206,7 +206,7 @@ def aip_policy_deny_code(code: str) -> tuple[str, int]:
codes; the spec's fixed error set expresses each as:
- ``token_expired`` -> ``expired_token`` (401)
- ``invalid_credential`` -> ``invalid_signature`` (401)
- ``api_error`` -> ``idp_unavailable`` (503, transient — the claims couldn't be evaluated)
- ``api_error`` -> ``idp_unavailable`` (503, transient: the claims couldn't be evaluated)
- everything else (compliance: ``wallet_not_trusted`` + ``sanctions_flagged`` /
``age_insufficient`` / ``jurisdiction_restricted`` / ``kyc_*``) -> ``insufficient_claims``
(403): the AIT did not attest (or attested a failing value for) the required compliance claim.
Expand All @@ -229,8 +229,8 @@ def build_aip_policy_deny_body(
"""Wrap an AgentScore AIT-path denial body in the RFC 9457 + AIP-spec superset.

Reuses :func:`build_aip_error_body`'s SHAPE convention (``type``/``title``/``status``/``detail``
+ escalation extensions) but for the *policy-deny* case — a verified AIT that ``/v1/assess``
then denied — which carries an AgentScore compliance/credential code, not a verify-failure
+ escalation extensions) but for the *policy-deny* case: a verified AIT that ``/v1/assess``
then denied: which carries an AgentScore compliance/credential code, not a verify-failure
reason.

The result is a SUPERSET: the canonical ``{ error, agent_instructions, ... }`` body is spread
Expand Down Expand Up @@ -263,7 +263,7 @@ def build_aip_policy_deny_body(
}
# Escalation extensions, scoped exactly as the spec mandates: ``required_claims`` /
# ``required_trust_level`` / ``required_amr`` on insufficient_claims. ``trusted_issuers``
# belongs to untrusted_issuer — a VERIFY failure that never reaches the policy-deny path — so it
# belongs to untrusted_issuer (a VERIFY failure that never reaches the policy-deny path) so it
# is not emitted here (the edge-deny ``build_aip_error_body`` owns that one).
if requirements is not None and spec_code == "insufficient_claims":
if requirements.required_claims:
Expand All @@ -274,7 +274,7 @@ def build_aip_policy_deny_body(
superset["required_amr"] = requirements.required_amr
# Spread the RFC 9457 envelope LAST so `type` / `title` / `status` / `detail` (and the
# escalation extensions) always win: `body` carries merchant `extra` passthrough fields, and a
# buggy or malicious hook must not clobber the problem+json envelope — or the HTTP status the
# buggy or malicious hook must not clobber the problem+json envelope: or the HTTP status the
# caller derives from it. The rich AgentScore fields (`error`, `agent_instructions`, `reasons`,
# ...) don't collide with the envelope, so they still ride along verbatim.
return {**body, **superset}
Expand Down
Loading
Loading