Commit 9d29754
feat(decisioning): F12 — auto-emit completion webhook on sync mutating responses (#331)
* feat(decisioning): F12 — auto-emit completion webhook on sync mutating responses
Sync ``create_media_buy``, ``update_media_buy``, ``sync_creatives``
responses now auto-fire a completion webhook when the buyer supplied
``push_notification_config.url``. Previously only the HITL
TaskHandoff path emitted, so sync responses left buyers polling.
Mirrors the JS-side ``emitSyncCompletionWebhook`` implementation
(commits ``8dc427f9`` and ``7a887dfa`` on
``src/lib/server/decisioning/runtime/from-platform.ts``). Wire-format
is identical: ``task_type``, ``status: 'completed'``, ``result``
field carrying the projected sync response, echoed ``token`` via
``X-AdCP-Push-Token`` header. ``task_id`` is synthesized as
``f"sync-{uuid4()}"`` since sync responses don't allocate a registry
task; buyers correlate via the resource ids embedded in ``result``.
New module ``adcp.decisioning.webhook_emit``:
* ``SPEC_WEBHOOK_TASK_TYPES`` — closed 20-value set mirroring the
on-disk spec enum at ``schemas/cache/enums/task-type.json``. The
``test_spec_webhook_task_types_matches_schema_cache`` test pins
the constant so out-of-band drift surfaces in CI.
* ``maybe_emit_sync_completion`` — fire-and-forget gate. Skips when
disabled, no sender wired, no push URL on the request, or the
tool isn't in the spec enum (logged warning so adopters notice
they extended the surface beyond spec).
* ``_BACKGROUND_WEBHOOK_TASKS`` — module-level strong-ref pin so the
asyncio loop's weak-ref behavior doesn't garbage-collect in-flight
emissions mid-flight. Mirrors the same pattern in
``dispatch._BACKGROUND_HANDOFF_TASKS``.
**Fire-and-forget posture (DoS defense).** Webhook delivery runs in a
background asyncio task; the sync response returns inline
immediately. A buyer-supplied slowloris webhook URL must not be able
to hold the seller's request worker for the full retry budget — the
JS round-2 fix at ``7a887dfa`` documented this DoS vector and Python
preserves the same posture from the start.
**TaskHandoff path doesn't double-fire.** The
``_maybe_auto_emit_sync_completion`` helper detects the projected
Submitted envelope (``status == 'submitted'`` shape) and skips
delivery. The HITL path's registry completion emits its own webhook
on terminal state.
Configuration on ``create_adcp_server_from_platform`` and ``serve``:
* ``webhook_sender: WebhookSender | None = None`` — BYO emitter.
``None`` silently disables auto-emit.
* ``auto_emit_completion_webhooks: bool = True`` — default-on.
Adopters who emit webhooks manually inside their handlers pass
``False`` to avoid duplicate delivery.
21 new tests cover: drift-guard against the on-disk schema cache,
URL+token extraction (incl. dict-params test fixtures), gate skips
(disabled, no sender, no URL, tool outside spec enum, no running
loop), happy-path delivery via ``WebhookSender.send_mcp``, token
echo via ``X-AdCP-Push-Token`` header, delivery-failure swallow,
sync-success fires, TaskHandoff doesn't double-fire, opt-out
suppresses, default-on, no-sender silent, sync_creatives fires too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(decisioning,webhooks): F12 round-2 — token via payload + exception isolation
Two P0 expert-review findings on PR #331:
P0-1 (cross-language wire divergence): the buyer's
``push_notification_config.token`` was being echoed via
``X-AdCP-Push-Token`` HTTP header. Per
``schemas/cache/core/push_notification_config.json`` ("Echoed back in
webhook payload to validate request authenticity") AND the JS
reference impl (``buildTaskWebhookPayload`` in
``src/lib/server/decisioning/runtime/from-platform.ts``), the token
belongs on ``payload.token``. Buyers validating against the spec read
``body.token``, not custom headers — header echo would silently fail
their auth check.
Fix: extend ``create_mcp_webhook_payload`` and
``WebhookSender.send_mcp`` to accept ``token`` and write it onto the
payload. Update F12's ``_emit_sync_completion_webhook`` to pass
``token=`` through instead of building ``extra_headers``.
Cross-language wire-parity restored.
P0-2 (exception isolation): ``maybe_emit_sync_completion`` runs
AFTER the platform method's successful return. ANY exception in the
gate body — extraction quirk on a weird ``params`` shape,
``loop.create_task`` failure — would propagate to the handler shim
and lose the buyer's sync response.
Fix: wrap the entire gate body in ``try/except Exception``;
logged-and-swallowed. Last-line defense ensures the post-success
path can never poison the buyer's response.
P1 fixes folded in:
* Submitted-shape detection tightened to the EXACT 2-key dict
``{"task_id", "status"}`` (not the loose
``status == "submitted"`` predicate). An adopter who legitimately
returns a sync ``{"status": "submitted", ...}`` with extra metadata
(queue acceptance) now correctly gets the auto-emit fired.
* No-running-loop branch bumped from ``logger.debug`` to
``logger.warning`` — production code landing here is mis-wired and
should be visible.
Round-2 tests added:
* ``test_handler_returns_before_webhook_delivers`` — pins the
non-blocking invariant (sync response returns before webhook
delivery completes).
* ``test_concurrent_emissions_dont_corrupt_strong_ref_set`` — 100
concurrent emissions exercising the
``_BACKGROUND_WEBHOOK_TASKS`` add/discard pattern.
* ``test_handler_does_not_skip_loose_submitted_shape`` — pins the
tightened submitted-shape detection.
* ``test_gate_swallows_unexpected_exceptions`` — pins the
exception-isolation invariant via a sender that raises on
attribute access.
25 F12 tests pass total (up from 21); 2208 total tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 6490948 commit 9d29754
6 files changed
Lines changed: 1106 additions & 31 deletions
File tree
- src/adcp
- decisioning
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
| 42 | + | |
42 | 43 | | |
43 | 44 | | |
44 | 45 | | |
| |||
70 | 71 | | |
71 | 72 | | |
72 | 73 | | |
| 74 | + | |
73 | 75 | | |
74 | 76 | | |
75 | 77 | | |
| |||
141 | 143 | | |
142 | 144 | | |
143 | 145 | | |
| 146 | + | |
| 147 | + | |
144 | 148 | | |
145 | 149 | | |
146 | 150 | | |
147 | 151 | | |
148 | 152 | | |
149 | 153 | | |
150 | 154 | | |
| 155 | + | |
| 156 | + | |
151 | 157 | | |
152 | 158 | | |
153 | 159 | | |
| |||
211 | 217 | | |
212 | 218 | | |
213 | 219 | | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
214 | 257 | | |
215 | 258 | | |
216 | 259 | | |
| |||
260 | 303 | | |
261 | 304 | | |
262 | 305 | | |
263 | | - | |
264 | | - | |
265 | | - | |
266 | | - | |
267 | | - | |
268 | | - | |
269 | | - | |
270 | | - | |
271 | | - | |
272 | | - | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
273 | 313 | | |
| 314 | + | |
| 315 | + | |
274 | 316 | | |
275 | 317 | | |
276 | 318 | | |
| |||
285 | 327 | | |
286 | 328 | | |
287 | 329 | | |
288 | | - | |
289 | | - | |
290 | | - | |
291 | | - | |
292 | | - | |
293 | | - | |
294 | | - | |
295 | | - | |
296 | | - | |
297 | | - | |
298 | | - | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
299 | 338 | | |
| 339 | + | |
| 340 | + | |
300 | 341 | | |
301 | 342 | | |
302 | 343 | | |
| |||
306 | 347 | | |
307 | 348 | | |
308 | 349 | | |
309 | | - | |
310 | | - | |
311 | | - | |
312 | | - | |
313 | | - | |
314 | | - | |
315 | | - | |
316 | | - | |
317 | | - | |
318 | | - | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
319 | 357 | | |
| 358 | + | |
| 359 | + | |
320 | 360 | | |
321 | 361 | | |
322 | 362 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
| 46 | + | |
46 | 47 | | |
47 | 48 | | |
48 | 49 | | |
| |||
75 | 76 | | |
76 | 77 | | |
77 | 78 | | |
| 79 | + | |
| 80 | + | |
78 | 81 | | |
79 | 82 | | |
80 | 83 | | |
| |||
117 | 120 | | |
118 | 121 | | |
119 | 122 | | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
120 | 141 | | |
121 | 142 | | |
122 | 143 | | |
| |||
213 | 234 | | |
214 | 235 | | |
215 | 236 | | |
| 237 | + | |
| 238 | + | |
216 | 239 | | |
217 | 240 | | |
218 | 241 | | |
| |||
226 | 249 | | |
227 | 250 | | |
228 | 251 | | |
| 252 | + | |
| 253 | + | |
229 | 254 | | |
230 | 255 | | |
231 | 256 | | |
| |||
246 | 271 | | |
247 | 272 | | |
248 | 273 | | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
249 | 282 | | |
250 | 283 | | |
251 | 284 | | |
| |||
267 | 300 | | |
268 | 301 | | |
269 | 302 | | |
| 303 | + | |
| 304 | + | |
270 | 305 | | |
271 | 306 | | |
272 | 307 | | |
| |||
0 commit comments