Skip to content

fix(decisioning): handler shims for every non-sales wire tool#337

Merged
bokelley merged 2 commits intomainfrom
bokelley/decisioning-handler-shims
May 1, 2026
Merged

fix(decisioning): handler shims for every non-sales wire tool#337
bokelley merged 2 commits intomainfrom
bokelley/decisioning-handler-shims

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 1, 2026

Summary

The Emma AudioStack DX smoke test caught a load-bearing gap. The breadth-sprint (PRs #332#335) shipped 10 Protocol classes + REQUIRED_METHODS coverage at the static layer, but `PlatformHandler` only had 9 sales-* shims. Every non-sales wire tool (`build_creative`, `get_signals`, `check_governance`, `get_brand_identity`, `list_content_standards`, `create_property_list`, `create_collection_list`, ...) was 404 at the wire even though:

  • capabilities advertised the slug
  • `validate_platform` reported green at boot
  • the wire-tool registry at `mcp_tools.py:1262-1320` already mapped each tool name to its Request type

This PR closes the gap: 31 new handler shims, one per wire tool the breadth-sprint Protocols cover.

Wins

  • AudioStack DX regression test pinned by name in the test suite — no silent regression.
  • UNSUPPORTED_FEATURE runtime gate for optional Protocol methods (`preview_creative`, `get_media_buy_artifacts`, `get_creative_features`). Buyers calling an unimplemented optional now get a structured wire error instead of `INTERNAL_ERROR`.
  • advertised_tools drift guard — the test `test_advertised_tools_covers_every_specialism_wire_tool` enforces every spec wire tool we ship has both a shim method AND an advertised entry. Adding a new specialism Protocol without wiring the shim will fail this test.

Test plan

  • 42 new tests in `tests/test_decisioning_handler_shims.py` covering: advertised set drift guard, parametrized shim-existence per tool, end-to-end shim→`_invoke_platform_method`→platform-method for one tool per Protocol family, `sync_audiences` arg-projection, `UNSUPPORTED_FEATURE` surface for optionals, AudioStack DX regression
  • `pytest tests/` — 2308 passed (up from 2266)
  • `mypy src/adcp/decisioning/` clean
  • Pre-commit gates pass

Follow-up

Re-run the Emma AudioStack test after this lands to confirm the friction report verdict moves from 4/10 ("partial fail") toward 8+/10. If the DX is now crazy-easy for creative-generative, the same pattern should hold for the other Protocol families.

Held release pile (now 11 PRs queued for 4.4.0)

🤖 Generated with Claude Code

bokelley and others added 2 commits May 1, 2026 07:04
The Emma DX smoke test (AudioStack creative-generative agent)
caught a load-bearing gap: the breadth-sprint shipped 10 Protocol
classes + REQUIRED_METHODS coverage at the static layer, but
``PlatformHandler`` only had 9 sales-* shims. Every non-sales wire
tool (``build_creative``, ``get_signals``, ``check_governance``,
``get_brand_identity``, ``list_content_standards``,
``create_property_list``, ``create_collection_list``, ...) was 404
at the wire even though capabilities advertised the slug,
``validate_platform`` reported green, and the wire-tool registry at
``mcp_tools.py`` already mapped each tool name to its Request type.

A creative-generative adopter wrapping AudioStack (Emma-style
real-world test) could implement the Protocol shape correctly,
``serve()`` started, ``tools/list`` returned the advertised set —
but ``tools/call name="build_creative"`` got
``Unknown tool: build_creative``. Silent buyer-facing failure with
the framework reporting green at boot.

Fix: 31 new shims following the existing sales-* template, one per
wire tool the breadth-sprint Protocols cover.

Per-Protocol-family advertised tool sets, kept as separate
frozensets for readability and future selective filtering. The
``advertised_tools`` ClassVar is the union of all 9 sets (40 tools
total).

New runtime gate: ``_OPTIONAL_PLATFORM_METHODS`` +
``_require_platform_method`` helper. For methods marked optional on
the per-specialism Protocol (``preview_creative`` on
CreativeBuilderPlatform, ``get_media_buy_artifacts`` /
``get_creative_features`` on ContentStandardsPlatform), the shim
pre-checks ``hasattr(platform, method_name)`` and surfaces
``AdcpError(code='UNSUPPORTED_FEATURE')`` to buyers when the adopter
chose not to wire the method. Without this, AttributeError would
get wrapped to ``INTERNAL_ERROR`` — adopter contract violation, not
buyer-fixable. Required methods are caught at server boot by
``validate_platform``; the optional set complements that gate at
runtime.

Account-resolution: every new shim uses
``getattr(params, "account", None)`` — most non-sales tools don't
carry ``account`` on the wire (catalog reads, governance plans,
brand-rights queries). The ``AccountStore`` impl handles the no-ref
case per its resolution mode (``'derived'`` tolerates None;
``'implicit'`` raises ``AUTH_INVALID``).

Arg-projection: ``sync_audiences`` arg-projects
``audiences=params.audiences`` to match the JS-side adopter
ergonomic (the AudiencePlatform method signature is
``sync_audiences(audiences, ctx)`` per PR #332).

Test coverage in ``tests/test_decisioning_handler_shims.py`` (42
new tests):

* ``advertised_tools`` covers every spec wire tool (drift guard
  against accidentally shipping unrouted advertised tools).
* Parametrized: every advertised non-sales tool has a shim method
  on PlatformHandler.
* End-to-end shim → ``_invoke_platform_method`` → platform method
  for one tool per Protocol family.
* ``sync_audiences`` arg-projection regression test.
* ``UNSUPPORTED_FEATURE`` surface for optional methods.
* AudioStack DX regression test — pinned by name so the Emma fix
  doesn't silently regress.

2308 tests pass (up from 2266).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
P0:
- F12 auto-emit on 12 webhook-eligible shims (activate_signal,
  get_signals, get_creative_delivery, sync_audiences, get_brand_identity,
  get_rights, acquire_rights, all 5 property_list ops). Mirrors the
  sales-* shim pattern. Fires only when (a) request schema allows
  push_notification_config and the buyer registered a URL, and
  (b) method_name is in SPEC_WEBHOOK_TASK_TYPES.
- build_creative bare-manifest / list / envelope projection helper
  (_project_build_creative) — JS-side projectBuildCreativeReturn parity.
  The CreativeBuilderPlatform.build_creative Protocol declares 3 return
  arms (single CreativeManifest, list, fully-shaped success envelope);
  the wire envelope has only 2 success arms ({creative_manifest} vs
  {creative_manifests}). Bare-manifest/list returns now project to wire
  shape instead of failing oneOf validation.
- sync_audiences list arm projection (_project_sync_audiences) — wire
  envelope is {audiences: [...]} but Protocol allows list-of-rows
  ergonomic. Mirrors JS-side wrapping.
- build_creative gated via _require_platform_method so a sales-only
  adopter routing here surfaces UNSUPPORTED_FEATURE instead of leaking
  AttributeError -> INTERNAL_ERROR.

P1:
- update_rights shim (BrandRightsPlatform mutation: extend term, change
  scope, revoke). Added to _BRAND_RIGHTS_ADVERTISED_TOOLS.
- acquire_rights docstring corrected from 3-arm to 4-arm
  (acquired/pending/rejected/error).
- Governance/brand-rights/content-standards shims switched from
  getattr(params, "account", None) to explicit None for the schemas
  that declare additionalProperties:false on the request type — the
  account field is forbidden, getattr was a foot-gun for future drift.
  get_media_buy_artifacts and get_creative_features keep getattr since
  their schemas DO carry account.

Tests:
- _project_build_creative and _project_sync_audiences arm coverage.
- F12 auto-emit fires on get_signals, acquire_rights,
  get_creative_delivery, sync_audiences (with projected envelope).
- update_rights routes through the shim.
- update_rights does NOT auto-emit (not in SPEC_WEBHOOK_TASK_TYPES).
- property_list ops do NOT auto-emit (wire schema forbids
  push_notification_config); regression-pin so a future schema change
  surfaces here.
- build_creative UNSUPPORTED_FEATURE when platform doesn't implement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit a51c5a4 into main May 1, 2026
12 checks passed
@bokelley bokelley deleted the bokelley/decisioning-handler-shims branch May 1, 2026 12:24
bokelley added a commit that referenced this pull request May 1, 2026
…irect P0s) (#338)

* fix(decisioning): wire-path dispatch + F12 silent no-op (Emma sales-direct P0s)

PR #337 merged 31 handler shims that closed the AudioStack 404 issue
but Emma's sales-direct backend test (verdict 2/10) surfaced three
P0s downstream of the shim layer: every MCP ``tools/call`` 500'd with
``'dict' object has no attribute 'account'``, and buyer-registered
push_notification_config.urls were silently dropped when the adopter
didn't wire ``webhook_sender``. Unit tests passed because they bypass
``create_tool_caller`` and call shim methods directly.

handler.py — moved every ``adcp.types`` Request/Response import out of
``if TYPE_CHECKING:`` so ``typing.get_type_hints()`` resolves the
forward refs at runtime. Without this, the dispatcher's typed-params
resolver raises ``NameError``, falls back to dict dispatch, and the
shim's ``params.account`` access crashes.

webhook_emit.py — warn when ``sender=None`` but the buyer registered
``push_notification_config.url``. Adopters who skip ``webhook_sender``
in ``serve()`` now see a WARNING citing the buyer's URL on first
call instead of having buyers complain that their notifications
never arrive.

mcp_tools.py — bumped ``_resolve_params_pydantic_model`` fallback
log from DEBUG to WARNING with explicit remediation guidance
(import the model at module scope vs. declare ``params: dict``).
Silent dispatch fallback is what hid the wire-dispatch break for
two PRs.

tests/test_decisioning_wire_dispatch.py (new, 29 tests) — pins:
- ``typing.get_type_hints`` resolves on every shim
- ``_resolve_params_pydantic_model`` returns the typed Pydantic class
- ``create_tool_caller`` round-trips a wire-shape dict through the
  full dispatch path without the params.account crash

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

* fix(decisioning): expert-review P1s on PR #338 wire-dispatch fix

Three reviewers (code-reviewer, python-expert, adtech-product-expert)
converged on five P1/P2 fixes before merge.

Wire-dispatch test:
- Parametrize over ``PlatformHandler.advertised_tools`` so any new
  shim auto-extends regression coverage. Was 14 hardcoded tools (28
  cases); now 40 (80 cases).
- Pin the typed-dispatch contract end-to-end: assert the platform
  method actually receives a Pydantic ``GetProductsRequest`` /
  ``BuildCreativeRequest``, not a raw dict.
- Drop hardcoded class-name assertions in favor of
  ``issubclass(resolved, BaseModel)``.

Resolver warning (mcp_tools.py):
- Surface the failing class name from ``NameError.name`` directly
  so adopters don't have to parse the bound-method repr.
- Trim historical reference per CLAUDE.md.
- Forward-compat note for PEP 749 (3.14 lazy annotations).

F12 webhook silent-drop (webhook_emit.py):
- Move the ``sender=None`` warning ABOVE the full URL/token
  extraction. A weird ``params`` shape that makes
  ``_extract_push_notification_url_and_token`` raise mid-traversal
  would otherwise re-introduce the silent-drop the previous fix
  was supposed to eliminate. Cheap pre-check on ``getattr(params,
  "push_notification_config", None)`` decides whether to warn; URL
  extraction is best-effort for log context.

Test count: 2832 passed (was 2777 — +55 from wider parametrize
coverage).

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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