feat(signing): default replay store, signed-fetch preset, migration guide#272
feat(signing): default replay store, signed-fetch preset, migration guide#272
Conversation
…uide Three buyer/seller ergonomic upgrades to bring the Python SDK to parity with the TypeScript ergonomics shipped in adcp-client #917: 1. VerifyOptions.replay_store defaults to a fresh InMemoryReplayStore when omitted. Defaulting to None silently disabled replay protection for callers who forgot to wire one — exactly the regression AdCP verifier checklist step 12 exists to prevent. Each VerifyOptions instance gets its own default store via field(default_factory=...); pass replay_store=None explicitly if you genuinely want to bypass the check (uncommon — typically only short-lived integration tests). 2. install_signing_event_hook + signing_operation context manager. Buyer-side preset for adapters that don't use ADCPClient — same shape ADCPClient uses internally, exposed as a public surface so raw-httpx orchestrators get the same auto-sign UX. Supports both static seller_capability and a sync/async capability_provider callable for lazy lookups. 3. docs/request-signing-migration.md mirrors adcp-go's MIGRATION.md shape — bootstrap, staged enforcement (supported_for → warn_for → required_for), key rotation (routine + compromise paths), common pitfalls, pre-enforcement checklist. README signing section gains short pointers to the new preset and the migration guide. Tests: 15 new (5 for VerifyOptions defaults, 10 for the buyer hook). Full suite: 2188 passed (was 2173). ruff clean. mypy clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ync detect `hasattr(result, "__await__")` returns true for `unittest.mock.Mock` because Mock synthesizes any attribute access — a sync Mock returning a RequestSigning would be detected as awaitable and crash on `await`. `inspect.isawaitable` covers coroutines, futures, and any __await__-defining object correctly. Adds tests: - Mock provider regression (catches the hasattr footgun). - Provider returning None skips signing. - covers_content_digest='forbidden' omits content-digest coverage. Tests: 18/18 in the new suite. Full suite: 2191 passed (was 2188). Review: #272 Flagged by python-expert + code-reviewer subagents. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Python 3.11 mypy in CI inferred `capability: RequestSigning` from the first assignment branch (`seller_capability is not None`), then rejected the second branch where `capability = await result` produces `RequestSigning | None`. The local mypy run with implicit-reveal inferred the union and let it through. Annotate `capability: RequestSigning | None` at first reference so the union is the explicit ground truth on both branches. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
a2a-sdk 1.0.2 (released 2026-04-24) calls `field.label` on the
protobuf C-extension FieldDescriptor in `proto_utils.py:217`. Newer
protobuf releases (current 7.34.1) no longer expose `label` on the
upb-backed FieldDescriptor, so every A2A test that exercises
proto_utils fails with:
AttributeError: 'google._upb._message.FieldDescriptor' object has no attribute 'label'
CI started installing 1.0.2 transitively the moment it dropped, which
took #272 red on Python 3.11 (12 A2A tests failing). The signing
work in this PR is unaffected — failures are 100% pre-existing
A2A integration tests broken by the dependency bump.
Pin <1.0.2 with an explanatory comment so the next maintainer can
lift the bound once a2a-sdk ships a fix. This is an unrelated
infrastructure pin bundled into this PR purely to unblock CI;
fine to revert once the upstream lands a release that handles
the new protobuf FieldDescriptor surface.
Verified locally: full 2191-test suite passes with a2a-sdk 1.0.1,
fails on a2a-sdk 1.0.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
CI fix landed: `5ff17696` pins `a2a-sdk<1.0.2` to work around an upstream regression that started breaking CI here (and presumably any other PR opened against `main`) the moment `a2a-sdk 1.0.2` was published. Root cause: `a2a-sdk 1.0.2` (`proto_utils.py:217`) calls `field.label` on the protobuf C-extension `FieldDescriptor`. Newer protobuf releases (current 7.34.1) no longer expose `label` on the upb-backed `FieldDescriptor`, so 12 A2A tests fail with: ``` I confirmed locally — full 2191-test suite passes with `a2a-sdk 1.0.1`, fails with `a2a-sdk 1.0.2`. None of the failing tests touch this PR's signing code. The pin is unrelated to the actual signing work in this PR. Happy to:
Whichever's easier — let me know. |
Summary
Three buyer/seller ergonomic upgrades to bring the Python SDK to parity with the TypeScript ergonomics shipped in adcp-client#917. All three are backwards compatible — existing callers that pass explicit stores or use `ADCPClient(signing=...)` are unaffected.
1. `VerifyOptions.replay_store` defaults to in-memory
`replay_store` was `ReplayStore | None = None`, and the verifier silently skipped the check when `None` — the exact security regression AdCP verifier checklist step 12 exists to prevent. Now defaults to `field(default_factory=InMemoryReplayStore)` so omitting it gives you working replay protection out of the box. Pass `replay_store=None` explicitly if you want to bypass (uncommon — typically only short-lived integration tests).
`revocation_checker` and `revocation_list` stay `None` — most agents don't track revocations at runtime, and the verifier correctly skips when both are absent.
2. `install_signing_event_hook` + `signing_operation` preset
Buyer-side preset for adapters that don't use `ADCPClient`. The high-level client already wires the signing event hook internally — this exposes the same shape as a public API for raw-httpx orchestrators, edge proxies, and anything the high-level client doesn't fit:
```python
import httpx
from adcp.signing import SigningConfig, install_signing_event_hook, signing_operation
client = httpx.AsyncClient()
install_signing_event_hook(
client,
signing=SigningConfig(private_key=..., key_id="my-agent-2026"),
seller_capability=seller_caps.request_signing,
)
async with client:
with signing_operation("create_media_buy"):
resp = await client.post("https://seller.example.com/mcp\", json=payload)
```
Supports both static `seller_capability` and a sync/async `capability_provider` callable for lazy/dynamic lookups.
3. `docs/request-signing-migration.md`
Mirrors the structure of adcp-go's `MIGRATION.md`:
README signing section gains short pointers to the new preset and the migration guide.
Why this PR
Follow-up from the adcp#3064 docs review. After landing the TypeScript ergonomics in adcp-client#917, @benminer and I scoped parallel work for Python and Go so the docs can cite consistent ergonomics across all three SDKs:
Test plan
Migration impact
None. Existing callers:
cc @benminer — this lands the Python half of the cross-SDK ergonomics work we discussed on adcp#3064.
🤖 Generated with Claude Code