From d5ca4201f2d77e6b00926985d4e07e5c36e84fc6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 4 May 2026 01:17:47 +0000 Subject: [PATCH] feat(decisioning): surface advertise_all=False on create_adcp_server_from_platform Adopters using the standalone build-the-handler path saw ~40+ entries in handler.advertised_tools after create_adcp_server_from_platform() because the ClassVar holds the full union across all PlatformHandler shims. This adds advertise_all=False (matching serve()'s default) and filters the instance-level advertised_tools to the platform's claimed specialisms, consistent with the existing advertised_tools_for_instance() hook already used by get_tools_for_handler. Existing serve() callers get the same filtering applied to handler.advertised_tools for consistency. https://claude.ai/code/session_01VYgrLhaR7HWfBf3VxbMn5K --- src/adcp/decisioning/serve.py | 24 ++++++++++++++++ tests/test_decisioning_serve.py | 49 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/adcp/decisioning/serve.py b/src/adcp/decisioning/serve.py index b32503a10..57207729c 100644 --- a/src/adcp/decisioning/serve.py +++ b/src/adcp/decisioning/serve.py @@ -87,6 +87,7 @@ def create_adcp_server_from_platform( buyer_agent_registry: BuyerAgentRegistry | None = None, config_store: ProductConfigStore | None = None, property_list_fetcher: PropertyListFetcher | None = None, + advertise_all: bool = False, ) -> tuple[PlatformHandler, ThreadPoolExecutor, TaskRegistry]: """Build the :class:`PlatformHandler` + supporting wiring from a :class:`DecisioningPlatform`. @@ -176,6 +177,14 @@ def create_adcp_server_from_platform( (avoid duplicate delivery; idempotency-key dedup at the receiver would handle it but explicit suppression matches the v5 manual-emit posture for adopters mid-migration). + :param advertise_all: When ``False`` (default), ``handler.advertised_tools`` + is filtered to the tools covered by the platform's claimed + specialisms — matching :func:`serve`'s default. When ``True``, + the full spec surface is retained on ``handler.advertised_tools`` + (useful for spec-compliance storyboards). Either way, the + per-instance specialism filter and the override-detection filter + still apply inside :func:`~adcp.server.mcp_tools.get_tools_for_handler` + when the handler is wired into an MCP/A2A server. To wire a :class:`ProposalManager` (v1 two-platform composition), pass it on a :class:`PlatformRouter` via @@ -331,6 +340,20 @@ def create_adcp_server_from_platform( validate_capabilities_response_shape(handler) + # Filter handler.advertised_tools to the platform's claimed specialisms + # when advertise_all=False (the default). The ClassVar holds the full + # union of all shim-covered tools (~40+); adopters using the standalone + # build-the-handler path saw all of them when accessing + # handler.advertised_tools directly, instead of the per-specialism + # subset that serve() already projects through + # get_tools_for_handler → advertised_tools_for_instance(). Override at + # the instance level so the ClassVar (and the _HANDLER_TOOLS registry + # populated at class-definition time) are unaffected. + if not advertise_all: + per_instance = set(handler.advertised_tools_for_instance()) + if per_instance: + handler.advertised_tools = per_instance # type: ignore[misc] + return handler, executor, registry @@ -428,6 +451,7 @@ def serve( buyer_agent_registry=buyer_agent_registry, config_store=config_store, property_list_fetcher=property_list_fetcher, + advertise_all=advertise_all, ) # Phase 1 sandbox-authority — wire the comply controller's account diff --git a/tests/test_decisioning_serve.py b/tests/test_decisioning_serve.py index c68f773ea..6200cc61f 100644 --- a/tests/test_decisioning_serve.py +++ b/tests/test_decisioning_serve.py @@ -504,3 +504,52 @@ def test_serve_does_not_fire_gate_for_platform_without_webhook_eligible_tools() handler, executor, _ = create_adcp_server_from_platform(platform) assert handler._webhook_sender is None executor.shutdown(wait=True) + + +# ---- Issue #519: advertise_all=False filters handler.advertised_tools ---- + + +def test_create_default_filters_advertised_tools_to_specialism() -> None: + """handler.advertised_tools with default advertise_all=False returns + only the tools for the platform's claimed specialisms, not the full + ~40-tool union across all PlatformHandler shims.""" + from adcp.decisioning.handler import _SALES_ADVERTISED_TOOLS + + platform = _SalesPlatformWithRequiredMethods() + # Disable auto-emit so the webhook-sender gate doesn't fire — this + # test focuses on advertised_tools filtering, not F12 validation. + handler, executor, _ = create_adcp_server_from_platform( + platform, auto_emit_completion_webhooks=False + ) + executor.shutdown(wait=True) + assert handler.advertised_tools == set(_SALES_ADVERTISED_TOOLS) + assert "build_creative" not in handler.advertised_tools + assert "activate_signal" not in handler.advertised_tools + assert "sync_audiences" not in handler.advertised_tools + + +def test_create_advertise_all_true_retains_full_tool_surface() -> None: + """advertise_all=True keeps handler.advertised_tools as the full + class-level union — spec-compliance storyboards use this escape hatch.""" + from adcp.decisioning.handler import PlatformHandler + + platform = _SalesPlatformWithRequiredMethods() + handler, executor, _ = create_adcp_server_from_platform( + platform, advertise_all=True, auto_emit_completion_webhooks=False + ) + executor.shutdown(wait=True) + assert handler.advertised_tools == PlatformHandler.advertised_tools + assert "build_creative" in handler.advertised_tools + assert "activate_signal" in handler.advertised_tools + + +def test_create_bare_platform_no_specialism_leaves_classlevel_intact() -> None: + """When advertised_tools_for_instance() returns empty (no recognized + specialism), the fallback leaves the ClassVar intact so the handler + is not accidentally muted.""" + from adcp.decisioning.handler import PlatformHandler + + platform = _BarePlatform() + handler, executor, _ = create_adcp_server_from_platform(platform) + executor.shutdown(wait=True) + assert handler.advertised_tools == PlatformHandler.advertised_tools