Skip to content

feat: 2.1.0 — A2A v1.0 canonical wire format + tests - #52

Merged
vvillait88 merged 8 commits into
mainfrom
commerce/2.1.0
May 18, 2026
Merged

feat: 2.1.0 — A2A v1.0 canonical wire format + tests#52
vvillait88 merged 8 commits into
mainfrom
commerce/2.1.0

Conversation

@vvillait88

Copy link
Copy Markdown
Contributor

Summary

Align our A2A AgentCard builder with the canonical wire format from @a2a-js/sdk@0.3.13 (the TypeScript reference SDK). This is the only behavior change in 2.1.0; the rest of the train shipped in 2.0.x.

A2A wire format rewrite

Python attribute names stay snake_case (PEP 8); to_dict() emits camelCase keys to match the canonical A2A wire format.

  • Hoisted url, protocol_version, preferred_transport to top-level (were per-interface)
  • Demoted supported_interfaces → optional additional_interfaces[] (only set for multi-binding agents)
  • Dropped per-interface protocol_version and non-canonical tenant
  • Moved extended_agent_card (capability) → top-level supports_authenticated_extended_card
  • Added state_transition_history capability + per-skill security
  • Renamed security_requirementssecurity (typed list[dict[str, list[str]]])
  • All wire JSON keys are camelCase
  • New constants: A2A_PROTOCOL_VERSION, A2A_DEFAULT_TRANSPORT

Permanent regression guards

  • New tests/test_a2a_jws_roundtrip.py exercises sign + verify + tamper-detection using joserfc EdDSA.
  • Existing tests/test_a2a.py rewritten with an 18-key forbidden-snake-case list scanned against the serialized output.

Note on PyPI a2a-sdk

a2a-sdk@1.0.3 (Google's Python A2A package, protobuf-derived) ships an OLDER schema (supportedInterfaces[] + securityRequirements, no top-level url/protocolVersion). We align to the newer TypeScript SDK schema which mirrors https://a2a-protocol.org/latest/. The node-commerce sibling pins @a2a-js/sdk@0.3.13 as a compile-time type guard.

Test plan

  • uv run ruff check (clean)
  • uv run ty check (clean)
  • uv run vulture (clean)
  • uv run pytest (1380 passing, 95.06% coverage)
  • Live validation: core/store booted via TestClient, served /.well-known/agent-card.json validated structurally against the canonical AgentCard shape — zero drift
  • CI green on PR

🤖 Generated with Claude Code

vvillait88 and others added 5 commits May 18, 2026 10:40
New helpers:
- extract_owner_scope + OwnerScope dataclass (identity.tokens)
- default_read_only_on_denied (identity.default_denied)
- create_pay_to_address_from_stripe_pi (stripe_multichain)
- has_x402_header / has_mppx_header (payment.payment_header)

Behavior:
- SolanaMppRailSpec.__post_init__ derives token (mint) from network
  (devnet CAIP-2 or raw 'devnet' -> devnet USDC mint). Mirrors
  X402BaseRailSpec.__post_init__. Explicit overrides still win.
- AgentScoreGate.on_denied (FastAPI / Flask / aiohttp / Sanic) now
  accepts an optional 3-tuple (body, status, headers). Lets
  default_read_only_on_denied thread Cache-Control: no-store through
  to the response. Legacy 2-tuple (body, status) still works.

Surface:
- Adapter __all__ lists (fastapi / flask / django / aiohttp / sanic /
  middleware) trimmed to framework-specific surface only. Shared helpers
  (denial_reason_to_body, FIXABLE_DENIAL_REASONS, build_*_body, etc.)
  import from canonical top-level: agentscore_commerce.
- identity/__init__.py drops cross-package re-export of has_payment_header.

Bug fixes:
- Inner-SDK: checkout.py + checkout_compute_first.py drop private
  _has_x402_header/_has_mppx_header; import from payment.payment_header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hoist url/protocol_version/preferred_transport to top-level (camelCase
on the wire), demote supported_interfaces to optional additional_interfaces,
drop per-interface protocol_version, drop tenant, move extended_agent_card
to top-level supports_authenticated_extended_card, add state_transition_history
capability and per-skill security, rename security_requirements to security.
Python attribute names stay snake_case; to_dict() emits camelCase to match
the canonical wire format. Validated against @a2a-js/sdk@0.3.13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign + verify + tamper-detection using joserfc EdDSA against the unsigned
card body. Covers the signing contract end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace inert `# type: ignore[misc]` markers on the 3-tuple destructure
  branches in fastapi/flask/sanic/aiohttp on-denied wiring with explicit
  cast() narrowing — ty's len() check isn't a recognized type guard
- Drop unused `retry_on_error_max_attempts` kwarg from `_try_create_redis`
- Whitelist `seconds` Protocol method param in `_RedisLike` (structural
  typing for `redis.asyncio.Redis.expire`)

ty + vulture + pytest all clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread agentscore_commerce/checkout_compute_first.py Fixed
Comment thread agentscore_commerce/middleware/_core.py
Comment thread agentscore_commerce/middleware/_core.py
Comment thread vulture_whitelist.py Fixed
Comment thread tests/test_quote_cache_redis.py Fixed
Comment thread agentscore_commerce/middleware/flask.py Fixed
vvillait88 and others added 3 commits May 18, 2026 12:37
…kout

Mirror the node-commerce fix. CodeQL js/stack-trace-exposure flagged the
`detail: str(exc)` field in the upstream_failed wire response — merchant
errors may carry stack traces; the merchant's own logger is the right
channel.

Also add an explanatory comment on the empty `except RuntimeError` in the
Flask rate-limit middleware's `_run_async` helper (per github-code-quality
nudge): the except is intentional fallthrough to asyncio.run when no loop
is current on the calling thread.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…re_names

The whitelist file relied on bare-identifier statements (vulture's
documented whitelist syntax). github-code-quality flagged each bare name
as "statement has no effect" — accurate from a static-analysis perspective,
since vulture matches `Name` AST nodes that look identical to dead code
to other tools. Move the same name list into `[tool.vulture] ignore_names`
in pyproject.toml; drop the whitelist file and its lefthook argument.

Also collapse the mixed import style in
tests/test_quote_cache_redis.py::test_quote_cache_in_memory_evicts_expired_entries
(per the other github-code-quality nudge): use `import time` directly and
patch `time.monotonic` — the quote_cache module re-exports `time` so a
single identity is patched in both modules.

vulture / ruff / ty / pytest (1380) all clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the lefthook + pyproject.toml change in 1492c17. The ignore list
now lives in [tool.vulture] ignore_names; the workflow only needs to scan
the package directory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vvillait88
vvillait88 merged commit 024e1ae into main May 18, 2026
7 checks passed
@vvillait88
vvillait88 deleted the commerce/2.1.0 branch May 18, 2026 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants