Skip to content
Merged
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
4 changes: 2 additions & 2 deletions docs/adcp-3.3-sdk-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ it adopted the bundle, and what is left to the codegen/wire-fidelity (2) and
| `by_property`, `by_installment_property` | `MediaBuyFeatures.supports_property_breakdown`, `supports_installment_property_breakdown` | Property-grain rollups. | generated; storage package rollups: (3), #1669 |
| Reliable Reporting `source.custody` | `core/reporting-report-definition.json` `Source.custody`; enum `reporting-custody` (`system_of_record`, `upstream_derived`) | | generated; `adcp.reporting` ledger: (3), #1669 |
| `reporting_webhook.operation_id` | `core/reporting-webhook.json` (`minLength` 1, `maxLength` 255, `^[A-Za-z0-9_.:-]{1,255}$`) | Native in 3.3 (adcp#7895); echoed verbatim; nothing is sent to a registration without it. The 3.2 forward-compat runtime patch in `_forward_compat.py` is retired. | done (1) |
| `reporting_webhook.authentication`, `artifact_webhook.authentication` optional | `core/reporting-webhook.json` `required` is `url`, `reporting_frequency`; `create_media_buy` `ArtifactWebhook` | Required only on requests served below 3.3; buyers pinned to 3.2 keep sending them. The 3.3 → 3.2 downgrade of a request without `authentication` must fail structured (#1621). | generated; downgrade fails structured: done (3), #1667; seller signing when absent: `webhook_signing_profile_for` (also refuses a missing `authentication` below 3.3), and `WebhookSender` refuses before any I/O to send such a registration unsigned or under another profile: done (3), #1644; Web Bot Auth webhook signing: not built |
| `signing_profile` | `ReportingWebhook`, `PushNotificationConfig`, `NotificationConfig`, `AgentNotificationConfig`, `ArtifactWebhook`; enum `webhook-signing-profile` | Per-registration webhook signing profile (Web Bot Auth or RFC 9421). | generated; webhook sender profile selection: (3) |
| `reporting_webhook.authentication`, `artifact_webhook.authentication` optional | `core/reporting-webhook.json` `required` is `url`, `reporting_frequency`; `create_media_buy` `ArtifactWebhook` | Required only on requests served below 3.3; buyers pinned to 3.2 keep sending them. The 3.3 → 3.2 downgrade of a request without `authentication` must fail structured (#1621). | generated; downgrade fails structured: done (3), #1667; seller signing when absent: `webhook_signing_profile_for` (also refuses a missing `authentication` below 3.3), and `WebhookSender` refuses before any I/O to send such a registration unsigned or under another profile: done (3), #1644; Web Bot Auth webhook signing (`WebhookSender.from_wba`, `with_web_bot_auth`): done (3), #1859 |
| `signing_profile` | `ReportingWebhook`, `PushNotificationConfig`, `NotificationConfig`, `AgentNotificationConfig`, `ArtifactWebhook`; enum `webhook-signing-profile` | Per-registration webhook signing profile (Web Bot Auth or RFC 9421). | generated; webhook sender profile selection: done (3), #1859 — `WebhookSender` signs under the registration's profile; `PgTaskWebhookOutbox`, `PgNotificationOutbox` and the reporting notification worker (`ReportingNotificationSubscription.signing_profile`, `ReportingWebBotAuthKey`) carry it to every attempt. `NotificationConfig` / `AgentNotificationConfig` registrations: the caller sets `required_signing_profile` on the prepared notification |
| Multi-qualifier metrics omit flat totals | `totals` on buys, windows and webhook per-buy | Receivers ignore `aggregated_totals` in delivery webhooks. | `WebhookReceiver` drops `aggregated_totals` from a `media_buy_delivery` result before parsing; the sender already dropped it: done (3), #1644. The SDK builds no buy or window `totals` (adopters' `get_media_buy_delivery` does), so there is no flat carrier to omit; `adcp.reporting` readers: #1669 |
| `sync_audiences` `audience_size`, `targetable_size`, `match_rate_basis`, `platform_id` | `core/audience-size.json`; enums `audience-size-precision`, `audience-match-rate-basis` | Ranges; `matched_count` / `effective_match_rate` may be absent on `ready` audiences. | generated |
| `sync_event_sources` `platform_id`, `archived` | request/response `EventSource`; capability `media_buy.event_source_archive` | `platform_id` dual-emitted with deprecated `seller_id`. | generated |
Expand Down
10 changes: 9 additions & 1 deletion docs/reporting-production.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,15 @@ contract. Private key material stays in the resolver and sender. The operator
must publish its brand document and corresponding signing keys at the declared
identity; the SDK does not assert that a supplied URL proves external ownership.
The declaration is RFC 9421 `adcp/webhook-signing/v1`, the exact supplied
algorithm set, no legacy fallback, and an 86,400-second retry horizon. Each
algorithm set, no legacy fallback, and an 86,400-second retry horizon. Pass
`web_bot_auth=True` to also sign subscriptions selecting `web-bot-auth` (AdCP
3.3): the declaration then lists both `profiles`, `ed25519` must be among the
algorithms, and every resolved `ReportingSigningMaterial` must carry
`web_bot_auth=ReportingWebBotAuthKey(key, agent_origin)`. Without it, a
`web-bot-auth` subscription is skipped and logged as an error (alert on it:
that subscriber receives nothing), and the account's other subscribers are
still notified. See
[Web Bot Auth webhooks](webhooks/web-bot-auth.md#reporting-notifications). Each
queue reserves the first HTTP attempt and its key/body binding before peer I/O,
using database time in PostgreSQL. Window insertion, HTTP attempt and ordinal
reservation share one transaction; a failed reservation leaves none of them.
Expand Down
9 changes: 5 additions & 4 deletions docs/webhooks/reporting-operation-id-3.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,11 @@ raises `WebhookSigningProfileUnavailableError` before any network I/O unless the
sender signs under that profile. A bearer or HMAC sender
(`from_bearer_token`, `from_adcp_legacy_hmac`, `from_standard_webhooks_secret`)
would otherwise send a credential the buyer never registered. Use
`WebhookSender.from_jwk` / `from_pem` for these registrations. The SDK does not
yet sign webhooks under `web-bot-auth`, so a registration selecting it also
raises; do not list `web-bot-auth` in `webhook_signing.profiles` and reject such
registrations with `INVALID_REQUEST`. For artifact webhooks pass the stored
`WebhookSender.from_jwk` / `from_pem` for `adcp/webhook-signing/v1`, and
`WebhookSender.from_wba` (or `with_web_bot_auth` for both profiles) for
`web-bot-auth`; see [Web Bot Auth webhooks](web-bot-auth.md). Reject a
registration selecting a profile you do not list in `webhook_signing.profiles`
with `INVALID_REQUEST`. For artifact webhooks pass the stored
`artifact_webhook` as `registration=` to `prepare_artifact_webhook` /
`send_artifact_webhook`, or call `sender.check_registration(registration)`.

Expand Down
185 changes: 185 additions & 0 deletions docs/webhooks/web-bot-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Web Bot Auth webhooks (AdCP 3.3)

At AdCP 3.3 a webhook registration (`push_notification_config`,
`reporting_webhook`, `artifact_webhook`, `accounts[].notification_configs[]`,
or an agent-level notification configuration) may omit `authentication` and
name a `signing_profile`. The seller signs every delivery under that profile:

- `adcp/webhook-signing/v1`: the AdCP RFC 9421 webhook profile, also used
when the registration names no profile. Sign with
`WebhookSender.from_jwk` / `from_pem`.
- `web-bot-auth`: the Web Bot Auth profile, experimental in 3.3 and the only
HTTP signing profile in AdCP 4.0. A webhook is an ordinary Web Bot Auth
request from the seller's origin. Sign with `WebhookSender.from_wba`.

## Senders

A seller whose buyers all use Web Bot Auth:

```python
from adcp.signing import load_private_key_pem
from adcp.webhooks import WebhookSender

wba_key = load_private_key_pem(Path("wba-ed25519.pem").read_bytes())
sender = WebhookSender.from_wba(wba_key, agent_origin="https://seller.example")
```

During 3.3 a seller usually has buyers on both profiles. Extend the existing
RFC 9421 sender, which stays the default:

```python
sender = WebhookSender.from_jwk(webhook_jwk).with_web_bot_auth(
wba_key, agent_origin="https://seller.example"
)
assert sender.signing_profiles == {"adcp/webhook-signing/v1", "web-bot-auth"}
```

The sender signs a delivery under the profile its registration selected,
which the prepared request carries as `required_signing_profile`
(`prepare_media_buy_delivery`, `prepare_artifact_webhook(registration=...)`,
and the task outbox below set it). A delivery whose profile the sender holds
no key for raises `WebhookSigningProfileUnavailableError` before any network
I/O. `sender.check_registration(registration)` runs the same check when the
buyer registers. Prove control of a notification destination under its
profile too: `challenge_webhook_destination(..., sender=sender,
signing_profile=config.signing_profile)` signs the challenge under it
(`adcp/webhook-signing/v1` when omitted).

Each `web-bot-auth` attempt is signed afresh by
`adcp.signing.wba.sign_wba_request`: a new 64-byte nonce and `created`,
`Signature-Agent` naming the origin, the key's RFC 7638 thumbprint as
`keyid`, and `@method`, `@target-uri`, `@authority`, `content-type`,
`content-digest` and `signature-agent` covered. Receivers record nonces,
including on an attempt they answered 503, so `resend`, `send_prepared` and
both outboxes never resend a stored signature. `resend` signs under the
profile the original was signed under. `Signature-Agent` is reserved like the
other signature fields: `extra_headers` cannot set it.

The key must be Ed25519, the profile's one algorithm. An ES256 key stays on
`adcp/webhook-signing/v1`.

## Publish the key directory first

Receivers verify a `web-bot-auth` webhook against the key directory at
`https://seller.example/.well-known/http-message-signatures-directory`. The
sender's key is verifiable only once that directory is live, so publish it
before the first delivery:

```python
from adcp.signing.wba import DIRECTORY_PATH

directory = sender.sign_web_bot_auth_directory(
other_keys=[request_signing_key], # every other Web Bot Auth key of the origin
)

@app.get(DIRECTORY_PATH)
def key_directory() -> Response:
return Response(directory.body, status_code=200, headers=dict(directory.headers))
```

The directory belongs to the origin, not to the sender. List every Ed25519 key
the origin signs with under Web Bot Auth, including request-signing keys and
keys mid-rotation, in `other_keys`; a key left out stops verifying once
receivers refresh their cached copy (up to five minutes). The signatures
expire after `expires_in_seconds` (one day by default), so re-sign and
republish well before then. When rotating, publish the new key, wait for
caches to expire, switch the sender, and keep the old key in the directory
while redeliveries signed with it may still arrive.
`adcp.signing.wba.sign_key_directory` is the underlying helper; see
[agent identity](../agent-identity-3-3-migration.md) for the directory rules.

## Capabilities

Advertise the profiles you can sign under:

```python
WebhookSigning(
supported=True,
profiles=["adcp/webhook-signing/v1", "web-bot-auth"],
algorithms=["ed25519"],
delivery_retry_horizon_seconds=86400,
)
```

At boot, `validate_webhook_signing_for_capabilities` refuses a wired sender
that holds no key for an advertised profile (omitted `profiles` means
`adcp/webhook-signing/v1`, so a `from_wba`-only sender must list
`web-bot-auth`), or that signs with an algorithm outside `algorithms` (a dual
sender is checked for both of its keys).

## Outboxes

`PgTaskWebhookOutbox` reads `push_notification_config.signing_profile` when
the task is issued and stores it in the encrypted registration and, at the
terminal transition, in the encrypted delivery row. Every attempt is signed
under it, `adcp/webhook-signing/v1` when the registration named none and
carried no `authentication`. Before the task is issued, a registration is
refused with `INVALID_REQUEST` (field `push_notification_config.signing_profile`)
when the profile it requires is not in this request's advertised
`webhook_signing.profiles` (`adcp/webhook-signing/v1` when omitted), checked
against the effective capabilities, a tenant's
`get_adcp_capabilities_for_request` override included. That applies to every
publisher: a fixed-sender outbox, a `sender_resolver` outbox, or an externally
managed one. A fixed sender is also checked for a key under that profile. A
`sender_resolver` outbox's resolved sender is checked again at each attempt,
and a row it cannot sign is quarantined. A resolved sender's every algorithm
must be in the scope's `advertised_algorithms`, including its Web Bot Auth
key's `ed25519`. Registrations naming `adcp/webhook-signing/v1` are stored
exactly like ones naming no profile.

`PgNotificationOutbox` stores the prepared request's
`required_signing_profile` with the body and signs each attempt under it. For
notification registrations (`accounts[].notification_configs[]`, agent-level
configurations), set it from the stored registration before enqueueing:

```python
from dataclasses import replace
from adcp.webhooks import webhook_signing_profile_for

prepared = WebhookSender.prepare_account_status_changed(url=str(config.url), payload=event)
prepared = replace(prepared, required_signing_profile=webhook_signing_profile_for(config))
await outbox.enqueue_prepared(
conn, prepared, notification_type="account.status_changed", caller_scope_id=scope
)
```

Rolling deploys: a delivery row stored with a profile by an upgraded pod is
quarantined, not sent under the wrong profile, by a pod running an earlier
SDK, in both outboxes. Rows without a profile are unchanged. One gap remains:
a task registered on an upgraded pod and completed on an earlier one is
enqueued and signed by the earlier pod under its sender's default profile,
because that pod ignores the registration's profile. Finish the rollout
before listing `web-bot-auth` in `webhook_signing.profiles`.

## Reporting notifications

`ReportingNotificationSubscription` takes the registration's
`signing_profile` (only with a `signing_scope_id`, never with legacy
`authentication`). A subscription without one is stored and fingerprinted
exactly as before, so existing subscriptions and queued deliveries are
unaffected. For a `web-bot-auth` subscription, the
`ReportingSigningMaterial` your `ReportingSigningResolver` returns must carry
the origin's key:

```python
from adcp.reporting.outbox import ReportingSigningMaterial, ReportingWebBotAuthKey

return ReportingSigningMaterial(
webhook_key, webhook_kid, "ed25519", frozenset({"ed25519"}),
web_bot_auth=ReportingWebBotAuthKey(wba_key, "https://seller.example"),
)
```

The worker builds a `from_wba` sender for each attempt of a `web-bot-auth`
subscription, and keeps the `adcp/webhook-signing/v1` key for every other
one. A `web-bot-auth` subscription whose material carries no Web Bot Auth key
is quarantined (`permanent_scope`). `ed25519` must be among
`advertised_algorithms` whenever `web_bot_auth` is set. Production reporting
opts in with `ReportingProductionSigning(..., web_bot_auth=True)`; see
[reporting production](../reporting-production.md).

Rolling deploys: a pod on an earlier SDK quarantines a delivery stored for a
subscription with `signing_profile` (`integrity_failure`) rather than sign it
under the wrong profile, and its resolver cannot return such a subscription.
Deploy every reporting worker before your subscription resolver starts
setting the field.
Loading
Loading