Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/adcp/decisioning/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions tests/test_decisioning_serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading