Skip to content

Commit a1c65ef

Browse files
authored
docs: correct A2A signing docs and supported versions; drop em-dashes (#152)
## Summary Doc corrections from an audit of every repo's docs against code, plus an em-dash sweep. - **SECURITY.md** listed 2.x as the supported line; the package is on 3.x. - **README A2A example** called the card a "Signed Agent Card" whose identity claims live in a separate JWS wrapping it. `build_a2a_agent_card` returns the card unsigned, and an A2A 1.0 card carries its JWS entries in its own `signatures` field. The comment now says that. - **A2A module docstring** said to "wrap" the card in a signature and called the spec "Google A2A"; it now says to attach the JWS entries as `signatures`. - **"the 2.0 high-level surface"** removed from the README and CLAUDE.md (version-introduction text, and the package is on 3.x). - **CLAUDE.md** `/api` row now lists the four names `agentscore_commerce.api` re-exports, not one. - **examples/README.md**: the compute-first example uses `compute_first_checkout`, which is backed by `create_quote_cache`; it does not import the cache itself. - A broken `# ,` comment in the README UCP example is fixed. - **Em-dashes** removed everywhere except the one test that asserts `skill.md` output contains none. A spaced em-dash became a colon; dashes used as a pair became parentheses. This touches agent-facing strings (denial instructions, `skill.md` text), which ship in the next release; no release is cut for this alone. ## Type of change - [ ] Bug fix (no breaking change) - [ ] New feature (no breaking change) - [ ] Breaking change (existing callers must update) - [x] Docs, tests, or internal maintenance only ## Public API None. No exported name, signature or value changed. String content in agent-facing text changed punctuation only. ## Test plan `ruff check`, `ruff format --check`, `ty check` and `pytest` (1892 passed, 4 skipped) all exit 0. Repo-wide em-dash count afterwards: 1, the assertion in `tests/test_skill_md.py`. The sweep kept `# noqa` codes parseable and the Sphinx `#:` attribute comments intact. ## Checklist - [x] Tests cover the new behavior, and the suite passes locally (no behavior changed; suite passes) - [x] Lint, format, and type checks pass - [x] Docs and README examples updated if the public surface changed - [x] No secrets, credentials, or personal data in the diff or the tests
1 parent a6d0cb4 commit a1c65ef

137 files changed

Lines changed: 634 additions & 634 deletions

File tree

Some content is hidden

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

‎CLAUDE.md‎

Lines changed: 11 additions & 11 deletions
Large diffs are not rendered by default.

‎README.md‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pip install 'agentscore-commerce[fastapi,x402,coinbase]'
2424

2525
| Submodule | What it provides |
2626
|---|---|
27-
| `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). |
27+
| `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). |
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); `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))`). |
3030
| `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). |
@@ -93,7 +93,7 @@ async def purchase(request: Request, assess=Depends(get_agentscore_data)):
9393
return {"ok": True}
9494
```
9595

96-
## Checkout orchestrator (the 2.0 high-level surface)
96+
## Checkout orchestrator
9797

9898
`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.
9999

@@ -315,12 +315,12 @@ from agentscore_commerce.identity import (
315315
ucp_a2a_extension,
316316
)
317317

318-
# Google A2A v1.0 Signed Agent Card. Publish at /.well-known/agent-card.json.
318+
# A2A v1.0 Agent Card. Publish at /.well-known/agent-card.json.
319319
# Per UCP §A2A binding the card MUST declare the canonical UCP extension URI in
320320
# `capabilities.extensions[]`; pass `ucp_a2a_extension()` with empty capabilities
321321
# until you bind formal UCP capabilities (dev.ucp.shopping.checkout, etc.).
322-
# Skills are top-level AgentSkill objects; identity claims live in a separate
323-
# AgentCardSignature (RFC 7515 JWS) wrapping the serialized card.
322+
# Skills are top-level AgentSkill objects. The card is returned unsigned: to sign it,
323+
# compute RFC 7515 JWS entries over the card body and pass them as `signatures`.
324324
card = build_a2a_agent_card(
325325
name="My Service",
326326
description="Buy products via agent payments.",
@@ -340,7 +340,7 @@ card = build_a2a_agent_card(
340340
# Google Universal Commerce Protocol. Publish at /.well-known/ucp.
341341
# Output shape: {"ucp": {"version", "services", "capabilities",
342342
# "payment_handlers", "name?", "supported_versions?"}, "keys": [...]}
343-
# , services / capabilities / payment_handlers are MAPS keyed by reverse-DNS
343+
# services / capabilities / payment_handlers are MAPS keyed by reverse-DNS
344344
# service / capability / handler name (UCP spec §3 + §6).
345345
profile = build_ucp_profile(
346346
name="My Service",

‎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-
| 2.x | ✅ |
23-
| < 2.0 | ❌ |
22+
| 3.x | ✅ |
23+
| < 3.0 | ❌ |

‎agentscore_commerce/__init__.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Agentic commerce SDK — identity middleware + payment helpers + 402 builders + discovery + Stripe multichain.
1+
"""Agentic commerce SDK: identity middleware + payment helpers + 402 builders + discovery + Stripe multichain.
22
33
Submodules:
44
agentscore_commerce.identity - per-framework gate adapters
@@ -163,7 +163,7 @@
163163
try:
164164
__version__ = _pkg_version("agentscore-commerce")
165165
except PackageNotFoundError:
166-
# Editable install or pre-build state — fall back to a sentinel so consumers
166+
# Editable install or pre-build state: fall back to a sentinel so consumers
167167
# don't crash on a missing dist-info dir. Real version always comes from
168168
# pyproject.toml at install time.
169169
__version__ = "0.0.0+local"

‎agentscore_commerce/_headers.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Internal header helpers — case-normalization for HTTP headers.
1+
"""Internal header helpers: case-normalization for HTTP headers.
22
33
Replaces hand-rolled ``{k.lower(): v for k, v in headers.items()}`` loops in
44
``checkout``, ``signer`` and ``challenge.respond_402``.

‎agentscore_commerce/_mppx_receipt.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ def extract_mppx_receipt_header_from_raw(raw: Any) -> str | None:
1919
2020
Covers three shapes hand-rolled hooks commonly return:
2121
22-
* ``raw.receipt_header`` — pympp's current direct-attribute shape.
23-
* ``raw.to_payment_receipt()`` — pympp's older Receipt return-method shape
22+
* ``raw.receipt_header``: pympp's current direct-attribute shape.
23+
* ``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`` — a shape that wraps an
26+
* ``raw.with_receipt(response) -> Response``: a shape that wraps an
2727
outgoing Response and attaches the header.
2828
2929
Returns ``None`` when none match or the underlying call raises.

‎agentscore_commerce/_redis.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
``stripe_multichain.pi_cache``, and ``middleware._core`` so they don't drift
55
on logging posture, TLS handling, or connect-error semantics.
66
7-
``redis`` is an optional peer dep — callers pass ``redis_url`` (or rely on
7+
``redis`` is an optional peer dep: callers pass ``redis_url`` (or rely on
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
@@ -80,7 +80,7 @@ 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-
Pairs with the per-caller ``redis_url`` opt — when ``url`` is ``None`` AND
83+
Pairs with the per-caller ``redis_url`` opt: when ``url`` is ``None`` AND
8484
``REDIS_URL`` is unset, the getter resolves to ``None`` once and remains so
8585
for the lifetime of the caller.
8686
"""

‎agentscore_commerce/_warnings.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def warn_missing_api_key_once(label: str) -> None:
2828
return
2929
_warned_no_api_key = True
3030
logging.getLogger(__name__).warning(
31-
f"[{label}] AGENTSCORE_API_KEY is not set — wallet OFAC SDN sanctions are NOT being enforced. "
31+
f"[{label}] AGENTSCORE_API_KEY is not set: wallet OFAC SDN sanctions are NOT being enforced. "
3232
"Set the env var to enable strict-liability protection on settle."
3333
)
3434

‎agentscore_commerce/aip/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""AIP (Agentic Identity Protocol) — AIT verification (verifier role) + RFC 9421 signing.
1+
"""AIP (Agentic Identity Protocol): AIT verification (verifier role) + RFC 9421 signing.
22
33
This package is the AgentScore verifier for Agent Identity Tokens (AITs): a merchant gate
44
hands a parsed request plus a trusted-issuer :class:`JwksCache` to the orchestrator and gets

‎agentscore_commerce/aip/gate.py‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
33
``verify_ait_request`` is the one call a framework adapter makes: hand it a parsed request
44
plus a :class:`~agentscore_commerce.aip.jwks.JwksCache`, and it returns the verified AIT claims
5-
or a typed failure. The helpers here also map that failure onto the AIP wire contract — HTTP
6-
status + error code + an RFC 9457 problem-details body — so every adapter renders denials
5+
or a typed failure. The helpers here also map that failure onto the AIP wire contract: HTTP
6+
status + error code + an RFC 9457 problem-details body: so every adapter renders denials
77
identically.
88
99
This layer does identity *verification* only (is this a real, key-bound AIT from a trusted
10-
IdP?). Policy enrichment — sanctions, jurisdiction, cross-merchant graph — happens when the
10+
IdP?). Policy enrichment (sanctions, jurisdiction, cross-merchant graph) happens when the
1111
merchant additionally feeds the verified claims to ``/v1/assess``; that's the gate's choice,
1212
not something this module forces.
1313
"""
@@ -41,14 +41,14 @@ class AipGateOptions:
4141
jwks: JwksCache
4242
now: float | None = None
4343
max_skew_seconds: float | None = None
44-
# Minimum ``trust_level`` (autonomous < human_present < human_confirmed) the AIT must assert —
44+
# Minimum ``trust_level`` (autonomous < human_present < human_confirmed) the AIT must assert:
4545
# the spec's human-presence gate. Insufficient -> 403 weak_auth with ``required_trust_level``.
4646
# Enforced by :func:`evaluate_aip_request` / :func:`evaluate_aip_parts`. Unset = any trust level.
4747
require_trust_level: TrustLevel | None = None
4848
# Acceptable ``auth.amr`` methods (RFC 8176); the AIT must carry >=1. Insufficient -> 403
4949
# weak_auth with ``required_amr``. Unset = not enforced.
5050
require_amr: list[str] | None = None
51-
# Identity claims the endpoint needs — surfaced as ``required_claims`` on insufficient_claims
51+
# Identity claims the endpoint needs: surfaced as ``required_claims`` on insufficient_claims
5252
# denials so the agent can self-correct. Advisory only (enforce by feeding the verified claims
5353
# to your own policy / ``/v1/assess``; this gate does identity + trust_level/amr).
5454
required_claims: list[str] | None = None
@@ -104,7 +104,7 @@ def aip_error_code(failure: VerifyAitFailure) -> str:
104104
if failure == "invalid_claims":
105105
return "insufficient_claims"
106106
if failure == "key_unavailable":
107-
# The IdP's JWKS could not be fetched/resolved — our infra couldn't reach a trusted
107+
# The IdP's JWKS could not be fetched/resolved: our infra couldn't reach a trusted
108108
# issuer, not a client-side auth failure. Distinct code so agents back off + retry
109109
# rather than uselessly re-signing.
110110
return "idp_unavailable"
@@ -173,8 +173,8 @@ def build_aip_error_body(
173173
"""Build an RFC 9457 problem-details body for an AIP verify failure.
174174
175175
Adapters serialize this as ``application/problem+json`` with :func:`aip_error_status`.
176-
Optionally carries the merchant's requirements — ``trusted_issuers`` on untrusted_issuer;
177-
``required_claims`` / ``required_trust_level`` / ``required_amr`` on insufficient_claims —
176+
Optionally carries the merchant's requirements: ``trusted_issuers`` on untrusted_issuer;
177+
``required_claims`` / ``required_trust_level`` / ``required_amr`` on insufficient_claims:
178178
so the agent learns what would satisfy the gate.
179179
"""
180180
code = aip_error_code(failure)
@@ -206,7 +206,7 @@ def aip_policy_deny_code(code: str) -> tuple[str, int]:
206206
codes; the spec's fixed error set expresses each as:
207207
- ``token_expired`` -> ``expired_token`` (401)
208208
- ``invalid_credential`` -> ``invalid_signature`` (401)
209-
- ``api_error`` -> ``idp_unavailable`` (503, transient — the claims couldn't be evaluated)
209+
- ``api_error`` -> ``idp_unavailable`` (503, transient: the claims couldn't be evaluated)
210210
- everything else (compliance: ``wallet_not_trusted`` + ``sanctions_flagged`` /
211211
``age_insufficient`` / ``jurisdiction_restricted`` / ``kyc_*``) -> ``insufficient_claims``
212212
(403): the AIT did not attest (or attested a failing value for) the required compliance claim.
@@ -229,8 +229,8 @@ def build_aip_policy_deny_body(
229229
"""Wrap an AgentScore AIT-path denial body in the RFC 9457 + AIP-spec superset.
230230
231231
Reuses :func:`build_aip_error_body`'s SHAPE convention (``type``/``title``/``status``/``detail``
232-
+ escalation extensions) but for the *policy-deny* case — a verified AIT that ``/v1/assess``
233-
then denied — which carries an AgentScore compliance/credential code, not a verify-failure
232+
+ escalation extensions) but for the *policy-deny* case: a verified AIT that ``/v1/assess``
233+
then denied: which carries an AgentScore compliance/credential code, not a verify-failure
234234
reason.
235235
236236
The result is a SUPERSET: the canonical ``{ error, agent_instructions, ... }`` body is spread
@@ -263,7 +263,7 @@ def build_aip_policy_deny_body(
263263
}
264264
# Escalation extensions, scoped exactly as the spec mandates: ``required_claims`` /
265265
# ``required_trust_level`` / ``required_amr`` on insufficient_claims. ``trusted_issuers``
266-
# belongs to untrusted_issuer — a VERIFY failure that never reaches the policy-deny path — so it
266+
# belongs to untrusted_issuer (a VERIFY failure that never reaches the policy-deny path) so it
267267
# is not emitted here (the edge-deny ``build_aip_error_body`` owns that one).
268268
if requirements is not None and spec_code == "insufficient_claims":
269269
if requirements.required_claims:
@@ -274,7 +274,7 @@ def build_aip_policy_deny_body(
274274
superset["required_amr"] = requirements.required_amr
275275
# Spread the RFC 9457 envelope LAST so `type` / `title` / `status` / `detail` (and the
276276
# escalation extensions) always win: `body` carries merchant `extra` passthrough fields, and a
277-
# buggy or malicious hook must not clobber the problem+json envelope — or the HTTP status the
277+
# buggy or malicious hook must not clobber the problem+json envelope: or the HTTP status the
278278
# caller derives from it. The rich AgentScore fields (`error`, `agent_instructions`, `reasons`,
279279
# ...) don't collide with the envelope, so they still ride along verbatim.
280280
return {**body, **superset}

0 commit comments

Comments
 (0)