Skip to content

Commit 1d3be96

Browse files
committed
fix: allow owned signals discovery without activation
1 parent 1ad6df9 commit 1d3be96

13 files changed

Lines changed: 222 additions & 49 deletions

examples/hello_seller_signals.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Hello-seller-signals — minimal SignalsPlatform adopter.
22
3-
The smallest possible ``signal-marketplace`` (or ``signal-owned``)
4-
seller. Two methods: ``get_signals`` for catalog discovery and
5-
``activate_signal`` for provisioning to destination platforms.
3+
The smallest possible ``signal-marketplace`` seller. Two methods:
4+
``get_signals`` for catalog discovery and ``activate_signal`` for
5+
provisioning to destination platforms. ``signal-owned`` sellers can be
6+
discovery-only when their owned signals are already usable on seller
7+
inventory.
68
79
This is the template for signal-marketplace adopters (LiveRamp,
810
Nielsen, 1P data providers).

schemas/cache/3.0/manifest.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020
"response_schema": "signals/activate-signal-response.json",
2121
"async_response_schemas": [],
2222
"specialisms": [
23-
"signal_marketplace",
24-
"signal_owned"
23+
"signal_marketplace"
2524
]
2625
},
2726
"build_creative": {
@@ -1181,10 +1180,9 @@
11811180
"get_signals"
11821181
],
11831182
"exercised_tools": [
1184-
"activate_signal",
11851183
"get_adcp_capabilities",
11861184
"get_signals"
11871185
]
11881186
}
11891187
}
1190-
}
1188+
}

src/adcp/decisioning/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ def create_media_buy(
191191
ContentStandardsPlatform,
192192
CreativeAdServerPlatform,
193193
CreativeBuilderPlatform,
194+
OwnedSignalsPlatform,
194195
PropertyListsPlatform,
195196
SalesPlatform,
196197
SignalsPlatform,
@@ -358,6 +359,7 @@ def __init__(self, *args: object, **kwargs: object) -> None:
358359
"MockProposalManager",
359360
"NoAuth",
360361
"OAuthCredential",
362+
"OwnedSignalsPlatform",
361363
"PermissionDeniedError",
362364
"PgProposalStore",
363365
"PgTaskRegistry",

src/adcp/decisioning/dispatch.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,9 @@
206206
"sync_catalogs",
207207
}
208208
),
209-
# Signals specialisms — third-party data brokers and first-party
210-
# data providers share the same SignalsPlatform Protocol surface.
209+
# Signals specialisms. Marketplace/provisioned signals require
210+
# activation onto destinations; seller-owned signals are already
211+
# usable on that seller's inventory, so discovery is sufficient.
211212
"signal-marketplace": frozenset(
212213
{
213214
"get_signals",
@@ -217,7 +218,6 @@
217218
"signal-owned": frozenset(
218219
{
219220
"get_signals",
220-
"activate_signal",
221221
}
222222
),
223223
# Audience-sync — first-party CRM audience push with delta upsert.

src/adcp/decisioning/handler.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,11 @@
267267
"activate_signal",
268268
}
269269
)
270+
_OWNED_SIGNALS_ADVERTISED_TOOLS: frozenset[str] = frozenset(
271+
{
272+
"get_signals",
273+
}
274+
)
270275
_AUDIENCE_ADVERTISED_TOOLS: frozenset[str] = frozenset(
271276
{
272277
"sync_audiences",
@@ -337,6 +342,9 @@
337342
# ContentStandardsPlatform optional analyzer reads
338343
"get_media_buy_artifacts",
339344
"get_creative_features",
345+
# signal-owned platforms expose discovery-only owned signals;
346+
# activate_signal remains required for signal-marketplace.
347+
"activate_signal",
340348
# AudiencePlatform adopter-internal helper (not wire-served, but
341349
# listed here for symmetry should a future shim wire it)
342350
"poll_audience_statuses",
@@ -375,9 +383,11 @@
375383
"creative-generative": _CREATIVE_ADVERTISED_TOOLS,
376384
"creative-template": _CREATIVE_ADVERTISED_TOOLS,
377385
"creative-ad-server": _CREATIVE_ADVERTISED_TOOLS,
378-
# Signals — marketplace + owned share the same wire surface.
386+
# Signals — marketplace/provisioned signals need activation;
387+
# seller-owned signals are discovery-only because they are already
388+
# usable on that seller's inventory.
379389
"signal-marketplace": _SIGNALS_ADVERTISED_TOOLS,
380-
"signal-owned": _SIGNALS_ADVERTISED_TOOLS,
390+
"signal-owned": _OWNED_SIGNALS_ADVERTISED_TOOLS,
381391
# Audience.
382392
"audience-sync": _AUDIENCE_ADVERTISED_TOOLS,
383393
# Governance — spend-authority + delivery-monitor share the
@@ -2261,6 +2271,7 @@ async def activate_signal( # type: ignore[override]
22612271
context: ToolContext | None = None,
22622272
) -> ActivateSignalSuccessResponse:
22632273
"""Provision a signal onto destination platforms."""
2274+
self._require_platform_method("activate_signal")
22642275
tool_ctx = context or ToolContext()
22652276
account = await self._resolve_account(getattr(params, "account", None), tool_ctx)
22662277
ctx = self._build_ctx(tool_ctx, account)

src/adcp/decisioning/platform.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ class DecisioningCapabilities:
116116
117117
:param specialisms: AdCP specialism slugs the platform claims —
118118
e.g. ``['sales-non-guaranteed', 'sales-broadcast-tv']``,
119-
``['audience-sync']``, ``['signal-marketplace',
120-
'signal-owned']``. Each maps to a ``Protocol`` class under
119+
``['audience-sync']``, ``['signal-marketplace']``, or
120+
``['signal-owned']``. Each maps to a ``Protocol`` class under
121121
:mod:`adcp.decisioning.specialisms`. Drives method-conformance
122122
validation at boot AND projects to the wire ``specialisms``
123123
field.

src/adcp/decisioning/platform_router.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@
7575
----------------------
7676
7777
At construction time the router walks the specialism Protocol classes
78-
(``SalesPlatform``, ``AudiencePlatform``, ``SignalsPlatform``, etc.)
78+
(``SalesPlatform``, ``AudiencePlatform``, ``SignalsPlatform``,
79+
``OwnedSignalsPlatform``, etc.)
7980
declared in :mod:`adcp.decisioning.specialisms` and synthesizes a
8081
delegating method for each method any child platform implements. New
8182
specialism Protocols added to the SDK are picked up automatically — no
@@ -113,6 +114,7 @@
113114
ContentStandardsPlatform,
114115
CreativeAdServerPlatform,
115116
CreativeBuilderPlatform,
117+
OwnedSignalsPlatform,
116118
PropertyListsPlatform,
117119
SalesPlatform,
118120
SignalsPlatform,
@@ -135,6 +137,7 @@
135137
# to enumerate them.
136138
_KNOWN_SPECIALISM_PROTOCOLS: tuple[type, ...] = (
137139
SalesPlatform,
140+
OwnedSignalsPlatform,
138141
SignalsPlatform,
139142
AudiencePlatform,
140143
CreativeBuilderPlatform,
@@ -165,7 +168,9 @@ def _protocol_method_names(proto: type) -> frozenset[str]:
165168
Annotation-only attributes (``foo: int``) are NOT picked up because
166169
no Protocol in :mod:`adcp.decisioning.specialisms` declares
167170
attribute-only members; if that changes, broaden the walk to
168-
``__annotations__`` too.
171+
``__annotations__`` too. Direct Protocol inheritance does not need
172+
special handling here because inherited method providers are listed
173+
separately in :data:`_KNOWN_SPECIALISM_PROTOCOLS`.
169174
"""
170175
declared: set[str] = set()
171176
for name, value in vars(proto).items():

src/adcp/decisioning/specialisms/__init__.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212
* :class:`SalesPlatform` — covers the spec ``sales-*`` slugs
1313
(non-guaranteed, guaranteed, broadcast-tv, social, proposal-mode,
1414
catalog-driven) under one unified hybrid shape.
15-
* :class:`SignalsPlatform` — covers ``signal-marketplace`` +
16-
``signal-owned``. Two methods: ``get_signals`` (catalog discovery)
17-
and ``activate_signal`` (provisioning onto destination platforms).
15+
* :class:`SignalsPlatform` — covers ``signal-marketplace``. Two
16+
methods: ``get_signals`` (catalog discovery) and ``activate_signal``
17+
(provisioning onto destination platforms).
18+
* :class:`OwnedSignalsPlatform` — covers ``signal-owned``. One method:
19+
``get_signals`` (publisher-owned signal catalog discovery; signals
20+
are already usable on seller inventory).
1821
* :class:`AudiencePlatform` — covers ``audience-sync``. Two methods:
1922
``sync_audiences`` (push first-party CRM audiences with delta
2023
upsert) and ``poll_audience_statuses`` (batch state read).
@@ -71,7 +74,7 @@
7174
PropertyListsPlatform,
7275
)
7376
from adcp.decisioning.specialisms.sales import SalesPlatform
74-
from adcp.decisioning.specialisms.signals import SignalsPlatform
77+
from adcp.decisioning.specialisms.signals import OwnedSignalsPlatform, SignalsPlatform
7578

7679
__all__ = [
7780
"AudiencePlatform",
@@ -81,6 +84,7 @@
8184
"ContentStandardsPlatform",
8285
"CreativeAdServerPlatform",
8386
"CreativeBuilderPlatform",
87+
"OwnedSignalsPlatform",
8488
"PropertyListsPlatform",
8589
"SalesPlatform",
8690
"SignalsPlatform",

src/adcp/decisioning/specialisms/signals.py

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
"""SignalsPlatform Protocol — covers ``signal-marketplace`` + ``signal-owned``.
1+
"""SignalsPlatform Protocols for marketplace and owned signals.
22
3-
A platform claiming either ``signal-marketplace`` (third-party data
4-
brokers — LiveRamp, Oracle Data Cloud, third-party DMPs) or
5-
``signal-owned`` (first-party data providers — publisher first-party
6-
data, retailer customer-graph) implements the methods on this Protocol.
7-
The slugs mirror ``schemas/cache/enums/specialism.json``.
3+
``signal-marketplace`` covers marketplace/provisioned signals that need
4+
buyer-triggered destination activation. ``signal-owned`` covers
5+
publisher-owned first-party signals that are already usable on that
6+
seller inventory and only need catalog discovery. The slugs mirror
7+
``schemas/cache/enums/specialism.json``.
88
9-
Two methods:
9+
Common method:
1010
1111
* :meth:`get_signals` — sync catalog discovery
12+
13+
Marketplace-only method:
14+
1215
* :meth:`activate_signal` — sync provisioning onto destination platforms
1316
1417
Async story: ``activate_signal`` is sync at the wire level — its
@@ -19,7 +22,7 @@
1922
``ctx.publish_status_change(resource_type='signal', ...)`` events as
2023
each deployment reaches ``activating`` / ``deployed`` / ``failed``.
2124
22-
Mirrors the JS-side ``SignalsPlatform`` interface at
25+
Mirrors the JS-side signals interfaces at
2326
``src/lib/server/decisioning/specialisms/signals.ts``.
2427
"""
2528

@@ -41,14 +44,15 @@
4144

4245
#: Per-platform metadata generic; matches ``RequestContext[TMeta]`` and
4346
#: ``Account[TMeta]`` upstream so a platform parameterizing
44-
#: ``SignalsPlatform[TenantMeta]`` gets ``ctx.account.metadata``-style
45-
#: typed access inside method bodies.
47+
#: ``SignalsPlatform[TenantMeta]`` or
48+
#: ``OwnedSignalsPlatform[TenantMeta]`` gets
49+
#: ``ctx.account.metadata``-style typed access inside method bodies.
4650
TMeta = TypeVar("TMeta", default=dict[str, Any])
4751

4852

4953
@runtime_checkable
50-
class SignalsPlatform(Protocol, Generic[TMeta]):
51-
"""Catalog discovery + activation for marketplace / owned signals.
54+
class OwnedSignalsPlatform(Protocol, Generic[TMeta]):
55+
"""Catalog discovery for seller-owned first-party signals.
5256
5357
Methods may be sync (return ``T`` directly) or async (return
5458
``Awaitable[T]``); the dispatch adapter detects via
@@ -81,6 +85,17 @@ def get_signals(
8185
"""
8286
...
8387

88+
89+
@runtime_checkable
90+
class SignalsPlatform(OwnedSignalsPlatform[TMeta], Protocol, Generic[TMeta]):
91+
"""Catalog discovery + activation for marketplace/provisioned signals.
92+
93+
Use this Protocol for ``signal-marketplace``. Use
94+
:class:`OwnedSignalsPlatform` for ``signal-owned`` platforms where
95+
returned signals are already usable in later media-buy targeting and
96+
there is no buyer-triggered destination provisioning step.
97+
"""
98+
8499
def activate_signal(
85100
self,
86101
req: ActivateSignalRequest,
@@ -111,4 +126,4 @@ def activate_signal(
111126
...
112127

113128

114-
__all__ = ["SignalsPlatform"]
129+
__all__ = ["OwnedSignalsPlatform", "SignalsPlatform"]

tests/test_decisioning_advertised_per_specialism.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,14 @@ def activate_signal(self, req, ctx):
101101
return {}
102102

103103

104+
class _OwnedSignalsOnlyPlatform(DecisioningPlatform):
105+
capabilities = DecisioningCapabilities(specialisms=["signal-owned"])
106+
accounts = SingletonAccounts(account_id="owned-signals-only")
107+
108+
def get_signals(self, req, ctx):
109+
return {"signals": []}
110+
111+
104112
class _CreativeOnlyPlatform(DecisioningPlatform):
105113
capabilities = DecisioningCapabilities(specialisms=["creative-generative"])
106114
accounts = SingletonAccounts(account_id="creative-only")
@@ -168,6 +176,28 @@ def test_signals_only_does_not_advertise_sales_tools(executor) -> None:
168176
assert not leaked, f"signals-only leaked: {sorted(leaked)}"
169177

170178

179+
def test_owned_signals_only_advertises_discovery_not_activation(executor) -> None:
180+
handler = PlatformHandler(
181+
_OwnedSignalsOnlyPlatform(),
182+
executor=executor,
183+
registry=InMemoryTaskRegistry(),
184+
)
185+
tools = {tool["name"] for tool in get_tools_for_handler(handler)}
186+
187+
assert "get_signals" in tools
188+
assert "activate_signal" not in tools
189+
190+
forbidden = {
191+
"get_products",
192+
"create_media_buy",
193+
"build_creative",
194+
"acquire_rights",
195+
"check_governance",
196+
}
197+
leaked = forbidden & tools
198+
assert not leaked, f"owned-signals-only leaked: {sorted(leaked)}"
199+
200+
171201
def test_creative_only_does_not_advertise_sales_or_signals_tools(executor) -> None:
172202
"""Mirror test for the creative path — AudioStack/Stability AI shape."""
173203
handler = PlatformHandler(

0 commit comments

Comments
 (0)