Skip to content

Commit fb6193a

Browse files
vvillait88claude
andcommitted
Release 2.4.0: payment correctness + SDK parity + branch coverage
- x402 amount-field aliasing is now opt-in: the 402 builders no longer emit maxAmountRequired by default (strict x402 v2 settle matches the whole accepts object byte-for-byte). alias_amount_fields stays exported for v1-hardcoded clients. - extract_payment_signer tries the x402 header before MPP, matching the precheck path - directive amounts route through usd_to_atomic (Decimal, ROUND_HALF_UP) - build_default_checkout_rails derives tempo-testnet network/chain_id/token - fix solana_mpp accepted-method: x402/exact -> solana/charge (it is an MPP charge rail) - flask rate-limit adapter runs coroutines on a persistent background loop so a Redis client is not stranded on a closed per-call loop (sync WSGI + async-Flask) - remove dead returns after the NoReturn _deny() in the FastAPI gate - fix mppx_stripe dynamic import (mpp.methods.stripe; dist is pympp, import module is mpp) - bump agentscore-py dep -> >=2.4.0; SECURITY 1.x -> 2.x - lift branch coverage to ~96.4%; strip internal/parity references from public source Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f69b080 commit fb6193a

69 files changed

Lines changed: 2444 additions & 351 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pip install 'agentscore-commerce[fastapi,x402,coinbase]'
2727
| `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}`); `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). |
2828
| `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. |
2929
| `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); `has_payment_header` / `has_x402_header` / `has_mppx_header` (request discriminators — any-credential vs x402 vs MPP); `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))`). |
30-
| `agentscore_commerce.payment` | `networks`, `USDC`, `rails` registries; `payment_directive`, `build_payment_directive`, `www_authenticate_header`, `payment_required_header`, `alias_amount_fields` (v1↔v2 amount field shim that emits both `amount` and `maxAmountRequired` so v1-only x402 parsers like Coinbase awal can read v2 bodies), `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; parses the credential, lifts signer + network, returns a `ZeroSettleResult` shaped identically to the success path so callers branch on rail, not on result shape); `usd_to_atomic` (Decimal-based USD → atomic int, ROUND_HALF_UP — for Tempo / Solana / Base USDC amount construction). |
30+
| `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; parses the credential, lifts signer + network, returns a `ZeroSettleResult` shaped identically to the success path so callers branch on rail, not on result shape); `usd_to_atomic` (Decimal-based USD → atomic int, ROUND_HALF_UP — for Tempo / Solana / Base USDC amount construction). |
3131
| `agentscore_commerce.discovery` | `is_discovery_probe_request`, `build_discovery_probe_response` (with optional `x402_sample` for x402-aware crawlers like `awal x402 details`), `sample_x402_accept_for_network` (USDC sample-accept builder for known CAIP-2 networks), `build_well_known_mpp`, `build_llms_txt` + `llms_txt_identity_section` + `llms_txt_payment_section` (compact + verbose modes), `build_skill_md` (Claude-Skill-compatible `/skill.md` agent-discovery manifest; strictly agent-facing data only, no internal posture), `build_redemption_skill_md` (delivery-neutral redemption-code template — printed mailers, emailed codes, API trial credits all covered; `endpoint_path`/`delivery_intro`/`body_shape`/`body_rules`/`extra_recovery_rows` overrides for non-goods shapes), `build_merchant_index_json` (canonical `/` discovery body), `standard_endpoint_descriptions(kind=)` (canonical method+path → description map for goods vs api merchants; optional `include_order_status_route` for goods), `build_success_next_steps` (universal Passport-active success block), `build_agentscore_onboarding_steps` (canonical skill.md onboarding for goods or API merchants), `agentscore_openapi_snippets`, `build_bazaar_discovery_payload`, `NoindexNonDiscoveryMiddleware` (ASGI middleware emitting `X-Robots-Tag: noindex` on every path except the agent-discovery surfaces; pure helpers `is_discovery_path` + `DEFAULT_DISCOVERY_PATHS` for non-ASGI frameworks). Plus the UCP/JWKS publish surface: `build_signed_ucp_response`, `build_signed_jwks_response`, `well_known_preflight_response`, `default_a2a_services`, `bootstrap_ucp_signing_key`, framework-neutral `SignedDiscoveryResponse` + per-framework wrappers `signed_response_{fastapi,flask,django,aiohttp,sanic}`. |
3232
| `agentscore_commerce.challenge` | `build_402_body`, `build_accepted_methods`, `build_identity_metadata` (auto-attached by `Checkout` when an inbound `X-Wallet-Address` header is present), `build_how_to_pay`, `build_agent_instructions` (auto-emits per-rail `compatible_clients`: smoke-verified CLIs the agent should use; vendor override supported; pure helper `compatible_clients_by_rails(rails)` returns the same map for vendors building custom 402s), `build_pricing_block` (cents to dollar-string with optional shipping/tax), `first_encounter_agent_memory` (cross-merchant hint, returns the canonical block or `None` based on a per-merchant first-seen flag), `Receipt` + `ReceiptNextSteps` + `ProductInfo` + `ShippingAddress` (canonical 200-receipt dataclasses — universal across goods + API merchants); `respond_402`, a drop-in 402 emit that preserves pympp's `WWW-Authenticate` and layers x402's `PAYMENT-REQUIRED`. `build_validation_error`: structured 4xx body builder (`{error: {code, message}, required_fields?, example_body?, next_steps?, ...extra}`) so vendors compose body shapes by name instead of inlining at every validation site. |
3333
| `agentscore_commerce.stripe_multichain` | `create_multichain_payment_intent` (returns `MultichainPaymentIntentResult(payment_intent_id, deposit_addresses)`; read `result.deposit_addresses[network]` directly), `create_pay_to_address_from_stripe_pi(authorization_header=, amount_cents=, stripe=, pi_cache=, networks=, static_recipients=, metadata=, order_id=, preferred_network=)` — per-order payTo resolver: on the settle leg, reuses the buyer's signed-against payTo from the MPP credential (after `pi_cache.has_address` check OR a `static_recipients` match — the static address is always-accepted because the merchant owns it); on the discovery leg, mints a fresh PI for the rails NOT covered by `static_recipients`, caches the merged map, registers static addresses with `pi_cache.cache_address` so verify-leg lookups pass. `mint_multichain_recipients(...same kwargs)` — structured variant returning `MintMultichainRecipientsResult(recipients, payment_intent_id, reused_from_credential)` for the full per-rail map (typical multi-rail merchant hook). Use `static_recipients={"solana": "<wallet>"}` for low-margin endpoints where Solana per-call ATA rent (~$0.50 against MPP spec §13.6) dominates revenue — the SDK skips Stripe minting on that network, reuses the static recipient forever, and (with a one-time external pre-funding of the recipient's USDC ATA) lets every settle pay only the ~$0.001 per-tx fee. `SolanaMppRailSpec.ata_creation_required` defaults to `True` (data-only; solana method registration through `create_mppx_server` is a follow-up — merchants building the solana method directly via `pympp` should pass the flag themselves to the charge factory). `simulate_crypto_deposit`; `create_pi_cache` (TTL'd PI / deposit-address cache, Redis-backed when `redis_url` set, in-memory otherwise), `simulate_deposit_if_test_mode` (gates on `sk_test_` and looks up the PI for you), `STRIPE_TEST_TX_HASH_SUCCESS` / `STRIPE_TEST_TX_HASH_FAILED` constants. Peer dep on `stripe`. |

SECURITY.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@ We will acknowledge receipt within 48 hours and aim to release a fix within 7 da
1919

2020
| Version | Supported |
2121
|---------|-----------|
22-
| 1.x ||
23-
| < 1.0 ||
22+
| 2.x ||
23+
| < 2.0 ||

agentscore_commerce/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
PricingResult,
2626
SettleOutcome,
2727
format_pydantic_errors,
28+
get_identity_status,
2829
pricing_result,
2930
validation_envelope,
3031
validation_response_aiohttp,
@@ -51,8 +52,8 @@
5152
from agentscore_commerce.errors import CheckoutValidationError
5253

5354
# Re-export the most commonly used helpers at the package root so consumers
54-
# don't have to remember which submodule each one lives in. Mirrors node's
55-
# top-level `index.ts` surface; submodule imports still work for power users.
55+
# don't have to remember which submodule each one lives in; submodule imports
56+
# still work for power users.
5657
from agentscore_commerce.identity import (
5758
A2A_DEFAULT_TRANSPORT,
5859
A2A_PROTOCOL_VERSION,
@@ -244,6 +245,7 @@
244245
"format_pydantic_errors",
245246
"format_usd_cents",
246247
"generate_ucp_signing_key",
248+
"get_identity_status",
247249
"has_mppx_header",
248250
"has_payment_header",
249251
"has_x402_header",

agentscore_commerce/_headers.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
"""Internal header helpers — case-normalization for HTTP headers.
22
33
Replaces hand-rolled ``{k.lower(): v for k, v in headers.items()}`` loops in
4-
``checkout``, ``signer`` and ``challenge.respond_402``. Mirrors node-commerce
5-
``src/_headers.ts``.
4+
``checkout``, ``signer`` and ``challenge.respond_402``.
65
76
Not part of the public API; consumed by SDK internals only.
87
"""

agentscore_commerce/_mppx_receipt.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Shared by ``Checkout.handle_mppx`` and ``compute_first_checkout``'s MPP
44
settle path so the rail-label / signer derivation stays one source of truth.
55
6-
Mirrors node-commerce ``src/_mppx_receipt.ts``. Not part of the public API.
6+
Not part of the public API.
77
"""
88

99
from __future__ import annotations
@@ -23,8 +23,8 @@ def extract_mppx_receipt_header_from_raw(raw: Any) -> str | None:
2323
* ``raw.to_payment_receipt()`` — pympp's older Receipt return-method shape
2424
(also reached when ``raw`` is a ``(credential, receipt)`` tuple OR a
2525
dict/object carrying ``.receipt``).
26-
* ``raw.with_receipt(response) -> Response`` — node-compat shape that
27-
wraps an outgoing Response and attaches the header.
26+
* ``raw.with_receipt(response) -> Response`` — a shape that wraps an
27+
outgoing Response and attaches the header.
2828
2929
Returns ``None`` when none match or the underlying call raises.
3030
"""
@@ -75,8 +75,8 @@ def extract_mppx_receipt_method(header: str) -> str | None:
7575
7676
The returned method is ``'tempo'`` / ``'solana'`` / ``'stripe'``, or the
7777
legacy ``'<scheme>/charge'`` form. Returns ``None`` when the header is
78-
malformed or mppx isn't importable. Uses ``Receipt.from_payment_receipt``
79-
(Python pympp) — equivalent to node's ``Receipt.deserialize``.
78+
malformed or mppx isn't importable. Uses pympp's
79+
``Receipt.from_payment_receipt``.
8080
"""
8181
try:
8282
from mpp import Receipt # type: ignore[import-untyped]

agentscore_commerce/_redis.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
``REDIS_URL`` env); when unset or the lazy import fails, this returns ``None``
99
and the caller falls back to its in-process dict.
1010
11-
Mirrors node-commerce ``src/_redis.ts``. Not part of the public API.
11+
Not part of the public API.
1212
"""
1313

1414
from __future__ import annotations
@@ -49,7 +49,7 @@ async def _try_create_redis(
4949
- the import / construction raises for any other reason
5050
5151
``rediss://`` URLs auto-enable TLS via ``redis.asyncio.from_url``.
52-
Matches the node sibling's connect-timeout and retry-cap (3s, 1 retry).
52+
Uses a 3s connect-timeout and a 1-retry cap.
5353
"""
5454
resolved = url if url is not None else os.environ.get("REDIS_URL")
5555
if not resolved:
@@ -80,10 +80,9 @@ def memoized_redis(*, url: str | None, label: str) -> Callable[[], Awaitable[Any
8080
First call constructs the client; later calls return the same client
8181
(or the same ``None``).
8282
83-
Mirrors node's ``memoizedRedis`` closure pattern. Pairs with the per-caller
84-
``redis_url`` opt — when ``url`` is ``None`` AND ``REDIS_URL`` is unset, the
85-
getter resolves to ``None`` once and remains so for the lifetime of the
86-
caller.
83+
Pairs with the per-caller ``redis_url`` opt — when ``url`` is ``None`` AND
84+
``REDIS_URL`` is unset, the getter resolves to ``None`` once and remains so
85+
for the lifetime of the caller.
8786
"""
8887
client: Any | None = None
8988
attempted = False

agentscore_commerce/_warnings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
Module-level state ensures each warning fires at most once per process,
44
regardless of how many ``Checkout`` / ``compute_first_checkout`` instances
5-
trigger it. Mirrors ``node-commerce/src/_warnings.ts``.
5+
trigger it.
66
"""
77

88
from __future__ import annotations

agentscore_commerce/challenge/accepted_methods.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ async def build_accepted_methods(
5353
if solana_mpp is not None:
5454
out.append(
5555
{
56-
"method": "x402/exact",
56+
"method": "solana/charge",
5757
"network": solana_mpp.network,
5858
"token": solana_mpp.token,
5959
"symbol": solana_mpp.symbol,

agentscore_commerce/challenge/body.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from typing import Any, Literal
55

66
from agentscore_commerce.challenge.pricing import PricingBlock
7-
from agentscore_commerce.payment.wwwauthenticate import alias_amount_fields
87

98

109
@dataclass
@@ -37,7 +36,11 @@ def build_402_body(
3736
body: dict[str, Any] = {"payment_required": True, "accepted_methods": accepted_methods}
3837
if x402:
3938
body["x402Version"] = x402.version
40-
body["accepts"] = alias_amount_fields(x402.accepts)
39+
# No v1<->v2 amount alias: strict x402 v2 settlement matches the echoed
40+
# requirement against the rebuilt one by exact comparison, so an extra
41+
# maxAmountRequired the rebuild lacks silently fails settle. Keep accepts
42+
# identical to build_payment_requirements output.
43+
body["accepts"] = x402.accepts
4144
if x402.extensions:
4245
body["extensions"] = x402.extensions
4346
if amount_usd is not None:

agentscore_commerce/challenge/how_to_pay.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ async def build_how_to_pay(
6666
``op_token_placeholder`` defaults to ``"<your_opc_token>"``. Pass ``None``
6767
(gateless merchants) to strip the ``-H 'X-Operator-Token: ...'`` snippet
6868
from every rail command — appropriate when the merchant doesn't run an
69-
identity gate. The wallet OFAC SDN default (TEC-311) does NOT need an
69+
identity gate. The always-on wallet OFAC SDN default does NOT need an
7070
operator token, so gateless merchants emit cleaner commands.
7171
"""
7272
total_num = float(total_usd) if isinstance(total_usd, str) else total_usd

0 commit comments

Comments
 (0)