feat(decisioning): comprehensive Emma DX follow-up (P0/P1/P2 + examples)#339
Merged
feat(decisioning): comprehensive Emma DX follow-up (P0/P1/P2 + examples)#339
Conversation
…ng P1) Three independent Emma backend tests (sales-direct, AudioStack/creative, signals-marketplace, Stability AI/creative) all flagged the same bug: ``tools/list`` advertises 40+ tools regardless of the platform's claimed specialisms. A sales-only adopter saw ``acquire_rights``, ``build_creative``, ``check_governance``; on every call buyers got NOT_SUPPORTED. The override-detection filter (``_is_method_overridden``) walks ``PlatformHandler.__mro__`` and finds the class concretely defines all 40+ shims — every tool shows as "implemented" regardless of what the underlying platform claims. Fix: add ``advertised_tools_for_instance(self) -> frozenset[str]`` on PlatformHandler. The framework's ``get_tools_for_handler`` checks for this hook on instances and intersects the candidate set with the per-instance result BEFORE the override-detection filter. The hook maps each claimed specialism to its per-Protocol-family advertised set via ``SPECIALISM_TO_ADVERTISED_TOOLS``. Empty per-instance set (novel specialism slug not in the map) falls back to the class-level universe — muting the handler entirely on a forward-compat slug would be worse than over-advertising. Static inspection by class also keeps the full universe so storyboard tests and spec-conformance docs aren't disrupted. Tests: 9 new (drift guards, per-specialism leak guards for sales/signals/creative/hybrid, novel-specialism fallback, advertise_all interaction, class-level inspection). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… P2)
Adopters debugging "Error executing tool X: AdcpError[INTERNAL_ERROR /
terminal]: An internal error occurred" had no wire-side breadcrumb —
they had to grep server logs to even see which exception class fired.
The Emma AudioStack backend test (verdict 6/10) explicitly flagged
this: "the wire side An internal error occurred is a dead end."
Add ``details.caused_by`` to the wire envelope when the framework
wraps a non-AdcpError exception:
{
"code": "INTERNAL_ERROR",
"message": "Platform method 'build_creative' raised AttributeError; see details for cause",
"recovery": "terminal",
"details": {
"caused_by": {
"type": "AttributeError",
"message": "'dict' object has no attribute 'message'"
}
}
}
Exposes class name + truncated str (200 char cap) — no traceback, no
module path, no chained __cause__. Full repr stays in server logs via
``logger.exception``. Truncation is defense-in-depth against an
adopter who throws on secret material with a sloppy repr; the cap
prevents secret-shaped values from landing on the wire.
Applied to all three INTERNAL_ERROR wrap sites (sync method,
non-projected TypeError, handoff fn). Drift guard: a unit test
verifies the truncation cap matches the constant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adopters claiming any specialism whose tool surface includes a spec-eligible webhook task type (``create_media_buy``, ``activate_signal``, ``acquire_rights``, etc.) but who skip ``webhook_sender`` would have every buyer-registered ``push_notification_config.url`` silently dropped. PR #338 added a runtime WARNING on first call; this commit adds the boot-time fail-fast that adtech-product-expert called for — "the same posture as ``validate_platform``'s governance opt-in gate." ``adcp.decisioning.serve.create_adcp_server_from_platform`` now calls ``validate_webhook_sender_for_platform`` after the handler is constructed. Uses the per-instance advertised set (NOT the class-level universe), so test fixtures with no claimed specialisms — and discovery-only agents — don't accidentally trip the gate. Adopter remediation paths surfaced in the error: * Wire a configured ``WebhookSender``. * Or set ``auto_emit_completion_webhooks=False`` if handling webhooks manually. Tests: 4 new gate behaviors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every Emma backend test (sales 2/10, AudioStack 6/10, Signals 8/10, Stability 5/10) flagged "no example for my specialism" as P1 friction. Adopters writing creative/signals/audience/governance/etc. agents had only ``hello_seller.py`` (sales-non-guaranteed) plus the Protocol's 170-line docstring — adtech-product-expert called this the "highest-leverage follow-up after tools/list." Eight new example files, one per non-sales Protocol family: * ``hello_seller_creative.py`` — CreativeBuilderPlatform. Bare CreativeManifest projection, AudioStack/Stability shape. * ``hello_seller_signals.py`` — SignalsPlatform. Catalog + sync activate + TaskHandoff template. * ``hello_seller_audience.py`` — AudiencePlatform. Demonstrates arg-projection ergonomics. * ``hello_seller_governance.py`` — CampaignGovernancePlatform. governance_aware=True opt-in + 4 required methods. * ``hello_seller_brand_rights.py`` — BrandRightsPlatform. 4-arm acquire_rights discriminated union. * ``hello_seller_content_standards.py`` — ContentStandardsPlatform. 6 required + optional UNSUPPORTED_FEATURE gating. * ``hello_seller_property_lists.py`` — PropertyListsPlatform. In-memory CRUD + fetch-token + security-critical delete. * ``hello_seller_collection_lists.py`` — CollectionListsPlatform. Mirror of property-lists for collections. Each example fits in <100 lines, runs standalone, uses canonical type names (``CreativeManifest``, ``AudioContent``, ``FormatReferenceStructuredObject``). The ``AudioContent`` callout in the creative example documents the v4.0 payload/slot naming split that Emma's AudioStack adopter tripped on. Tests: 8 new — boot each example via PlatformHandler and verify ``advertised_tools_for_instance()`` narrows to the specialism's tools without leaking to other Protocol families. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-reviewer + adtech-product-expert second-pass on PR #339. Three P0/P1 findings + the deferred port-collision and /mcp redirect work folded into the comprehensive bundle. P0 fix (code-reviewer): - All 10 hello_seller_*.py examples called ``serve()`` without ``webhook_sender``. The new A3 boot-gate (this PR) rejects them because their advertised tools are in SPEC_WEBHOOK_TASK_TYPES. Run instructions in each example crashed at boot. Fix: pass ``auto_emit_completion_webhooks=False`` with a comment teaching adopters where to wire ``webhook_sender=`` in production. New smoke test ``test_example_boots_via_create_adcp_server_from_platform`` catches this regression class going forward. P1 fix (adtech-product-expert): - Boot-time webhook gate raised ``ValueError``; now raises ``AdcpError(INVALID_REQUEST)`` for parity with ``validate_platform``'s sibling boot-time gates (governance opt-in, missing required methods). Adopter ``except AdcpError`` clauses catch all platform-config failures uniformly. ``details.missing`` + ``details.webhook_eligible_tools`` for programmatic remediation. Deferred → folded in: - **Port-3001 EADDRINUSE friendly remediation** (2-of-4 Emma reports). ``_bind_reusable_socket`` projects EADDRINUSE OSError to a remediation-bearing message citing the busy port + ``port=`` / ``ADCP_PORT`` knobs. Other OSErrors (perm denied, address-not-avail) pass through unchanged so adopters debugging a different problem don't get a misleading port-collision message. - **/mcp vs /mcp/ 307 redirect** (2-of-4 Emma reports). New ASGI middleware ``_wrap_with_path_normalize`` strips trailing slashes before dispatch. Buyer libs POSTing to ``/mcp/`` now route to the same handler as ``/mcp`` without the 307 (which silently broke libs that don't follow redirects on POST — they revert to GET on the redirected URL, losing the body). Root path ``/`` left alone to avoid health-check 404. Scope-copy semantics preserved so outer middlewares aren't affected. Tests: 2868 pass (was 2854). 6 new (2 port-collision, 4 path-normalize) + 8 new example-boot smoke tests. F12 gate test updated to assert ``AdcpError("INVALID_REQUEST")`` + structured ``details``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Addresses the cross-cutting findings from four Emma backend tests
(sales-direct 2/10, AudioStack 6/10, Stability AI 5/10, Signals 8/10).
Sequenced per adtech-product-expert's recommendation: framework
fixes first, examples last, every commit independently bisectable.
Tier A — Framework P0/P1
A1: Per-specialism
tools/listfilter (3 of 4 reports flagged this — biggest DX issue)PlatformHandler.advertised_tools_for_instance()intersects the universe of shim coverage with the platform's claimed specialisms viaSPECIALISM_TO_ADVERTISED_TOOLS.get_tools_for_handlercalls the hook on instances; class-level inspection preserves full universe.warnings.warn(novel)forward-compat semantic.A2:
INTERNAL_ERRORwire breadcrumb (Emma AudioStack P2)details.caused_by = {type, message}on the wire envelope when the framework wraps a non-AdcpError exception. Class name + truncated str (200 char cap). No traceback, no module path, no chained__cause__.A3: Boot-time webhook_sender fail-fast (Emma F12 P1)
SPEC_WEBHOOK_TASK_TYPEStool ANDwebhook_sender=NoneAND auto_emit on, fail atserve()boot. Same posture asvalidate_platform's governance opt-in gate.Tier C — Examples (Tier B was a no-op once
*Assetcollision risk surfaced)8 new per-Protocol-family templates: creative / signals / audience / governance / brand_rights / content_standards / property_lists / collection_lists. Each <100 lines, runs standalone via
serve(), uses canonical type names. The creative example calls out the v4.0*Contentvs*Assetrename that AudioStack/Stability tripped on.8 new smoke tests boot each example via
PlatformHandlerand verify the per-specialism filter narrows correctly.Deferred to follow-up PRs
Per adtech-product-expert's bundle-order recommendation, separable concerns:
_bind_reusable_socket)/mcpvs/mcp/307 redirect handling*Assetlegacy re-exports were attempted then reverted —test_asset_aliases_stable.pyis enforcing a deliberate v4.0 design (the*Assetnames collided with*FormatAssetslot types). Right call: keep the canonical*Contentnames, document them in the creative example'sAudioContentcallout.Test plan
test_asset_aliases_stable.pystill passing (no*Assetre-exports added)🤖 Generated with Claude Code