Skip to content

feat(decisioning): boot-time validator for declared idempotency vs wired @wrap#543

Merged
bokelley merged 2 commits into
mainfrom
bokelley/sdk-asks-r2
May 4, 2026
Merged

feat(decisioning): boot-time validator for declared idempotency vs wired @wrap#543
bokelley merged 2 commits into
mainfrom
bokelley/sdk-asks-r2

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 4, 2026

Summary

Closes the silent-lie configuration where a platform advertises capabilities.adcp.idempotency.supported=True on the AdCP get_adcp_capabilities response but never applies @IdempotencyStore.wrap to any handler method. Buyers reading the capabilities envelope assume retries are deduped; without the decorator, every retry re-executes side effects.

  • IdempotencyStore.wrap registers the wrapper in a private WeakSet (defense-in-depth over a spoofable sentinel attr)
  • validate_idempotency_wiring fires from create_adcp_server_from_platform next to the existing property_list / webhook_sender / capabilities boot gates — AdcpError(recovery="terminal") on misconfig
  • Loose check by design ("any method wrapped"). Tightening to per-method coverage requires the spec to expose a canonical mutating-tool enum, which AdCP #2315 doesn't yet
  • Escape hatch: _adcp_idempotency_external = True on the platform class for adopters with upstream gateway dedup or BYO middleware the SDK can't introspect
  • Error details carry candidate_methods, decorator_import path, and both opt-out flags so coding agents can auto-resolve the misconfig

Test plan

  • 20 new tests in tests/test_validate_idempotency_wiring.py cover: is_wrapped semantics (incl. attr-spoofing rejection, bound-method resolution), capability reader (object-shaped capabilities, missing arms), validator units (no cap / unsupported / wrap-OK / async-but-unwrapped raises / external opt-out / property-with-boot-side-effect doesn't blow up the scan / dynamic bind in __init__), end-to-end through build_asgi_app (lying platform raises, honest platform boots, gateway-dedup platform boots)
  • Adjacent suites green: tests/test_idempotency*.py, tests/test_decisioning_property_list.py, tests/test_decisioning_capabilities_submodule.py, tests/test_testing_decisioning.py (205 tests)
  • Full unit suite green (3827 passed, 0 failures)
  • ruff check + mypy clean on changed files

Reviewed by

Pre-PR review by code-reviewer, ad-tech-protocol-expert, dx-expert agents — all material concerns addressed in the final commit (WeakSet over attr sentinel, dir()+try/except over inspect.getmembers, escape hatch, error details for agents, real-async test coverage instead of lambdas).

🤖 Generated with Claude Code

bokelley and others added 2 commits May 3, 2026 22:12
…red @Wrap

Catches the silent-lie configuration: a platform that advertises
capabilities.adcp.idempotency.supported=True on the AdCP get_adcp_capabilities
response while never applying @IdempotencyStore.wrap to any handler method.
Buyers reading the capabilities envelope assume retries are deduped; without
the decorator every retry re-executes side effects.

* IdempotencyStore.wrap registers the wrapper in a private WeakSet —
  defense-in-depth over a public sentinel attr (which adopters could
  spoof and silently defeat the validator).
* validate_idempotency_wiring fires from create_adcp_server_from_platform
  next to the existing property_list / webhook_sender / capabilities boot
  gates. AdcpError(recovery="terminal") on misconfig.
* Loose check by design: "any method wrapped" passes. Tightening to
  per-method coverage requires the spec to expose a canonical
  mutating-tool enum, which AdCP #2315 doesn't yet.
* Escape hatch: _adcp_idempotency_external = True on the platform class
  for adopters with upstream gateway dedup or BYO middleware the SDK
  can't introspect.
* Error details carry candidate_methods, decorator_import path, and
  both opt-out flags so coding agents can auto-resolve the misconfig.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The new boot-time validator (validate_idempotency_wiring) caught the
multi-platform-seller example advertising IdempotencySupported(supported=True)
on both child platforms and the PlatformRouter union without any
@IdempotencyStore.wrap applied. That's the silent-lie configuration the
validator exists to catch — switch the mocks to IdempotencyUnsupported
to honestly reflect what they implement.

Real adopters with in-memory dedup keep supported=True and wrap their
mutating handlers; this example doesn't model that surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 7657ad4 into main May 4, 2026
15 checks passed
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.

1 participant