diff --git a/schemas/cache/.hashes.json b/schemas/cache/.hashes.json index c8429e8f..819cf0e7 100644 --- a/schemas/cache/.hashes.json +++ b/schemas/cache/.hashes.json @@ -1,143 +1,155 @@ { - "https://adcontextprotocol.org/schemas/2.5.0/index.json": "12ae44f8c5e11b47103074739218f463b7a69b5179ca745944030ae75691d801", - "https://adcontextprotocol.org/schemas/2.5.0/adagents.json": "18f8f53206a241f66870c78c188959ff14369976c0395aef80efd9385eef1585", - "https://adcontextprotocol.org/schemas/2.5.0/core/activation-key.json": "69d74e59a3bec0747605253f234e83aa9fc056892377207874c0a12e09012215", - "https://adcontextprotocol.org/schemas/2.5.0/core/assets/audio-asset.json": "5f1d811e1983a437bb695cc900095cbcf1f18160edd609b48b84bf1c7a1c3b46", - "https://adcontextprotocol.org/schemas/2.5.0/core/assets/css-asset.json": "023bb4e9a0fb97e6f1896fc01f1675d5fac96552f5ceb468c1f251fec41b2e63", - "https://adcontextprotocol.org/schemas/2.5.0/core/assets/daast-asset.json": "2dcd653cdd5f0d1cd72374a83fab1bc0469e8b1d9ae71457cc035bf32391195c", - "https://adcontextprotocol.org/schemas/2.5.0/core/assets/html-asset.json": "60b061219f2cece8d770d965163382c69df92e1a3c14915b67ab598ebce5ee3f", - "https://adcontextprotocol.org/schemas/2.5.0/core/assets/image-asset.json": "70f0b0f567494812b8f6264dfec7fec3af89a49df264219ee52a2d2e05e86191", - "https://adcontextprotocol.org/schemas/2.5.0/core/assets/javascript-asset.json": "5defcca636ba3f9b5e571b12269fd6188291fc81f4ad79e9532f75b0f0e26349", - "https://adcontextprotocol.org/schemas/2.5.0/core/assets/text-asset.json": "869a16d3e64845986b6a28be0e691f767171bd288383c7ebf2ac85543b922196", - "https://adcontextprotocol.org/schemas/2.5.0/core/assets/url-asset.json": "cabcb7eda29f264f8b9dc0ff6426c50709052af16d54ead97df0cd72520c58f7", - "https://adcontextprotocol.org/schemas/2.5.0/core/assets/vast-asset.json": "1176e0ac890d6d2b24b1ea1b1b30e5b09af7796e4466f2cc50e24f9ed1cb0b0c", - "https://adcontextprotocol.org/schemas/2.5.0/core/assets/video-asset.json": "af37258569c308b9da7a0cfa5d6da7c74d40842ff3581287303cb97a173294a0", - "https://adcontextprotocol.org/schemas/2.5.0/core/assets/webhook-asset.json": "310a147bb6c287902e916538c127cc3da27e659afd94de8b39cf39628347a281", - "https://adcontextprotocol.org/schemas/2.5.0/core/brand-manifest-ref.json": "0ab26106d32afae64a0ef16a912458fce16b500971e1395f1c8c052404cd5481", - "https://adcontextprotocol.org/schemas/2.5.0/core/brand-manifest.json": "a314870c441a3fe81de2d0c03f09acb371ce4f6cd30dca6a2525847943e81139", - "https://adcontextprotocol.org/schemas/2.5.0/core/context.json": "9b015157497c0c130030d0878b203cc6c7c82fa898ddd952612d045808980478", - "https://adcontextprotocol.org/schemas/2.5.0/core/creative-asset.json": "0ff1e730b446cca77f9e41d95004d7296c00fa0b7ff4ccbf614638f7e4899ecd", - "https://adcontextprotocol.org/schemas/2.5.0/core/creative-assignment.json": "399a52c6f88431cf2bc90819105cdb5c0f2b3907fbb228d7ad3cea675160c402", - "https://adcontextprotocol.org/schemas/2.5.0/core/creative-filters.json": "8156216754b6d07601c2bc908bbdf0e6fd430326f280cff8fc040e24107ba77c", - "https://adcontextprotocol.org/schemas/2.5.0/core/creative-manifest.json": "aeb6b1569d70a9a58dcedaeea08dbc1a60ee2933d42b30aad733093d7d5c52c9", - "https://adcontextprotocol.org/schemas/2.5.0/core/creative-policy.json": "d3afdf46108442696d461ad72378dfef8f4d12fb1b55ddacf04857b5974c4029", - "https://adcontextprotocol.org/schemas/2.5.0/core/delivery-metrics.json": "f20bad6fd68b8618f7069f94a95b58efbb476007bd7695c0fa0d258f9b8fdbb3", - "https://adcontextprotocol.org/schemas/2.5.0/core/deployment.json": "6345d07d5e58114f42b9d864b0342b5913ab2510792bf4b7babfbad9170d2108", - "https://adcontextprotocol.org/schemas/2.5.0/core/destination.json": "0b06af3ad980b8773eff8fc8dbdba05a98d57b8a969f8a443b6f42d4a4c05fb1", - "https://adcontextprotocol.org/schemas/2.5.0/core/dimensions.json": "7a03d2dedf9a82247464d312deb2d46e073ef19279bd44bfef22ab6e3c7721c3", - "https://adcontextprotocol.org/schemas/2.5.0/core/error.json": "3ddbad798b5b0058f45b1f58877017dbd5acd6f108e7bc1439ec825d2b54546b", - "https://adcontextprotocol.org/schemas/2.5.0/core/ext.json": "9ca11333a43642cd448796793dc0400780016482765284fa14621c0399b72ca1", - "https://adcontextprotocol.org/schemas/2.5.0/core/format-id.json": "b1572630a1c719d68001b61eef9bf987c68e5685cfbdb5f4430cce075ba561b6", - "https://adcontextprotocol.org/schemas/2.5.0/core/format.json": "cc288b1d85a7d2e346f4fd5df71c2359f55fd92de14a6da435f6cf0c35afc842", - "https://adcontextprotocol.org/schemas/2.5.0/core/frequency-cap.json": "b0cb792aae4f68e80a5870b1da54d2d2b9a2c9b5aa13e02d6f7f9ce9524b6e4a", - "https://adcontextprotocol.org/schemas/2.5.0/core/measurement.json": "1d61cf36dad5abdcf9b1cd6ebd16bd9233449868b362d8054dd7d1f4caa938d5", - "https://adcontextprotocol.org/schemas/2.5.0/core/media-buy.json": "8155b8245a763cd4de145faa4942217dc43f2230244ec583d2ba4294fc40e204", - "https://adcontextprotocol.org/schemas/2.5.0/core/package.json": "6d2e4cbfc1869ee0085b8c4d9c5859883e25d52d9a8ce298c8dd962d73e5bc61", - "https://adcontextprotocol.org/schemas/2.5.0/core/performance-feedback.json": "f052763e9220be979ea57c5852b560f88afc4f05dca749c456faa4a5606cab2f", - "https://adcontextprotocol.org/schemas/2.5.0/core/placement.json": "2a4d71292f5b8a1e46fe76e366056117349833076323922aa02919953f916d86", - "https://adcontextprotocol.org/schemas/2.5.0/core/pricing-option.json": "f209d5ea766255e7a85f79056432ad2b7c99d872214a484b8944ff6c3fa3511b", - "https://adcontextprotocol.org/schemas/2.5.0/core/product-filters.json": "5715691a4ea4fa83847652078b228b78423978fe5cb007b1db87a598c69c5afe", - "https://adcontextprotocol.org/schemas/2.5.0/core/product.json": "9bb86a33bb747bf217f5d0b22aa643fa351fff72b550b97bf2487bebddc057ab", - "https://adcontextprotocol.org/schemas/2.5.0/core/promoted-offerings.json": "8e6630e303a26c9005bc93ae8ecf6c2fc09e0501fc592654d21256d14027f20e", - "https://adcontextprotocol.org/schemas/2.5.0/core/promoted-products.json": "e306e0704d0815152e32092e068a472285ff8332dbc50d0cd40e818bc4a0da2b", - "https://adcontextprotocol.org/schemas/2.5.0/core/property-id.json": "dcbdac1ba04e4722d60f1a3644802557e6ed7d4b7b957944249cbf7204de3f0e", - "https://adcontextprotocol.org/schemas/2.5.0/core/property-tag.json": "a6f36055fbf8de4428c280416c51bf202a4878a5b33f70df80e3e23aeeaa5bec", - "https://adcontextprotocol.org/schemas/2.5.0/core/property.json": "2674eca4f44aff9ff2882f451d6a74bd6aa83ffbe376135b263ec265399345b9", - "https://adcontextprotocol.org/schemas/2.5.0/core/protocol-envelope.json": "75b6ccb393996eda79e086298419cf10c4b59dd9d260df733cbcb456212a97bf", - "https://adcontextprotocol.org/schemas/2.5.0/core/publisher-property-selector.json": "8e4f138a7895e29d1fc7bf6bb4ff74ccc731231ba8bdcccb71d61fa5ffb87001", - "https://adcontextprotocol.org/schemas/2.5.0/core/push-notification-config.json": "4284a9226f582b108c82b66e60fc96ca4411556f9ee11b685928dfbb51894650", - "https://adcontextprotocol.org/schemas/2.5.0/core/reporting-capabilities.json": "7f48a485feb70500a900e93a4c60188a95f67501db7abbc718f85c396faabee9", - "https://adcontextprotocol.org/schemas/2.5.0/core/response.json": "8552ba941ff83d60c799bf037facfd12000dc108b9d82359c9ae57960209ea34", - "https://adcontextprotocol.org/schemas/2.5.0/core/signal-filters.json": "2449389688cde069d45d9ae2a27372f5b2f317efc489acf620335e66aa12684e", - "https://adcontextprotocol.org/schemas/2.5.0/core/start-timing.json": "f07f12ef82b73dbd30611a537081f54a77017d42421c910baf1d85b3c06f7cc1", - "https://adcontextprotocol.org/schemas/2.5.0/core/sub-asset.json": "6312d9d70971075cfcd52b5d1ad1b29a5a07b66bec29be8406a9d7a4aea9fd30", - "https://adcontextprotocol.org/schemas/2.5.0/core/targeting.json": "261dfdda62f75bdb9f6d38e0adad8bea5d5ca9065db7272eec19807804daa7e0", - "https://adcontextprotocol.org/schemas/2.5.0/core/webhook-payload.json": "38e3a0752e24d3099bbb70a6c1e853fafa18a725722bcf2dd913dbbfdcec35b8", - "https://adcontextprotocol.org/schemas/2.5.0/creative/asset-types/index.json": "b91662cbe3fa133155c71d8afa3db465a9fface137cbdd764fb5ee9d37584908", - "https://adcontextprotocol.org/schemas/2.5.0/creative/list-creative-formats-request.json": "e0e107969b2134ddc41e4495e6043bbe5169e840f6c1734c7eae0cb37ccf51fa", - "https://adcontextprotocol.org/schemas/2.5.0/creative/list-creative-formats-response.json": "8d7bab833eb91e45d82253a152d4cd60bc6a1b4dcf28e021d6be2da6b73eb15d", - "https://adcontextprotocol.org/schemas/2.5.0/creative/preview-creative-request.json": "29e991e85c5e3c0c78118570c7afbfb15acbd5d8055117f9a067611a53f7039b", - "https://adcontextprotocol.org/schemas/2.5.0/creative/preview-creative-response.json": "c8df237ad0b275c10c9c74afa3f60d63a992cc8d416d6618054a755c431d76bc", - "https://adcontextprotocol.org/schemas/2.5.0/creative/preview-render.json": "f8e0c9f1c2e8abf0be1c66fc2a5e49fc7ba143e63d3dcbea0d167fd3952ca7ea", - "https://adcontextprotocol.org/schemas/2.5.0/enums/adcp-domain.json": "cadb89e00807072c6e3158d1aaacb72f111616e926847c7e3d38c769d72493ba", - "https://adcontextprotocol.org/schemas/2.5.0/enums/asset-content-type.json": "7e7db1cc136ba8df21275126eb14ee7ffcdb1aad58d685ae0a84cd3713c50e50", - "https://adcontextprotocol.org/schemas/2.5.0/enums/auth-scheme.json": "ad3bcf99a0bdbf08c2259a5b4f50b4c659aa356b8baf0a34c11cf889966b0170", - "https://adcontextprotocol.org/schemas/2.5.0/enums/available-metric.json": "5222f0bc556a85cb47199d08a858713481de01b04f0a62051dd04416d247cf70", - "https://adcontextprotocol.org/schemas/2.5.0/enums/channels.json": "ded76d87ce008b41b0b1d768b751c519c78e7ebffe8488badad332b826521d20", - "https://adcontextprotocol.org/schemas/2.5.0/enums/co-branding-requirement.json": "299a946ef8aa7ceca675d54a157beb4ab4a6401946e89de315987a8a0f47acdb", - "https://adcontextprotocol.org/schemas/2.5.0/enums/creative-action.json": "6e260d4492efd25782af2dc436d3f26c5e73334df65cffda912d895c25969430", - "https://adcontextprotocol.org/schemas/2.5.0/enums/creative-agent-capability.json": "9146da742d489c3d2cf2ed3015f9773bb02eaeac27925dfa03e653dcc53e8d42", - "https://adcontextprotocol.org/schemas/2.5.0/enums/creative-sort-field.json": "86473d13a548687549ca839978bf2b261a66e8bd1ac42e7b031ee853477c7355", - "https://adcontextprotocol.org/schemas/2.5.0/enums/creative-status.json": "dd4154c3d550c63ac9875e0c9a940e85404e1eb996e589b3312d8e842c247bf3", - "https://adcontextprotocol.org/schemas/2.5.0/enums/daast-tracking-event.json": "de8e20b1645a6e9034e3010e9899007ad60dcb8dbb24880b468a6d541b63eb06", - "https://adcontextprotocol.org/schemas/2.5.0/enums/daast-version.json": "3429882ff91dcf98001404f358c948f7af2bf1bd01066461db2e3c94c3feb6de", - "https://adcontextprotocol.org/schemas/2.5.0/enums/delivery-type.json": "b2517047b2833e9cd1f51586a2e395105b51be1048b9ab0b1133e5876cb7a63d", - "https://adcontextprotocol.org/schemas/2.5.0/enums/dimension-unit.json": "ccc1815ca50762aaadafb2914312faea8ffb01a0ca4889302a26ca91042bd734", - "https://adcontextprotocol.org/schemas/2.5.0/enums/feed-format.json": "a55a93a46202f1a9e9a6922e9e1a699ee4493958a78698818f14b3570a8a3441", - "https://adcontextprotocol.org/schemas/2.5.0/enums/feedback-source.json": "0152769dace6934c9144920767903ffbcd0f900ae3e4bbb012979be76212e408", - "https://adcontextprotocol.org/schemas/2.5.0/enums/format-category.json": "8d8b733441ae10bc90dd170ef055c42d80765d9897a52976f6bc8687d3fd7be7", - "https://adcontextprotocol.org/schemas/2.5.0/enums/format-id-parameter.json": "6f632401c1566c5a663fc76e42d2bf709c0b55bde624386630be37a1a3fd90b6", - "https://adcontextprotocol.org/schemas/2.5.0/enums/frequency-cap-scope.json": "9a1c5236fc561dd0ad82ae0303ef0d1108cc3d7a05b4fbca8f98e61442f05bf1", - "https://adcontextprotocol.org/schemas/2.5.0/enums/history-entry-type.json": "8c95fe77ee02d6908fd24634e69bf5275196b70947614bc13c160db33089c653", - "https://adcontextprotocol.org/schemas/2.5.0/enums/http-method.json": "38419f4d19f171dca0e2994fcb3d73db1b14a1e6282b6fd5f39b49bfce3a451c", - "https://adcontextprotocol.org/schemas/2.5.0/enums/identifier-types.json": "1080807ac9e6f9abcc9cb79d0930f4c3a085d1cc21efff9c1bd8b2fbf8347c82", - "https://adcontextprotocol.org/schemas/2.5.0/enums/javascript-module-type.json": "9042e1e8d6b563622a1af8634eff434832cd8d716b5c7d423ffa2cf7ed79098b", - "https://adcontextprotocol.org/schemas/2.5.0/enums/landing-page-requirement.json": "4db6582eec4660f4d17f9402f9ff4674a6ca92d9d220505028a38c8659937af8", - "https://adcontextprotocol.org/schemas/2.5.0/enums/markdown-flavor.json": "5aff71ecb185c00d8a98034a0016c042d215d0959f624dfb244a2cbedbaa9ca5", - "https://adcontextprotocol.org/schemas/2.5.0/enums/media-buy-status.json": "447b4550a9569bf283e812074bb3ee3906d4fc08a5c8d0dc5e594dd0a9308164", - "https://adcontextprotocol.org/schemas/2.5.0/enums/metric-type.json": "44be258d86e6d1193c7cb89c4914f9c1bb15b310bb48b21463f9bd7ec24a5f02", - "https://adcontextprotocol.org/schemas/2.5.0/enums/notification-type.json": "c749771741a2df52cb6c264d09a73085e52e3cc4f5378338d67ee4cbf22c48cb", - "https://adcontextprotocol.org/schemas/2.5.0/enums/pacing.json": "0c11996fb5784d90ddbc8b344463db847e3d4324c181c3da4782cb982b896294", - "https://adcontextprotocol.org/schemas/2.5.0/enums/preview-output-format.json": "a8875d7566094f328f006049200c04ba65350bc9ffcca46fe782ddfbbfc2e8c4", - "https://adcontextprotocol.org/schemas/2.5.0/enums/pricing-model.json": "deab120eb56aa7782252f9967083cc46928516e9faa248bb2594f0fc1824c63c", - "https://adcontextprotocol.org/schemas/2.5.0/enums/property-type.json": "24d4004c725706cbf26903d3697a2278a63d904e3b392d2c60b15b36040a721e", - "https://adcontextprotocol.org/schemas/2.5.0/enums/publisher-identifier-types.json": "1bbbba42c952ab6d52957db32edcaf6ddfee99a426e48c93b9a18400b1a52d3d", - "https://adcontextprotocol.org/schemas/2.5.0/enums/reporting-frequency.json": "14420ff7c0e3b5d362c839fd08394b00a5b40466de5ae831b906c21e2e0c8bd1", - "https://adcontextprotocol.org/schemas/2.5.0/enums/signal-catalog-type.json": "9146a61c6de34b01fd2ed616d7dfa572dabfb988a1ea8ee552f8c3be71593ec5", - "https://adcontextprotocol.org/schemas/2.5.0/enums/sort-direction.json": "d926a3ce0dd78ed8ae5a9f22437b11f0f3fdf180a3d94d2d803f3219f9070e92", - "https://adcontextprotocol.org/schemas/2.5.0/enums/standard-format-ids.json": "f6419c7273e56929b0c6b66262559ce44e45a1cb4c14f12156e64260d661ad3c", - "https://adcontextprotocol.org/schemas/2.5.0/enums/task-status.json": "1ad7c53fd4fce4e8e8f867d5bfbf8d831acf2f4c27737e289d7f7b29ca8648e6", - "https://adcontextprotocol.org/schemas/2.5.0/enums/task-type.json": "f543d5076a640d29478076cf2a72b088a635a1a2343c48b0e2e3e059d85bfec8", - "https://adcontextprotocol.org/schemas/2.5.0/enums/update-frequency.json": "90fa867b8c7f8b81b64a3eb1e5dbbc6eba7fe318166635cc9fd924831c5e6dfc", - "https://adcontextprotocol.org/schemas/2.5.0/enums/url-asset-type.json": "8f3402e61befbbad0954881b918dd9a9df87fea3f804648b92f53df4362cf14c", - "https://adcontextprotocol.org/schemas/2.5.0/enums/validation-mode.json": "ebafe630f554e644862ed04f27e8e0b05e9c549358109a64d8d7dabdfc175241", - "https://adcontextprotocol.org/schemas/2.5.0/enums/vast-tracking-event.json": "3830f52d7f6d639b4bceeba9f62ef4f1950e53ae8e182192d33cf4266bfd6ffc", - "https://adcontextprotocol.org/schemas/2.5.0/enums/vast-version.json": "c44f314fdac547bf49d29f9c2d130f70503bda5b561ee9c3245358227b7e2264", - "https://adcontextprotocol.org/schemas/2.5.0/enums/webhook-response-type.json": "68708a08f25eef59f668f5c7de311579fd9fd2ccab942c073a0bd6d94fbb9315", - "https://adcontextprotocol.org/schemas/2.5.0/enums/webhook-security-method.json": "ce663ac84ff144ecee5b836ef408d01c02921a78f36134041a112c6ba108d5d2", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/build-creative-request.json": "7e74533bf01ed6ccb14aea5965eafc12c2192043d5d6ebe7c0a4ab2bfd19bb9d", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/build-creative-response.json": "1a8e7976988b4368cfb4bdace6b36b4118cd9df514cdf4735908f407e21b50f8", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/create-media-buy-request.json": "c0673c51e201bd4392e80b0ff49ae04da842c6a9fc335d7afe6c5c144dc23e7c", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/create-media-buy-response.json": "7ad3793035eac16253a9000c1c584fbd76f89b09fd1e441bd6137c431edd370e", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/get-media-buy-delivery-request.json": "71c06190ee05faefa77fb5bfa552266c4f08237348e1b5c39dc12a0770ac6c2c", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/get-media-buy-delivery-response.json": "13815c7e161e5b0199ebfaa55119e053df5ee828e47c207e51d8b2f8be926e54", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/get-products-request.json": "4b4efe187c13be2f81160b71a0cc740d4eddca36c285102e870f06d4b1913c1a", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/get-products-response.json": "16012d6009442bf4445a230e6614ebf48c360711cd9403b1616d0b3a6070a501", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/list-authorized-properties-request.json": "ef5041929b423e40c0741f0350ff53bbfecaf38777facef33eb3f22ae0fbdc7c", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/list-authorized-properties-response.json": "de0a8bcf164cd37565677a1fed9124491ad977a879f9113f816acf02fed3ff79", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/list-creative-formats-request.json": "fb0e7eb77912cd952b4d02658deec8e003c1c31d81823951d3a0421562973af3", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/list-creative-formats-response.json": "e48627ebb52886d62f0da1843d95d64310ed8b97fb3bb4910f7e5e26e2549df4", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/list-creatives-request.json": "2440a7d3871a945102efeeb3a25f0ab5993b7c1e92c2080c3e2c96b40ed65e2f", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/list-creatives-response.json": "51f8735380b58da1a97cd91575acd41300cf0c69a5edc2c1f5f9cbea6245d6f3", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/package-request.json": "3f7f3ffa9f1d1518666a48516e5c7a8e9d494176d0dbc7cfa1957a1b4afcb24c", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/provide-performance-feedback-request.json": "0de965bb23d9ab4bea34d29e1b10cfa56468a55874348e20a043aad26a59e226", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/provide-performance-feedback-response.json": "24ddc349f50477df23032614fbc1ce086cf45b239efddceba8bbdcc70a04f936", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/sync-creatives-request.json": "dda5d72f7158ec9314ef5fbefa27c934e98acee8487ca1f4daf987ab1731bf74", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/sync-creatives-response.json": "5bf401320c932f83744289b300ca00f24769267a7f4e79b1cb573a608f0fb666", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/update-media-buy-request.json": "47ef9c59afefed45876604d0d37d3473cce6547047e27763d548c386bb59d72a", - "https://adcontextprotocol.org/schemas/2.5.0/media-buy/update-media-buy-response.json": "c996d67019682c38930fc13b36838c5e30cbf383de34f84c952bee7a5be95f33", - "https://adcontextprotocol.org/schemas/2.5.0/pricing-options/cpc-option.json": "d69d64a757308228b4eed223cb68cd838da41271c7fe0416d1286a2780ac8b17", - "https://adcontextprotocol.org/schemas/2.5.0/pricing-options/cpcv-option.json": "88efdb887fe892a783f34bbc493f4c76b9e668068b88b63f8234d1ec4a167076", - "https://adcontextprotocol.org/schemas/2.5.0/pricing-options/cpm-auction-option.json": "bab72cd592a1c5ded871f3a2c0ddd790234e669e58b999e39dba7707d842e449", - "https://adcontextprotocol.org/schemas/2.5.0/pricing-options/cpm-fixed-option.json": "111e6e556b88098b2fa5807c546839c30190a016e49d4c6f12203b8ee650ee78", - "https://adcontextprotocol.org/schemas/2.5.0/pricing-options/cpp-option.json": "573acf6aade1c9955f82ec63a0e0b936cc65f07fd0ef9901a0c395021501d4fa", - "https://adcontextprotocol.org/schemas/2.5.0/pricing-options/cpv-option.json": "9604daebcbae7ba705671f0ad6bb2780d8a8cb57740e5efaf35f5fc9ed96cb51", - "https://adcontextprotocol.org/schemas/2.5.0/pricing-options/flat-rate-option.json": "b1572fd4400081207c33bcf83744c9ecab50c00ab9d67436f69ffdba04f4ae96", - "https://adcontextprotocol.org/schemas/2.5.0/pricing-options/vcpm-auction-option.json": "7ddf2f53a9562d408e0fde7c5d7439f3727b9e794be63a49b575a7ed828b30ec", - "https://adcontextprotocol.org/schemas/2.5.0/pricing-options/vcpm-fixed-option.json": "7c72c2ab274d64ed009c202ee53ce10e50d95d210774b9d66e10dbddd1f6f2bd", - "https://adcontextprotocol.org/schemas/2.5.0/protocols/adcp-extension.json": "49542b907e6b6d8f8f96557dc610eaced48947aa03c5e3c783f086d67530365f", - "https://adcontextprotocol.org/schemas/2.5.0/signals/activate-signal-request.json": "9fbe25c8f5427e93159d526cb88a84cabfbf4fc956fcb1c965cb3e8f055d638a", - "https://adcontextprotocol.org/schemas/2.5.0/signals/activate-signal-response.json": "39c868f153a364707a7a06b3816fae1bb125b44506a55a85131314fdebc978f0", - "https://adcontextprotocol.org/schemas/2.5.0/signals/get-signals-request.json": "9b07199e09c4f33447ae00eabb47ef82ccc77f0bfb7f3652aca1f09e1b35e77d", - "https://adcontextprotocol.org/schemas/2.5.0/signals/get-signals-response.json": "2addd193bc179618b19f2a6a0d7d5db2508f31ec7468556deaf87506f88fe3cb" + "https://adcontextprotocol.org/schemas/v1/index.json": "66f2aee679deca39a3568e306d2ee5fffd6787f2481c5091a939477350d94500", + "https://adcontextprotocol.org/schemas/latest/adagents.json": "ca971f574560e40457d05d6356adee231479592bce51dd07cd28b21d78d31dda", + "https://adcontextprotocol.org/schemas/latest/core/activation-key.json": "d4429a894016a2598f8ff48e330a438196bc4b42dad024f7aec55432e8970692", + "https://adcontextprotocol.org/schemas/latest/core/assets/audio-asset.json": "7ab9e762afe3486c9059a529736b37ef11cd905c60c8aaf8f19fb2812844ba85", + "https://adcontextprotocol.org/schemas/latest/core/assets/css-asset.json": "088774919bff4dd4319de55e663b828208596da92de15a50ab3f1479c102b8df", + "https://adcontextprotocol.org/schemas/latest/core/assets/daast-asset.json": "7dd9f5fb417926f6b214a8c9b29da3d89296ea7135112ab400239a7f8631d972", + "https://adcontextprotocol.org/schemas/latest/core/assets/html-asset.json": "532eebc05ad8534d8e23eb3cbce88e8171fcefc7821957530b4014d7d713228b", + "https://adcontextprotocol.org/schemas/latest/core/assets/image-asset.json": "342deb6c4918344842479dd53be9f8128401bcdea33512c6bd28edb4fdc89092", + "https://adcontextprotocol.org/schemas/latest/core/assets/javascript-asset.json": "72967e4e679a225e8bc8d1db325db0ee751edd97118ca7b36e1cb1f8a8b86644", + "https://adcontextprotocol.org/schemas/latest/core/assets/text-asset.json": "f5fff3619e3f12ce29c87c8c5b351853e8504e25454f3dcbcd905476551da819", + "https://adcontextprotocol.org/schemas/latest/core/assets/url-asset.json": "b7590bf3a3e48c95ebb08014a8cfb7fb1deea536b1e65bec7f7c329abeb4589f", + "https://adcontextprotocol.org/schemas/latest/core/assets/vast-asset.json": "a7914f85a3737c8e0aec858b8f6fc894eee5c877a653a1737ae4674b5533795c", + "https://adcontextprotocol.org/schemas/latest/core/assets/video-asset.json": "83937d482ef0e4c01f4f833711d47a67fdcfba42544043b21ab3bb9a5bda247e", + "https://adcontextprotocol.org/schemas/latest/core/assets/webhook-asset.json": "e1b9b45c3305349d571003371502693828b57eedc9fe9f12b7fb071a7f5ea3f2", + "https://adcontextprotocol.org/schemas/latest/core/async-response-data.json": "b3e3e57e1a121067cdd6392265223a14b89ebb27220a18d822ffdeb09e7b7523", + "https://adcontextprotocol.org/schemas/latest/core/brand-manifest-ref.json": "10bd77dc7e07fb8e8a0665272c18cdcb7f05afc72856ffd66e6f2505431d7612", + "https://adcontextprotocol.org/schemas/latest/core/brand-manifest.json": "127183790905f2ad1b52f2097e89f714f071543654d12ea3900f8b41acdeb99a", + "https://adcontextprotocol.org/schemas/latest/core/context.json": "b5b5de9482d662c1eacfcfdfa53de672a047664e295940cb204f2d822d5eeb31", + "https://adcontextprotocol.org/schemas/latest/core/creative-asset.json": "a971362eaef05186b663cf22db6d03bab876fba63547c836db14fea86c5f2ce1", + "https://adcontextprotocol.org/schemas/latest/core/creative-assignment.json": "36cabb2da7e19b7e99f7e91ba2d5ac62ba881f336f7735213484a9c5c9882f8b", + "https://adcontextprotocol.org/schemas/latest/core/creative-filters.json": "5da9f8ee13b3ea261b887acf39241893c022491f95e48f1ffbaf1bccedc1d331", + "https://adcontextprotocol.org/schemas/latest/core/creative-manifest.json": "0e5f29f44b35509f7d8709604a82dfdfb32401672ec22c4924fc78e3877c0914", + "https://adcontextprotocol.org/schemas/latest/core/creative-policy.json": "adfba827378e11aace7ded9c173d55043d5f6d4e6a61321b1e0640cd2e447545", + "https://adcontextprotocol.org/schemas/latest/core/delivery-metrics.json": "c04cc7a46cf2d8d11134ae0a9e5662d050991521781d4d2521c3d72ad0717508", + "https://adcontextprotocol.org/schemas/latest/core/deployment.json": "d72caa8138af5bca256cb1c5502b3313a249ff7484c611fc813ccbc59d77222b", + "https://adcontextprotocol.org/schemas/latest/core/destination.json": "0ce81da756e6e8d57747f106a1b3d6bfdae3756f835bbc6bcc8baff2896f5819", + "https://adcontextprotocol.org/schemas/latest/core/error.json": "db54eb32908dcfa23b77e28ef1d317bdb26c4715d2bcee48f5619429f21e9f11", + "https://adcontextprotocol.org/schemas/latest/core/ext.json": "0dc59d0cf5bcf97f9ce9ba635f977ec65b982e853cc0b05decb034cfb642f4b5", + "https://adcontextprotocol.org/schemas/latest/core/format-id.json": "1ae1959498a13f4d944196dc5967f098855d27a87e3eb4ea5d3e4e3d587f52cf", + "https://adcontextprotocol.org/schemas/latest/core/format.json": "db547a8703d297173c56a789aee7430328941d77881227deefb89beaf4a4373a", + "https://adcontextprotocol.org/schemas/latest/core/frequency-cap.json": "6b43e269f021f6230001ab7923761cafa6402468379d595c4e89a3afa1f17919", + "https://adcontextprotocol.org/schemas/latest/core/mcp-webhook-payload.json": "1dae520cb0c0564f4c647a5fba4cb8383897f28dcf72a2d046986684255f099e", + "https://adcontextprotocol.org/schemas/latest/core/measurement.json": "596c14a1c135928937a1a238a8a0aaa0d9bc43e6f7a497b553746afa8e9f4161", + "https://adcontextprotocol.org/schemas/latest/core/media-buy.json": "0bd1dd219544a254ad5ab38ca6f5fa7e7b14e3e1185d865035e23130a6037dd2", + "https://adcontextprotocol.org/schemas/latest/core/package.json": "892d138037b86077e2effee0f9eabdb4c0d2d87c52ae0c99c746affc4f90a791", + "https://adcontextprotocol.org/schemas/latest/core/performance-feedback.json": "69eb64438779c5732b6da6d9bcaeb2576bc1b7681722e6db8cd148e2caa255b9", + "https://adcontextprotocol.org/schemas/latest/core/placement.json": "15bf0a30a29406964e9e00f5f20431bf1a8ff157f071315892ea170ec283f08c", + "https://adcontextprotocol.org/schemas/latest/core/pricing-option.json": "f4f99dac2a28722dd3ce0ded62e8b93a295ba5fff43de6c3d1419e0cfccf4e12", + "https://adcontextprotocol.org/schemas/latest/core/product-filters.json": "1fef7a57e485d064b1b9c171980c861ddaddd901587b1c83731dbbdaa70e60ee", + "https://adcontextprotocol.org/schemas/latest/core/product.json": "d08b988eb16a84f9a9710cdec64c72427512e4c396590189db89ffdcdcf78a59", + "https://adcontextprotocol.org/schemas/latest/core/promoted-offerings.json": "0e5bc0d269ba9909863560890fd97d64545716a0b15ef2afbf49828c4f4b6b11", + "https://adcontextprotocol.org/schemas/latest/core/promoted-products.json": "cf9497d2751b9bcd34abf34199f9965828fd3f2e457076e69e5c8b729b53f1b2", + "https://adcontextprotocol.org/schemas/latest/core/property-id.json": "16ff353402c03691a7290a0f7578a901b63a0f7e83567b8903c5c6f89e1fb6f0", + "https://adcontextprotocol.org/schemas/latest/core/property-tag.json": "6f2c35324ca6c5533a2c3d1a5837360c813a0905be68b11da7a46965ec37d59c", + "https://adcontextprotocol.org/schemas/latest/core/property.json": "785bd5615917aa73e4e10f4925f35c0fc739b5c60f4351b53a005d131bb74657", + "https://adcontextprotocol.org/schemas/latest/core/protocol-envelope.json": "5e59b7890e40beffb4ab56b723eaefd57a6917ee561ab71e41e26491c45d50de", + "https://adcontextprotocol.org/schemas/latest/core/publisher-property-selector.json": "53f58431de12001c89af98dde18769cb1b021a6b88d3503111d8a2ac5e418eda", + "https://adcontextprotocol.org/schemas/latest/core/push-notification-config.json": "64ed4db710d5a5346d6935e238d28ffaa4374dcff9d08e5d4283d4786dfe6de5", + "https://adcontextprotocol.org/schemas/latest/core/reporting-capabilities.json": "5b49fa96cce7bda72e758ac1ffb2ad39166798bf3133d89fda3e143584140c64", + "https://adcontextprotocol.org/schemas/latest/core/response.json": "cd52aeaf429ed7ccd5e45f88cef9520d734874cbd7ea53831dac99691de2835b", + "https://adcontextprotocol.org/schemas/latest/core/signal-filters.json": "dc368066ffd83612767e6cd60698eb41b91704e9771abeb91a83b655f86ae757", + "https://adcontextprotocol.org/schemas/latest/core/start-timing.json": "8e07e0469f434f3d3ba998e2b1fe58fc3dd097915e2d5e0c547cadfd5a469158", + "https://adcontextprotocol.org/schemas/latest/core/sub-asset.json": "ae9a93b482eb023971c6b85296fe7cd64cbd47422e43e0ae7758eaa734b10d83", + "https://adcontextprotocol.org/schemas/latest/core/targeting.json": "a576185d458a258f0652d3d1c9056d7da7e5acd46e7b615b392c1ddd52fcf02a", + "https://adcontextprotocol.org/schemas/latest/creative/asset-types/index.json": "e191c0e50f800351f07e754bb70042c7f960bf75f9bb36ff8f73ff5b9b104cfd", + "https://adcontextprotocol.org/schemas/latest/creative/list-creative-formats-request.json": "4b0ccb85bb35fafabdf5ff38d29ebb9fe8c2d49c81e32488371da2e9a0444d5d", + "https://adcontextprotocol.org/schemas/latest/creative/list-creative-formats-response.json": "2c24b242e05fa2629599c2dffc6f2bd54d86d6917481066f9874652d8c00a788", + "https://adcontextprotocol.org/schemas/latest/creative/preview-creative-request.json": "aea9784b4f44c9648dd35748cf23a6b516a0fe16013e348c45122526a94fdbb9", + "https://adcontextprotocol.org/schemas/latest/creative/preview-creative-response.json": "f8d6b69641994c44bca3f89761b20eb95d74ed1be4f9474e108f035a6af6b3d5", + "https://adcontextprotocol.org/schemas/latest/creative/preview-render.json": "609f89f622792258069e7d6e6b0fc5bc4de7d5bf6e90630389d8fc02412f6be4", + "https://adcontextprotocol.org/schemas/latest/enums/adcp-domain.json": "f6c850ee4652848a0a4825c5911c4c53e4793324a4596973540b2eacb501c69d", + "https://adcontextprotocol.org/schemas/latest/enums/asset-content-type.json": "74414bc886efb8a52794da63cfbb873befd9d24c87d9293ac88008bd496dfafc", + "https://adcontextprotocol.org/schemas/latest/enums/auth-scheme.json": "492732201479760e022e7e20461874312403dab836af0f45c1382fd7b59c8b17", + "https://adcontextprotocol.org/schemas/latest/enums/available-metric.json": "30577d384f8ffa1f6b9285c59878dbe0d55b090ea9e9ae765efa9fe4aabd3825", + "https://adcontextprotocol.org/schemas/latest/enums/channels.json": "18344bb746790dd1a2d6874c604fa3735e4b6ea8cd903d71fad25f6de71ced3e", + "https://adcontextprotocol.org/schemas/latest/enums/co-branding-requirement.json": "7d533d01abfc5b134985e4a4611547a8d88601f2ad3dae166d30483433a5af8e", + "https://adcontextprotocol.org/schemas/latest/enums/creative-action.json": "c8f56831b63d868ecdadb81787fbd9bda85f791387cc04d10a4f3f4774edf591", + "https://adcontextprotocol.org/schemas/latest/enums/creative-agent-capability.json": "1393bde9696600816f802091fe73cd8553c305e039b8f0e39493b5112e4f9d27", + "https://adcontextprotocol.org/schemas/latest/enums/creative-sort-field.json": "adc83644f01b9948aa436c79dc0701d878a876e185adf7808485772fbad5eb28", + "https://adcontextprotocol.org/schemas/latest/enums/creative-status.json": "ac51ce7d2803901f5c2c9115becf16965c4c205a186c7d5bbc3079ac83ccca85", + "https://adcontextprotocol.org/schemas/latest/enums/daast-tracking-event.json": "caec0a9b0852c26a43a41c52312a36007656f7104c5af276d1111dddca9e2041", + "https://adcontextprotocol.org/schemas/latest/enums/daast-version.json": "95b06e213a2279ad3db0cca8912324a7773bc90a28bd0a39c676f73dcc4c70e2", + "https://adcontextprotocol.org/schemas/latest/enums/delivery-type.json": "3934bfba9af3ef9469f73b85b15c61c058b79771e74ae7bc387afc95905feacc", + "https://adcontextprotocol.org/schemas/latest/enums/dimension-unit.json": "405de49a9144c527466677ef3d3e5010b3cecea289f74b837958496da78045fb", + "https://adcontextprotocol.org/schemas/latest/enums/feed-format.json": "2147a55d3ab95d8108f6258d31c2ea3e37f32eef065af05236135600f4eb9c99", + "https://adcontextprotocol.org/schemas/latest/enums/feedback-source.json": "56283eec28ab10c8a11f09959c0f3dbef2b5bf02b7df84599343cb9fb0e69f78", + "https://adcontextprotocol.org/schemas/latest/enums/format-category.json": "12e5642d281d4657de9039e7a41af4ba72cc8cd3b0b72fc395f42d760a62789d", + "https://adcontextprotocol.org/schemas/latest/enums/format-id-parameter.json": "0783749a5ac1e2eff4cc523eec16e3a723e107b3718bd55e1bc671cf230b0a1f", + "https://adcontextprotocol.org/schemas/latest/enums/frequency-cap-scope.json": "3275a767cbde71235e7a23d0d775b5d58afb1859936393b04eb2c7c976ef2493", + "https://adcontextprotocol.org/schemas/latest/enums/history-entry-type.json": "e77c87be1493e84d83ffb567113303e2b50baa1dbd8c1ea791e2e8822f2bbf64", + "https://adcontextprotocol.org/schemas/latest/enums/http-method.json": "75c12b6e8868b0248841bbf8e666fd097308276f59a2c8f809fa279a88c06188", + "https://adcontextprotocol.org/schemas/latest/enums/identifier-types.json": "73190ea41c388af5b6b9c56358c4bce893b0f7cf61bc5062647f9987755451ed", + "https://adcontextprotocol.org/schemas/latest/enums/javascript-module-type.json": "da4746acdce608ce9497f59e7fdbc97a649367e55aebe4b9675feb3176e8525e", + "https://adcontextprotocol.org/schemas/latest/enums/landing-page-requirement.json": "5a09b76855c541b23ccf1aaa8ea87e77203d358b5c01c1b255722f21ac6327a4", + "https://adcontextprotocol.org/schemas/latest/enums/markdown-flavor.json": "df8836754968fb1c02fc7725aa1bce56e315c4cec11515a19058b5d6a0cf782b", + "https://adcontextprotocol.org/schemas/latest/enums/media-buy-status.json": "70bd2fca8832137c3b504c4b80934fdf8a57bcb45c4e1bf821fc49096aae53f9", + "https://adcontextprotocol.org/schemas/latest/enums/metric-type.json": "41eafcfaf6206670aabecd65abd5c1f2d0c4af019a3e4055d26aecfdccfd0a75", + "https://adcontextprotocol.org/schemas/latest/enums/notification-type.json": "be6d4177c9350e8fdced52773b483acf464beecd567934cf020e53c43e61afac", + "https://adcontextprotocol.org/schemas/latest/enums/pacing.json": "6fb8e62bfdc40bb48e0620ed71310f21a821fdfbe644c625d01b6a68ba4e40bb", + "https://adcontextprotocol.org/schemas/latest/enums/preview-output-format.json": "b14841f4e6689b3a3d2afa5740b4787ba49961d39ede2f9b92b709ad7970b32e", + "https://adcontextprotocol.org/schemas/latest/enums/pricing-model.json": "eb2aee267593d91e7f02ee0143fd8c2226021560fbee6b7ad983188f216f5f5a", + "https://adcontextprotocol.org/schemas/latest/enums/property-type.json": "aa431658c44beda5ead5e52977eeb4a2a0640a5e0be51027a769b39957910d53", + "https://adcontextprotocol.org/schemas/latest/enums/publisher-identifier-types.json": "3d4f8baf9ea57f525d1f126d58190588c17b05c7493ea2c9ff5737922cc96c4d", + "https://adcontextprotocol.org/schemas/latest/enums/reporting-frequency.json": "b480016e39a0b8a4493e55f5bda13b153e8df7c4c07ee49586cd77bdfdab0af5", + "https://adcontextprotocol.org/schemas/latest/enums/signal-catalog-type.json": "f899ae2550950b3e2269f42c20c50bfd09c1f2870fc656e334b73758bfbf2245", + "https://adcontextprotocol.org/schemas/latest/enums/sort-direction.json": "85a2cf23403297069995f14498a495c566a5a8bce4409d468d9071e18338ce6e", + "https://adcontextprotocol.org/schemas/latest/enums/standard-format-ids.json": "898565127bec42be24b65bff5ad0c650cd337601bc6b546bc7ee55d39e6f01e1", + "https://adcontextprotocol.org/schemas/latest/enums/task-status.json": "b45adb6b28b0b35ea5cf5564ec22d7b692e66c40d10d891d6dfe6661035c82b0", + "https://adcontextprotocol.org/schemas/latest/enums/task-type.json": "54a0ba43415b7948f3e0b1a61e06388b6eb239ce3a6073a9032c363d158c7905", + "https://adcontextprotocol.org/schemas/latest/enums/update-frequency.json": "657d0460d5675ddcd7edb4cf92655b56a93cd724ab3c0b4094241ef91ca644c8", + "https://adcontextprotocol.org/schemas/latest/enums/url-asset-type.json": "8cb3c557a43a0d5f85cd9c7b83366910c570349c6ab8625583ca66454fc88c03", + "https://adcontextprotocol.org/schemas/latest/enums/validation-mode.json": "0df6bb0c18921714d48d26c2813d6fa312c5a671e1a421f9c9f9ca0c0cfa6811", + "https://adcontextprotocol.org/schemas/latest/enums/vast-tracking-event.json": "c13b3a90cfa3f13f9446185a366095576b1b92574fc468c7de432c8c141167a8", + "https://adcontextprotocol.org/schemas/latest/enums/vast-version.json": "cf0f0e11eba22c9679b96f13608540016dfa3423101b8e22238ebb56abd224dd", + "https://adcontextprotocol.org/schemas/latest/enums/webhook-response-type.json": "72ba18db0a6fabd814195e895841e4cd6af5b54c4d6a954552c36284da5c4075", + "https://adcontextprotocol.org/schemas/latest/enums/webhook-security-method.json": "80c1a7622b750af3bcfd2695569a3b348e42d5c8614378c27a65f8c7374e9165", + "https://adcontextprotocol.org/schemas/latest/media-buy/build-creative-request.json": "12a7352bd4b0eb2ca856f9dd84dd611583a6f2bc5344890eb03c3496809dd439", + "https://adcontextprotocol.org/schemas/latest/media-buy/build-creative-response.json": "d07d4ef0e583b4bc20399bcaf204f28c16c0b30c701c13b9251c983312ecc858", + "https://adcontextprotocol.org/schemas/latest/media-buy/create-media-buy-async-response-input-required.json": "d0d53decd0e006bb66088898714692754eead832012f34e1e2d50b7b26d90d32", + "https://adcontextprotocol.org/schemas/latest/media-buy/create-media-buy-async-response-submitted.json": "8d9178048ee7f5adc46f9d27b9adb536dcd225f8ce17eea9e1f80edf3b815519", + "https://adcontextprotocol.org/schemas/latest/media-buy/create-media-buy-async-response-working.json": "e0f44bd7d3a9c3ef54ef672edf3a5806472498d2d89013ebe3ff731c25feba99", + "https://adcontextprotocol.org/schemas/latest/media-buy/create-media-buy-request.json": "af613bab7f81731e4bc3aefb7323fde499d1630453f79dc5a875e71ce11ed61a", + "https://adcontextprotocol.org/schemas/latest/media-buy/create-media-buy-response.json": "61565bb1c2edf344338a1e56d8a998c086f852a1973290674e9fee9ac3b967ff", + "https://adcontextprotocol.org/schemas/latest/media-buy/get-media-buy-delivery-request.json": "ef9de9c9f24de440c139a1205559ad1646aa38ea2665734502a979b09eab1af3", + "https://adcontextprotocol.org/schemas/latest/media-buy/get-media-buy-delivery-response.json": "f0ba34dd2b4086bcd7545bca0dcac05a465daccd0df6c8938b3a20331573167f", + "https://adcontextprotocol.org/schemas/latest/media-buy/get-products-async-response-input-required.json": "fa6f558932da62a48d53103044c23806cf6d3a8402aced8017791992a46f7e25", + "https://adcontextprotocol.org/schemas/latest/media-buy/get-products-async-response-submitted.json": "d96c9c3fd171ea9cb3bd3efdee11326862d90c114d73f373e68367d04a2c2f9a", + "https://adcontextprotocol.org/schemas/latest/media-buy/get-products-async-response-working.json": "bf028b7f3313f43c6f9d0ad4abfecf1dd7d8ddc581e797a1ee5ca8db84383f53", + "https://adcontextprotocol.org/schemas/latest/media-buy/get-products-request.json": "f2f28d9cdcce3d71919af01f998cdbf1680595e4afc7007d362647fdefc01004", + "https://adcontextprotocol.org/schemas/latest/media-buy/get-products-response.json": "6c236524c11b7b24b75c61b4a28507c2f863f6211dd6a8c68b141b0e145ec9fb", + "https://adcontextprotocol.org/schemas/latest/media-buy/list-authorized-properties-request.json": "be7ab0ee04f87158254e9eefb1ad26f35b97b09d590e29633269a24d62a28524", + "https://adcontextprotocol.org/schemas/latest/media-buy/list-authorized-properties-response.json": "628539fde80b56d671947d6ccf1a75c6bc8776be77948776bbf85fbedadfa6c9", + "https://adcontextprotocol.org/schemas/latest/media-buy/list-creative-formats-request.json": "69662d9fc33882ed6ee29d3639aa83c8c74cebcf5e445a21dd9ffb30ab479681", + "https://adcontextprotocol.org/schemas/latest/media-buy/list-creative-formats-response.json": "c296a680eaac7d9b74cb91bc3e7af0e26eb73cc2cc2228aed7d5ff3fb7d2c300", + "https://adcontextprotocol.org/schemas/latest/media-buy/list-creatives-request.json": "7ec7b0639acbf9488c4efddd36680674c0b05b7bf63e8b95ccd0e5476f849bbb", + "https://adcontextprotocol.org/schemas/latest/media-buy/list-creatives-response.json": "e50d9af1d6ddbfad7caeb022bdaa087db73e327155d9b5aa65e63ff8c6a1d67a", + "https://adcontextprotocol.org/schemas/latest/media-buy/package-request.json": "da9a6574d35c873cd3305e2b9d58d89f7aa0cc8f9eff0791dd13aa82e975c38d", + "https://adcontextprotocol.org/schemas/latest/media-buy/provide-performance-feedback-request.json": "b418295130bbe51d7c75e06e4279d2391b645fb5a6edb75edb3a9c15196c5db9", + "https://adcontextprotocol.org/schemas/latest/media-buy/provide-performance-feedback-response.json": "011bd01bef32f55a17d645636f0a29316fb55ab185714660456e5a61a926d575", + "https://adcontextprotocol.org/schemas/latest/media-buy/sync-creatives-async-response-input-required.json": "cee56e6ea91d5718cd300ab69c56002c782bd6b1bb1cbacdcfa112c43c6097f7", + "https://adcontextprotocol.org/schemas/latest/media-buy/sync-creatives-async-response-submitted.json": "3a4ce2ea5bdbfa0e81136b03977c19a5b5d2efda9b29bd68a6ca7b6dc944a9ae", + "https://adcontextprotocol.org/schemas/latest/media-buy/sync-creatives-async-response-working.json": "ee9c1215472361664f4b44149c524672c191bfd45474f437d63ef145891a053b", + "https://adcontextprotocol.org/schemas/latest/media-buy/sync-creatives-request.json": "ff8c89191ea0fa2d1469adbd97647cb098f68b646a7090e96815004082d73d1e", + "https://adcontextprotocol.org/schemas/latest/media-buy/sync-creatives-response.json": "9227bda293c14a4734290727a2a5cd13af690c8b9a0f107ffc3b664390b10b58", + "https://adcontextprotocol.org/schemas/latest/media-buy/update-media-buy-async-response-input-required.json": "3856eabfb464b5c9141fc80504810d10438b2883ed184908b6be6b0f3da2be50", + "https://adcontextprotocol.org/schemas/latest/media-buy/update-media-buy-async-response-submitted.json": "b843ded24cb2fb16c89050a534e075f52ba6ae2e11f726597b681fbdb2469bef", + "https://adcontextprotocol.org/schemas/latest/media-buy/update-media-buy-async-response-working.json": "dcf9cc0bbd5d23360b19a2da6da7b9650d7e2892a2e835339e421cbb8c12a328", + "https://adcontextprotocol.org/schemas/latest/media-buy/update-media-buy-request.json": "6810c163394eeb17f39be725aea20d5b1fdc1a363f2233055f92fd775ba96ae2", + "https://adcontextprotocol.org/schemas/latest/media-buy/update-media-buy-response.json": "2584822ce5f97f5ee2d19d61785906565d67d35631bebb13f7417fb7e3db8c03", + "https://adcontextprotocol.org/schemas/latest/pricing-options/cpc-option.json": "be10d3c12cd0d426496edf51b0769845f92000aa408db5bf049e89ce256dd19a", + "https://adcontextprotocol.org/schemas/latest/pricing-options/cpcv-option.json": "b0778b4dd666776d74565b29036bc4526395242ad4a08fbcab61794af2c68d9f", + "https://adcontextprotocol.org/schemas/latest/pricing-options/cpm-auction-option.json": "9c5db0364f491ca2ea90f9e6ecbfcef4765751c68084e94d043d63a52bd0be84", + "https://adcontextprotocol.org/schemas/latest/pricing-options/cpm-fixed-option.json": "2cc4697cc15db1f397d3b60c7115841592d730601e90ce5e2790e6e6866e270b", + "https://adcontextprotocol.org/schemas/latest/pricing-options/cpp-option.json": "a53067bf702a5c91423206ab6dee778696ce83efdcec7fd0b8bfe0e3b5f1263f", + "https://adcontextprotocol.org/schemas/latest/pricing-options/cpv-option.json": "dc391dba257bade8a3ed3271b51156e6b90830affa23aad94887c2fb35475b15", + "https://adcontextprotocol.org/schemas/latest/pricing-options/flat-rate-option.json": "ad3d86d9c8487a536738d0569498a1cfaa266b5f7ba2961a31c5da7819ae892e", + "https://adcontextprotocol.org/schemas/latest/pricing-options/vcpm-auction-option.json": "7908d73566f4fe54e0ad939cb2384b6ee07572183a9c9707a99f69f923e80a3b", + "https://adcontextprotocol.org/schemas/latest/pricing-options/vcpm-fixed-option.json": "7669e82e83d98650c154d5624378cfe600dd77eac5a1811014275bf168732666", + "https://adcontextprotocol.org/schemas/latest/protocols/adcp-extension.json": "67b25add68afab242ffa1e8477d030e3c805cc7e0bbf7162bb8f871d3e0b854d", + "https://adcontextprotocol.org/schemas/latest/signals/activate-signal-request.json": "5cd48cc897a5cae9cc7616d88596f578fc73cd184538206ce765c9daff2894e8", + "https://adcontextprotocol.org/schemas/latest/signals/activate-signal-response.json": "a4abaedbc47651c2bdbbde8bc2bedb3dcde2eaf1ca2dc9a369fa04a0a02dd56b", + "https://adcontextprotocol.org/schemas/latest/signals/get-signals-request.json": "8fa9b0270ae644a664ccf494696b6f1d4ca6afccbb11ffa2ceb89b42a2073016", + "https://adcontextprotocol.org/schemas/latest/signals/get-signals-response.json": "e8398ddb7b03e2b4e323c9e5e0f98c18173bc9ca56c3f4874e0bfe29122a4b3a" } \ No newline at end of file diff --git a/schemas/cache/adagents.json b/schemas/cache/adagents.json index 922232c7..ae355994 100644 --- a/schemas/cache/adagents.json +++ b/schemas/cache/adagents.json @@ -3,12 +3,12 @@ "description": "Declaration of authorized sales agents for advertising inventory. Hosted at /.well-known/adagents.json on publisher domains. Can either contain the full structure inline or reference an authoritative URL.", "examples": [ { - "$schema": "https://adcontextprotocol.org/schemas/adagents.json", + "$schema": "/schemas/latest/adagents.json", "authoritative_location": "https://cdn.example.com/adagents/v2/adagents.json", "last_updated": "2025-01-15T10:00:00Z" }, { - "$schema": "https://adcontextprotocol.org/schemas/adagents.json", + "$schema": "/schemas/latest/adagents.json", "authorized_agents": [ { "authorization_type": "property_tags", @@ -41,7 +41,7 @@ } }, { - "$schema": "https://adcontextprotocol.org/schemas/adagents.json", + "$schema": "/schemas/latest/adagents.json", "authorized_agents": [ { "authorization_type": "property_tags", @@ -135,7 +135,7 @@ } }, { - "$schema": "https://adcontextprotocol.org/schemas/adagents.json", + "$schema": "/schemas/latest/adagents.json", "authorized_agents": [ { "authorization_type": "property_tags", @@ -174,7 +174,7 @@ } }, { - "$schema": "https://adcontextprotocol.org/schemas/adagents.json", + "$schema": "/schemas/latest/adagents.json", "authorized_agents": [ { "authorization_type": "publisher_properties", @@ -226,7 +226,6 @@ "description": "URL reference variant - points to the authoritative location of the adagents.json file", "properties": { "$schema": { - "default": "https://adcontextprotocol.org/schemas/adagents.json", "description": "JSON Schema identifier for this adagents.json file", "type": "string" }, @@ -252,7 +251,6 @@ "description": "Inline structure variant - contains full agent authorization data", "properties": { "$schema": { - "default": "https://adcontextprotocol.org/schemas/adagents.json", "description": "JSON Schema identifier for this adagents.json file", "type": "string" }, diff --git a/schemas/cache/core/assets/image-asset.json b/schemas/cache/core/assets/image-asset.json index 201362d2..e2aa4840 100644 --- a/schemas/cache/core/assets/image-asset.json +++ b/schemas/cache/core/assets/image-asset.json @@ -1,32 +1,37 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "allOf": [ - { - "$ref": "../../core/dimensions.json" + "additionalProperties": false, + "description": "Image asset with URL and dimensions", + "properties": { + "alt_text": { + "description": "Alternative text for accessibility", + "type": "string" + }, + "format": { + "description": "Image file format (jpg, png, gif, webp, etc.)", + "type": "string" + }, + "height": { + "description": "Height in pixels", + "minimum": 1, + "type": "integer" }, - { - "additionalProperties": false, - "properties": { - "alt_text": { - "description": "Alternative text for accessibility", - "type": "string" - }, - "format": { - "description": "Image file format (jpg, png, gif, webp, etc.)", - "type": "string" - }, - "url": { - "description": "URL to the image asset", - "format": "uri", - "type": "string" - } - }, - "required": [ - "url" - ], - "type": "object" + "url": { + "description": "URL to the image asset", + "format": "uri", + "type": "string" + }, + "width": { + "description": "Width in pixels", + "minimum": 1, + "type": "integer" } + }, + "required": [ + "url", + "width", + "height" ], - "description": "Image asset with URL and dimensions", - "title": "Image Asset" + "title": "Image Asset", + "type": "object" } \ No newline at end of file diff --git a/schemas/cache/core/assets/video-asset.json b/schemas/cache/core/assets/video-asset.json index 9b93714f..de1747ad 100644 --- a/schemas/cache/core/assets/video-asset.json +++ b/schemas/cache/core/assets/video-asset.json @@ -1,38 +1,43 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "allOf": [ - { - "$ref": "../../core/dimensions.json" + "additionalProperties": false, + "description": "Video asset with URL and specifications", + "properties": { + "bitrate_kbps": { + "description": "Video bitrate in kilobits per second", + "minimum": 1, + "type": "integer" + }, + "duration_ms": { + "description": "Video duration in milliseconds", + "minimum": 1, + "type": "integer" + }, + "format": { + "description": "Video file format (mp4, webm, mov, etc.)", + "type": "string" }, - { - "additionalProperties": false, - "properties": { - "bitrate_kbps": { - "description": "Video bitrate in kilobits per second", - "minimum": 1, - "type": "integer" - }, - "duration_ms": { - "description": "Video duration in milliseconds", - "minimum": 1, - "type": "integer" - }, - "format": { - "description": "Video file format (mp4, webm, mov, etc.)", - "type": "string" - }, - "url": { - "description": "URL to the video asset", - "format": "uri", - "type": "string" - } - }, - "required": [ - "url" - ], - "type": "object" + "height": { + "description": "Height in pixels", + "minimum": 1, + "type": "integer" + }, + "url": { + "description": "URL to the video asset", + "format": "uri", + "type": "string" + }, + "width": { + "description": "Width in pixels", + "minimum": 1, + "type": "integer" } + }, + "required": [ + "url", + "width", + "height" ], - "description": "Video asset with URL and specifications", - "title": "Video Asset" + "title": "Video Asset", + "type": "object" } \ No newline at end of file diff --git a/schemas/cache/core/async-response-data.json b/schemas/cache/core/async-response-data.json new file mode 100644 index 00000000..26ac5de9 --- /dev/null +++ b/schemas/cache/core/async-response-data.json @@ -0,0 +1,87 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "$ref": "../media-buy/get-products-response.json", + "description": "Response for completed or failed get_products", + "title": "GetProductsResponse" + }, + { + "$ref": "../media-buy/get-products-async-response-working.json", + "description": "Progress data for working get_products", + "title": "GetProductsAsyncWorking" + }, + { + "$ref": "../media-buy/get-products-async-response-input-required.json", + "description": "Input requirements for get_products needing clarification", + "title": "GetProductsAsyncInputRequired" + }, + { + "$ref": "../media-buy/get-products-async-response-submitted.json", + "description": "Acknowledgment for submitted get_products (custom curation)", + "title": "GetProductsAsyncSubmitted" + }, + { + "$ref": "../media-buy/create-media-buy-response.json", + "description": "Response for completed or failed create_media_buy", + "title": "CreateMediaBuyResponse" + }, + { + "$ref": "../media-buy/create-media-buy-async-response-working.json", + "description": "Progress data for working create_media_buy", + "title": "CreateMediaBuyAsyncWorking" + }, + { + "$ref": "../media-buy/create-media-buy-async-response-input-required.json", + "description": "Input requirements for create_media_buy needing user input", + "title": "CreateMediaBuyAsyncInputRequired" + }, + { + "$ref": "../media-buy/create-media-buy-async-response-submitted.json", + "description": "Acknowledgment for submitted create_media_buy", + "title": "CreateMediaBuyAsyncSubmitted" + }, + { + "$ref": "../media-buy/update-media-buy-response.json", + "description": "Response for completed or failed update_media_buy", + "title": "UpdateMediaBuyResponse" + }, + { + "$ref": "../media-buy/update-media-buy-async-response-working.json", + "description": "Progress data for working update_media_buy", + "title": "UpdateMediaBuyAsyncWorking" + }, + { + "$ref": "../media-buy/update-media-buy-async-response-input-required.json", + "description": "Input requirements for update_media_buy needing user input", + "title": "UpdateMediaBuyAsyncInputRequired" + }, + { + "$ref": "../media-buy/update-media-buy-async-response-submitted.json", + "description": "Acknowledgment for submitted update_media_buy", + "title": "UpdateMediaBuyAsyncSubmitted" + }, + { + "$ref": "../media-buy/sync-creatives-response.json", + "description": "Response for completed or failed sync_creatives", + "title": "SyncCreativesResponse" + }, + { + "$ref": "../media-buy/sync-creatives-async-response-working.json", + "description": "Progress data for working sync_creatives", + "title": "SyncCreativesAsyncWorking" + }, + { + "$ref": "../media-buy/sync-creatives-async-response-input-required.json", + "description": "Input requirements for sync_creatives needing user input", + "title": "SyncCreativesAsyncInputRequired" + }, + { + "$ref": "../media-buy/sync-creatives-async-response-submitted.json", + "description": "Acknowledgment for submitted sync_creatives", + "title": "SyncCreativesAsyncSubmitted" + } + ], + "description": "Union of all possible data payloads for async task webhook responses. For completed/failed statuses, use the main task response schema. For working/input-required/submitted, use the status-specific schemas.", + "title": "AdCP Async Response Data" +} \ No newline at end of file diff --git a/schemas/cache/core/dimensions.json b/schemas/cache/core/dimensions.json deleted file mode 100644 index 2d36c9f6..00000000 --- a/schemas/cache/core/dimensions.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "description": "Pixel dimensions for visual formats (width and height must be specified together)", - "properties": { - "height": { - "description": "Height in pixels", - "minimum": 1, - "type": "integer" - }, - "width": { - "description": "Width in pixels", - "minimum": 1, - "type": "integer" - } - }, - "required": [ - "width", - "height" - ], - "title": "Dimensions", - "type": "object" -} \ No newline at end of file diff --git a/schemas/cache/core/mcp-webhook-payload.json b/schemas/cache/core/mcp-webhook-payload.json new file mode 100644 index 00000000..b0c9abce --- /dev/null +++ b/schemas/cache/core/mcp-webhook-payload.json @@ -0,0 +1,151 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": true, + "description": "Standard envelope for HTTP-based push notifications (MCP). This defines the wire format sent to the URL configured in `pushNotificationConfig`. NOTE: This envelope is NOT used in A2A integration, which uses native Task/TaskStatusUpdateEvent messages with the AdCP payload nested in `status.message.parts[].data`.", + "examples": [ + { + "data": { + "context_id": "ctx_abc123", + "domain": "media-buy", + "message": "Campaign budget $150K requires VP approval to proceed", + "operation_id": "op_456", + "result": { + "errors": [ + { + "code": "APPROVAL_REQUIRED", + "field": "total_budget", + "message": "Budget exceeds auto-approval threshold" + } + ], + "reason": "BUDGET_EXCEEDS_LIMIT" + }, + "status": "input-required", + "task_id": "task_456", + "task_type": "create_media_buy", + "timestamp": "2025-01-22T10:15:00Z" + }, + "description": "Webhook for input-required status (human approval needed)" + }, + { + "data": { + "domain": "media-buy", + "message": "Media buy created successfully with 2 packages ready for creative assignment", + "operation_id": "op_456", + "result": { + "buyer_ref": "nike_q1_campaign_2024", + "creative_deadline": "2024-01-30T23:59:59Z", + "media_buy_id": "mb_12345", + "packages": [ + { + "budget": 60000, + "buyer_ref": "nike_ctv_package", + "creative_assignments": [], + "format_ids_to_provide": [ + { + "agent_url": "https://creative.adcontextprotocol.org", + "id": "video_standard_30s" + } + ], + "pacing": "even", + "package_id": "pkg_12345_001", + "paused": false, + "pricing_option_id": "cpm-fixed-sports", + "product_id": "ctv_sports_premium" + } + ] + }, + "status": "completed", + "task_id": "task_456", + "task_type": "create_media_buy", + "timestamp": "2025-01-22T10:30:00Z" + }, + "description": "Webhook for completed create_media_buy" + }, + { + "data": { + "domain": "media-buy", + "message": "Validating inventory availability...", + "operation_id": "op_456", + "result": { + "current_step": "inventory_validation", + "percentage": 50, + "step_number": 2, + "total_steps": 4 + }, + "status": "working", + "task_id": "task_456", + "task_type": "create_media_buy", + "timestamp": "2025-01-22T10:20:00Z" + }, + "description": "Webhook for working status with progress" + }, + { + "data": { + "domain": "media-buy", + "message": "Creative sync failed due to invalid asset URLs", + "operation_id": "op_789", + "result": { + "errors": [ + { + "code": "INVALID_ASSET_URL", + "field": "creatives[0].asset_url", + "message": "One or more creative assets could not be accessed" + } + ] + }, + "status": "failed", + "task_id": "task_789", + "task_type": "sync_creatives", + "timestamp": "2025-01-22T10:46:00Z" + }, + "description": "Webhook for failed sync_creatives" + } + ], + "properties": { + "context_id": { + "description": "Session/conversation identifier. Use this to continue the conversation if input-required status needs clarification or additional parameters.", + "type": "string" + }, + "domain": { + "$ref": "../enums/adcp-domain.json", + "description": "AdCP domain this task belongs to. Helps classify the operation type at a high level." + }, + "message": { + "description": "Human-readable summary of the current task state. Provides context about what happened and what action may be needed.", + "type": "string" + }, + "operation_id": { + "description": "Publisher-defined operation identifier correlating a sequence of task updates across webhooks.", + "type": "string" + }, + "result": { + "$ref": "async-response-data.json", + "description": "Task-specific payload matching the status. For completed/failed, contains the full task response. For working/input-required/submitted, contains status-specific data. This is the data layer that AdCP specs - same structure used in A2A status.message.parts[].data." + }, + "status": { + "$ref": "../enums/task-status.json", + "description": "Current task status. Webhooks are triggered for status changes after initial submission." + }, + "task_id": { + "description": "Unique identifier for this task. Use this to correlate webhook notifications with the original task submission.", + "type": "string" + }, + "task_type": { + "$ref": "../enums/task-type.json", + "description": "Type of AdCP operation that triggered this webhook. Enables webhook handlers to route to appropriate processing logic." + }, + "timestamp": { + "description": "ISO 8601 timestamp when this webhook was generated.", + "format": "date-time", + "type": "string" + } + }, + "required": [ + "task_id", + "task_type", + "status", + "timestamp" + ], + "title": "MCP Webhook Payload", + "type": "object" +} \ No newline at end of file diff --git a/schemas/cache/core/push-notification-config.json b/schemas/cache/core/push-notification-config.json index 2f0b6c67..eb53ac39 100644 --- a/schemas/cache/core/push-notification-config.json +++ b/schemas/cache/core/push-notification-config.json @@ -1,7 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "description": "Webhook configuration for asynchronous task notifications. Uses A2A-compatible PushNotificationConfig structure. Supports Bearer tokens (simple) or HMAC signatures (production-recommended).", + "description": "Webhook configuration for asynchronous task notifications. Uses A2A-compatible PushNotificationConfig structure. Supports Bearer tokens (simple) or HMAC signatures (production-recommended). This schema is designed for composition via allOf - consuming schemas should define their own additionalProperties constraints.", "properties": { "authentication": { "additionalProperties": false, diff --git a/schemas/cache/core/webhook-payload.json b/schemas/cache/core/webhook-payload.json deleted file mode 100644 index d70fbd93..00000000 --- a/schemas/cache/core/webhook-payload.json +++ /dev/null @@ -1,249 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": true, - "allOf": [ - { - "if": { - "properties": { - "task_type": { - "const": "create_media_buy" - } - } - }, - "then": { - "properties": { - "result": { - "$ref": "../media-buy/create-media-buy-response.json" - } - } - } - }, - { - "if": { - "properties": { - "task_type": { - "const": "update_media_buy" - } - } - }, - "then": { - "properties": { - "result": { - "$ref": "../media-buy/update-media-buy-response.json" - } - } - } - }, - { - "if": { - "properties": { - "task_type": { - "const": "sync_creatives" - } - } - }, - "then": { - "properties": { - "result": { - "$ref": "../media-buy/sync-creatives-response.json" - } - } - } - }, - { - "if": { - "properties": { - "task_type": { - "const": "activate_signal" - } - } - }, - "then": { - "properties": { - "result": { - "$ref": "../signals/activate-signal-response.json" - } - } - } - }, - { - "if": { - "properties": { - "task_type": { - "const": "get_signals" - } - } - }, - "then": { - "properties": { - "result": { - "$ref": "../signals/get-signals-response.json" - } - } - } - } - ], - "description": "Payload structure sent to webhook endpoints when async task status changes. Protocol-level fields are at the top level and the task-specific payload is nested under the 'result' field. This schema represents what your webhook handler will receive when a task transitions from 'submitted' to a terminal or intermediate state.", - "examples": [ - { - "data": { - "context_id": "ctx_abc123", - "domain": "media-buy", - "message": "Campaign budget $150K requires VP approval to proceed", - "operation_id": "op_456", - "result": { - "errors": [ - { - "code": "APPROVAL_REQUIRED", - "field": "packages[0].budget", - "message": "Budget exceeds auto-approval threshold of $100K. Awaiting VP approval before media buy creation." - } - ] - }, - "status": "input-required", - "task_id": "task_456", - "task_type": "create_media_buy", - "timestamp": "2025-01-22T10:15:00Z" - }, - "description": "Webhook for input-required status (human approval needed)" - }, - { - "data": { - "domain": "media-buy", - "message": "Media buy created successfully with 2 packages ready for creative assignment", - "operation_id": "op_456", - "result": { - "buyer_ref": "nike_q1_campaign_2024", - "creative_deadline": "2024-01-30T23:59:59Z", - "media_buy_id": "mb_12345", - "packages": [ - { - "budget": 60000, - "buyer_ref": "nike_ctv_package", - "creative_assignments": [], - "format_ids_to_provide": [ - { - "agent_url": "https://creative.adcontextprotocol.org", - "id": "video_standard_30s" - } - ], - "pacing": "even", - "package_id": "pkg_12345_001", - "paused": false, - "pricing_option_id": "cpm-fixed-sports", - "product_id": "ctv_sports_premium" - } - ] - }, - "status": "completed", - "task_id": "task_456", - "task_type": "create_media_buy", - "timestamp": "2025-01-22T10:30:00Z" - }, - "description": "Webhook for completed create_media_buy" - }, - { - "data": { - "domain": "media-buy", - "error": "invalid_assets: One or more creative assets could not be accessed", - "message": "Creative sync failed due to invalid asset URLs", - "operation_id": "op_789", - "status": "failed", - "task_id": "task_789", - "task_type": "sync_creatives", - "timestamp": "2025-01-22T10:46:00Z" - }, - "description": "Webhook for failed sync_creatives" - } - ], - "notes": [ - "Webhooks are ONLY triggered when the initial response status is 'submitted' (long-running operations)", - "Webhook payloads include protocol-level fields (operation_id, task_type, status, optional task_id/context_id/timestamp/message) and the task-specific payload nested under 'result'", - "The task-specific response data is NOT merged at the top level; it is contained entirely within the 'result' field", - "For example, a create_media_buy webhook will include operation_id, task_type, status, and result.buyer_ref, result.media_buy_id, result.packages, etc.", - "Your webhook handler receives the complete information needed to process the result without making additional API calls" - ], - "properties": { - "context_id": { - "description": "Session/conversation identifier. Use this to continue the conversation if input-required status needs clarification or additional parameters.", - "type": "string" - }, - "domain": { - "$ref": "../enums/adcp-domain.json", - "description": "AdCP domain this task belongs to. Helps classify the operation type at a high level." - }, - "error": { - "description": "Error message for failed tasks. Only present when status is 'failed'.", - "type": [ - "string", - "null" - ] - }, - "message": { - "description": "Human-readable summary of the current task state. Provides context about what happened and what action may be needed.", - "type": "string" - }, - "operation_id": { - "description": "Publisher-defined operation identifier correlating a sequence of task updates across webhooks.", - "type": "string" - }, - "progress": { - "additionalProperties": false, - "description": "Progress information for tasks still in 'working' state. Rarely seen in webhooks since 'working' tasks typically complete synchronously, but may appear if a task transitions from 'submitted' to 'working'.", - "properties": { - "current_step": { - "description": "Current step or phase of the operation", - "type": "string" - }, - "percentage": { - "description": "Completion percentage (0-100)", - "maximum": 100, - "minimum": 0, - "type": "number" - }, - "step_number": { - "description": "Current step number", - "minimum": 1, - "type": "integer" - }, - "total_steps": { - "description": "Total number of steps in the operation", - "minimum": 1, - "type": "integer" - } - }, - "type": "object" - }, - "result": { - "description": "Task-specific payload for this status update. Validated against the appropriate response schema based on task_type.", - "type": [ - "object" - ] - }, - "status": { - "$ref": "../enums/task-status.json", - "description": "Current task status. Webhooks are only triggered for status changes after initial submission (e.g., submitted \u2192 input-required, submitted \u2192 completed, submitted \u2192 failed)." - }, - "task_id": { - "description": "Unique identifier for this task. Use this to correlate webhook notifications with the original task submission.", - "type": "string" - }, - "task_type": { - "$ref": "../enums/task-type.json", - "description": "Type of AdCP operation that triggered this webhook. Enables webhook handlers to route to appropriate processing logic." - }, - "timestamp": { - "description": "ISO 8601 timestamp when this webhook was generated.", - "format": "date-time", - "type": "string" - } - }, - "required": [ - "task_id", - "task_type", - "status", - "timestamp" - ], - "title": "Webhook Payload", - "type": "object" -} \ No newline at end of file diff --git a/schemas/cache/creative/asset-types/index.json b/schemas/cache/creative/asset-types/index.json index 7d67596c..aa6c6340 100644 --- a/schemas/cache/creative/asset-types/index.json +++ b/schemas/cache/creative/asset-types/index.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "adcp_version": "2.5.0", + "adcp_version": "latest", "architecture": { "format_aware_validation": { "description": "Creative manifests are validated in the context of their format specification", @@ -89,9 +89,9 @@ "typical_use": "DCO (Dynamic Creative Optimization), real-time personalization, server-side rendering" } }, - "baseUrl": "/schemas/2.5.0", + "baseUrl": "/schemas/latest", "description": "Registry of asset types used in AdCP creative manifests. Each asset type defines the structure of actual content payloads (what you send), not requirements or constraints (which belong in format specifications).", - "lastUpdated": "2025-11-22", + "lastUpdated": "2025-12-18", "title": "AdCP Asset Type Registry", "usage_notes": { "creative_manifests": "Creative manifests provide actual asset content, keyed by asset_id from the format. Asset type is determined by the format specification, not declared in the payload.", @@ -100,6 +100,6 @@ }, "version": "1.0.0", "versioning": { - "note": "AdCP uses build-time versioning. This directory contains schemas for AdCP 2.5.0. Full semantic versions are available at /schemas/{version}/ (e.g., /schemas/2.5.0/). Major version aliases point to the latest release: /schemas/v2/ \u2192 /schemas/2.5.0/." + "note": "AdCP uses build-time versioning. This directory contains schemas for AdCP latest. Full semantic versions are available at /schemas/{version}/ (e.g., /schemas/2.5.0/). Major version aliases point to the latest release: /schemas/vlatest/ \u2192 /schemas/latest/." } } \ No newline at end of file diff --git a/schemas/cache/creative/preview-creative-response.json b/schemas/cache/creative/preview-creative-response.json index 9f8a69c7..3290efd3 100644 --- a/schemas/cache/creative/preview-creative-response.json +++ b/schemas/cache/creative/preview-creative-response.json @@ -84,6 +84,7 @@ "previews", "expires_at" ], + "title": "PreviewCreativeSingleResponse", "type": "object" }, { @@ -114,7 +115,8 @@ }, "required": [ "response" - ] + ], + "title": "PreviewBatchResultSuccess" }, { "properties": { @@ -125,7 +127,8 @@ }, "required": [ "error" - ] + ], + "title": "PreviewBatchResultError" } ], "properties": { @@ -233,6 +236,7 @@ "response_type", "results" ], + "title": "PreviewCreativeBatchResponse", "type": "object" } ], diff --git a/schemas/cache/media-buy/build-creative-response.json b/schemas/cache/media-buy/build-creative-response.json index 2400d117..5ee757c2 100644 --- a/schemas/cache/media-buy/build-creative-response.json +++ b/schemas/cache/media-buy/build-creative-response.json @@ -25,6 +25,7 @@ "required": [ "creative_manifest" ], + "title": "BuildCreativeSuccess", "type": "object" }, { @@ -54,6 +55,7 @@ "required": [ "errors" ], + "title": "BuildCreativeError", "type": "object" } ], diff --git a/schemas/cache/media-buy/create-media-buy-async-response-input-required.json b/schemas/cache/media-buy/create-media-buy-async-response-input-required.json new file mode 100644 index 00000000..61dfab09 --- /dev/null +++ b/schemas/cache/media-buy/create-media-buy-async-response-input-required.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "description": "Payload when task is paused waiting for user input or approval.", + "properties": { + "context": { + "$ref": "../core/context.json" + }, + "errors": { + "description": "Optional validation errors or warnings for debugging purposes. Helps explain why input is required.", + "items": { + "$ref": "../core/error.json" + }, + "type": "array" + }, + "ext": { + "$ref": "../core/ext.json" + }, + "reason": { + "description": "Reason code indicating why input is needed", + "enum": [ + "APPROVAL_REQUIRED", + "BUDGET_EXCEEDS_LIMIT" + ], + "type": "string" + } + }, + "title": "Create Media Buy - Input Required", + "type": "object" +} \ No newline at end of file diff --git a/schemas/cache/media-buy/create-media-buy-async-response-submitted.json b/schemas/cache/media-buy/create-media-buy-async-response-submitted.json new file mode 100644 index 00000000..306f1889 --- /dev/null +++ b/schemas/cache/media-buy/create-media-buy-async-response-submitted.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "description": "Payload acknowledging the task is queued. Usually empty or just context.", + "properties": { + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "title": "Create Media Buy - Submitted", + "type": "object" +} \ No newline at end of file diff --git a/schemas/cache/media-buy/create-media-buy-async-response-working.json b/schemas/cache/media-buy/create-media-buy-async-response-working.json new file mode 100644 index 00000000..aa8469c2 --- /dev/null +++ b/schemas/cache/media-buy/create-media-buy-async-response-working.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "description": "Progress payload for active create_media_buy task.", + "properties": { + "context": { + "$ref": "../core/context.json" + }, + "current_step": { + "description": "Current step or phase of the operation", + "type": "string" + }, + "ext": { + "$ref": "../core/ext.json" + }, + "percentage": { + "description": "Completion percentage (0-100)", + "maximum": 100, + "minimum": 0, + "type": "number" + }, + "step_number": { + "description": "Current step number", + "minimum": 1, + "type": "integer" + }, + "total_steps": { + "description": "Total number of steps in the operation", + "minimum": 1, + "type": "integer" + } + }, + "title": "Create Media Buy - Working", + "type": "object" +} \ No newline at end of file diff --git a/schemas/cache/media-buy/create-media-buy-response.json b/schemas/cache/media-buy/create-media-buy-response.json index bed4bc75..0e869af2 100644 --- a/schemas/cache/media-buy/create-media-buy-response.json +++ b/schemas/cache/media-buy/create-media-buy-response.json @@ -43,6 +43,7 @@ "buyer_ref", "packages" ], + "title": "CreateMediaBuySuccess", "type": "object" }, { @@ -86,6 +87,7 @@ "required": [ "errors" ], + "title": "CreateMediaBuyError", "type": "object" } ], diff --git a/schemas/cache/media-buy/get-products-async-response-input-required.json b/schemas/cache/media-buy/get-products-async-response-input-required.json new file mode 100644 index 00000000..95343e44 --- /dev/null +++ b/schemas/cache/media-buy/get-products-async-response-input-required.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "description": "Payload when search is paused waiting for user clarification.", + "properties": { + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + }, + "partial_results": { + "description": "Partial product results that may help inform the clarification", + "items": { + "$ref": "../core/product.json" + }, + "type": "array" + }, + "reason": { + "description": "Reason code indicating why input is needed", + "enum": [ + "CLARIFICATION_NEEDED", + "BUDGET_REQUIRED" + ], + "type": "string" + }, + "suggestions": { + "description": "Suggested values or options for the required input", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "title": "Get Products - Input Required", + "type": "object" +} \ No newline at end of file diff --git a/schemas/cache/media-buy/get-products-async-response-submitted.json b/schemas/cache/media-buy/get-products-async-response-submitted.json new file mode 100644 index 00000000..57d8599c --- /dev/null +++ b/schemas/cache/media-buy/get-products-async-response-submitted.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "description": "Payload acknowledging the search is queued. Usually for custom/bespoke product curation.", + "properties": { + "context": { + "$ref": "../core/context.json" + }, + "estimated_completion": { + "description": "Estimated completion time for the search", + "format": "date-time", + "type": "string" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "title": "Get Products - Submitted", + "type": "object" +} \ No newline at end of file diff --git a/schemas/cache/media-buy/get-products-async-response-working.json b/schemas/cache/media-buy/get-products-async-response-working.json new file mode 100644 index 00000000..67343fd5 --- /dev/null +++ b/schemas/cache/media-buy/get-products-async-response-working.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "description": "Progress payload for active get_products task.", + "properties": { + "context": { + "$ref": "../core/context.json" + }, + "current_step": { + "description": "Current step in the search process (e.g., 'searching_inventory', 'validating_availability')", + "type": "string" + }, + "ext": { + "$ref": "../core/ext.json" + }, + "percentage": { + "description": "Progress percentage of the search operation", + "maximum": 100, + "minimum": 0, + "type": "number" + }, + "step_number": { + "description": "Current step number (1-indexed)", + "type": "integer" + }, + "total_steps": { + "description": "Total number of steps in the search process", + "type": "integer" + } + }, + "title": "Get Products - Working", + "type": "object" +} \ No newline at end of file diff --git a/schemas/cache/media-buy/provide-performance-feedback-response.json b/schemas/cache/media-buy/provide-performance-feedback-response.json index e33a21e8..f32a3fd1 100644 --- a/schemas/cache/media-buy/provide-performance-feedback-response.json +++ b/schemas/cache/media-buy/provide-performance-feedback-response.json @@ -26,6 +26,7 @@ "required": [ "success" ], + "title": "ProvidePerformanceFeedbackSuccess", "type": "object" }, { @@ -55,6 +56,7 @@ "required": [ "errors" ], + "title": "ProvidePerformanceFeedbackError", "type": "object" } ], diff --git a/schemas/cache/media-buy/sync-creatives-async-response-input-required.json b/schemas/cache/media-buy/sync-creatives-async-response-input-required.json new file mode 100644 index 00000000..4c803b36 --- /dev/null +++ b/schemas/cache/media-buy/sync-creatives-async-response-input-required.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "description": "Payload when sync_creatives task is paused waiting for buyer clarification or approval.", + "properties": { + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + }, + "reason": { + "description": "Reason code indicating why buyer input is needed", + "enum": [ + "APPROVAL_REQUIRED", + "ASSET_CONFIRMATION", + "FORMAT_CLARIFICATION" + ], + "type": "string" + } + }, + "title": "Sync Creatives - Input Required", + "type": "object" +} \ No newline at end of file diff --git a/schemas/cache/media-buy/sync-creatives-async-response-submitted.json b/schemas/cache/media-buy/sync-creatives-async-response-submitted.json new file mode 100644 index 00000000..364dfcb9 --- /dev/null +++ b/schemas/cache/media-buy/sync-creatives-async-response-submitted.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "description": "Payload acknowledging sync_creatives task is queued for processing.", + "properties": { + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "title": "Sync Creatives - Submitted", + "type": "object" +} \ No newline at end of file diff --git a/schemas/cache/media-buy/sync-creatives-async-response-working.json b/schemas/cache/media-buy/sync-creatives-async-response-working.json new file mode 100644 index 00000000..b92ba7e3 --- /dev/null +++ b/schemas/cache/media-buy/sync-creatives-async-response-working.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "description": "Progress payload for active sync_creatives task.", + "properties": { + "context": { + "$ref": "../core/context.json" + }, + "creatives_processed": { + "description": "Number of creatives processed so far", + "minimum": 0, + "type": "integer" + }, + "creatives_total": { + "description": "Total number of creatives to process", + "minimum": 0, + "type": "integer" + }, + "current_step": { + "description": "Current step or phase of the operation", + "type": "string" + }, + "ext": { + "$ref": "../core/ext.json" + }, + "percentage": { + "description": "Completion percentage (0-100)", + "maximum": 100, + "minimum": 0, + "type": "number" + }, + "step_number": { + "description": "Current step number", + "minimum": 1, + "type": "integer" + }, + "total_steps": { + "description": "Total number of steps in the operation", + "minimum": 1, + "type": "integer" + } + }, + "title": "Sync Creatives - Working", + "type": "object" +} \ No newline at end of file diff --git a/schemas/cache/media-buy/sync-creatives-response.json b/schemas/cache/media-buy/sync-creatives-response.json index 12e336f8..fc9ea41d 100644 --- a/schemas/cache/media-buy/sync-creatives-response.json +++ b/schemas/cache/media-buy/sync-creatives-response.json @@ -100,6 +100,7 @@ "required": [ "creatives" ], + "title": "SyncCreativesSuccess", "type": "object" }, { @@ -138,6 +139,7 @@ "required": [ "errors" ], + "title": "SyncCreativesError", "type": "object" } ], diff --git a/schemas/cache/media-buy/update-media-buy-async-response-input-required.json b/schemas/cache/media-buy/update-media-buy-async-response-input-required.json new file mode 100644 index 00000000..9547de76 --- /dev/null +++ b/schemas/cache/media-buy/update-media-buy-async-response-input-required.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "description": "Payload when update_media_buy task is paused waiting for user input or approval.", + "properties": { + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + }, + "reason": { + "description": "Reason code indicating why input is needed", + "enum": [ + "APPROVAL_REQUIRED", + "CHANGE_CONFIRMATION" + ], + "type": "string" + } + }, + "title": "Update Media Buy - Input Required", + "type": "object" +} \ No newline at end of file diff --git a/schemas/cache/media-buy/update-media-buy-async-response-submitted.json b/schemas/cache/media-buy/update-media-buy-async-response-submitted.json new file mode 100644 index 00000000..f56be9f4 --- /dev/null +++ b/schemas/cache/media-buy/update-media-buy-async-response-submitted.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "description": "Payload acknowledging update_media_buy task is queued for processing.", + "properties": { + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "title": "Update Media Buy - Submitted", + "type": "object" +} \ No newline at end of file diff --git a/schemas/cache/media-buy/update-media-buy-async-response-working.json b/schemas/cache/media-buy/update-media-buy-async-response-working.json new file mode 100644 index 00000000..c92509e7 --- /dev/null +++ b/schemas/cache/media-buy/update-media-buy-async-response-working.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "description": "Progress payload for active update_media_buy task.", + "properties": { + "context": { + "$ref": "../core/context.json" + }, + "current_step": { + "description": "Current step or phase of the operation", + "type": "string" + }, + "ext": { + "$ref": "../core/ext.json" + }, + "percentage": { + "description": "Completion percentage (0-100)", + "maximum": 100, + "minimum": 0, + "type": "number" + }, + "step_number": { + "description": "Current step number", + "minimum": 1, + "type": "integer" + }, + "total_steps": { + "description": "Total number of steps in the operation", + "minimum": 1, + "type": "integer" + } + }, + "title": "Update Media Buy - Working", + "type": "object" +} \ No newline at end of file diff --git a/schemas/cache/media-buy/update-media-buy-response.json b/schemas/cache/media-buy/update-media-buy-response.json index 659bb4b8..99b2eacc 100644 --- a/schemas/cache/media-buy/update-media-buy-response.json +++ b/schemas/cache/media-buy/update-media-buy-response.json @@ -45,6 +45,7 @@ "media_buy_id", "buyer_ref" ], + "title": "UpdateMediaBuySuccess", "type": "object" }, { @@ -88,6 +89,7 @@ "required": [ "errors" ], + "title": "UpdateMediaBuyError", "type": "object" } ], diff --git a/schemas/cache/signals/activate-signal-response.json b/schemas/cache/signals/activate-signal-response.json index f908d621..c76b35a9 100644 --- a/schemas/cache/signals/activate-signal-response.json +++ b/schemas/cache/signals/activate-signal-response.json @@ -28,6 +28,7 @@ "required": [ "deployments" ], + "title": "ActivateSignalSuccess", "type": "object" }, { @@ -57,6 +58,7 @@ "required": [ "errors" ], + "title": "ActivateSignalError", "type": "object" } ], diff --git a/scripts/generate_types.py b/scripts/generate_types.py index a572bc48..8308db0f 100755 --- a/scripts/generate_types.py +++ b/scripts/generate_types.py @@ -171,7 +171,6 @@ def generate_types(input_dir: Path): "--target-python-version", "3.10", "--use-annotated", - "--collapse-root-models", "--reuse-model", "--set-default-enum-member", "--enum-field-as-literal", diff --git a/src/adcp/__init__.py b/src/adcp/__init__.py index 75a3cf0f..96cf887d 100644 --- a/src/adcp/__init__.py +++ b/src/adcp/__init__.py @@ -101,6 +101,7 @@ ListCreativeFormatsResponse, ListCreativesRequest, ListCreativesResponse, + McpWebhookPayload, MediaBuy, MediaBuyStatus, Package, @@ -178,6 +179,12 @@ validate_product, validate_publisher_properties_item, ) +from adcp.webhooks import ( + create_a2a_webhook_payload, + create_mcp_webhook_payload, + extract_webhook_result_data, + get_adcp_signed_headers_for_webhook, +) __version__ = "2.13.0" @@ -215,6 +222,12 @@ def get_adcp_version() -> str: "TaskResult", "TaskStatus", "WebhookMetadata", + # Webhook utilities + "create_mcp_webhook_payload", + "create_a2a_webhook_payload", + "get_adcp_signed_headers_for_webhook", + "extract_webhook_result_data", + "McpWebhookPayload", # Common request/response types (re-exported for convenience) "CreateMediaBuyRequest", "CreateMediaBuyResponse", diff --git a/src/adcp/client.py b/src/adcp/client.py index 65cfe120..f338b3c1 100644 --- a/src/adcp/client.py +++ b/src/adcp/client.py @@ -11,6 +11,7 @@ from datetime import datetime, timezone from typing import Any +from a2a.types import Task, TaskStatusUpdateEvent from pydantic import BaseModel from adcp.exceptions import ADCPWebhookSignatureError @@ -45,7 +46,6 @@ SyncCreativesResponse, UpdateMediaBuyRequest, UpdateMediaBuyResponse, - WebhookPayload, ) from adcp.types.core import ( Activity, @@ -55,6 +55,7 @@ TaskResult, TaskStatus, ) +from adcp.types.generated_poc.core.async_response_data import AdcpAsyncResponseData from adcp.utils.operation_id import create_operation_id logger = logging.getLogger(__name__) @@ -811,13 +812,23 @@ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: """Async context manager exit.""" await self.close() - def _verify_webhook_signature(self, payload: dict[str, Any], signature: str) -> bool: + def _verify_webhook_signature( + self, payload: dict[str, Any], signature: str, timestamp: str + ) -> bool: """ Verify HMAC-SHA256 signature of webhook payload. + The verification algorithm matches get_adcp_signed_headers_for_webhook: + 1. Constructs message as "{timestamp}.{json_payload}" + 2. JSON-serializes payload with compact separators + 3. UTF-8 encodes the message + 4. HMAC-SHA256 signs with the shared secret + 5. Compares against the provided signature (with "sha256=" prefix stripped) + Args: payload: Webhook payload dict - signature: Signature to verify + signature: Signature to verify (with or without "sha256=" prefix) + timestamp: ISO 8601 timestamp from X-AdCP-Timestamp header Returns: True if signature is valid, False otherwise @@ -825,22 +836,53 @@ def _verify_webhook_signature(self, payload: dict[str, Any], signature: str) -> if not self.webhook_secret: return True - payload_bytes = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8") + # Strip "sha256=" prefix if present + if signature.startswith("sha256="): + signature = signature[7:] + + # Serialize payload to JSON with consistent formatting (matches signing) + payload_bytes = json.dumps(payload, separators=(",", ":"), sort_keys=False).encode("utf-8") + + # Construct signed message: timestamp.payload (matches get_adcp_signed_headers_for_webhook) + signed_message = f"{timestamp}.{payload_bytes.decode('utf-8')}" + + # Generate expected signature expected_signature = hmac.new( - self.webhook_secret.encode("utf-8"), payload_bytes, hashlib.sha256 + self.webhook_secret.encode("utf-8"), signed_message.encode("utf-8"), hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected_signature) - def _parse_webhook_result(self, webhook: WebhookPayload) -> TaskResult[Any]: + def _parse_webhook_result( + self, + task_id: str, + task_type: str, + operation_id: str, + status: GeneratedTaskStatus, + result: Any, + timestamp: datetime | str, + message: str | None, + context_id: str | None, + ) -> TaskResult[AdcpAsyncResponseData]: """ - Parse webhook payload into typed TaskResult based on task_type. + Parse webhook data into typed TaskResult based on task_type. Args: - webhook: Validated webhook payload + task_id: Unique identifier for this task + task_type: Task type from application routing (e.g., "get_products") + operation_id: Operation identifier from application routing + status: Current task status + result: Task-specific payload (AdCP response data) + timestamp: ISO 8601 timestamp when webhook was generated + message: Human-readable summary of task state + context_id: Session/conversation identifier Returns: TaskResult with task-specific typed response data + + Note: + This method works with both MCP and A2A protocols by accepting + protocol-agnostic parameters rather than protocol-specific objects. """ from adcp.utils.response_parser import parse_json_or_text @@ -859,21 +901,20 @@ def _parse_webhook_result(self, webhook: WebhookPayload) -> TaskResult[Any]: } # Handle completed tasks with result parsing - - if webhook.status == GeneratedTaskStatus.completed and webhook.result is not None: - response_type = response_type_map.get(webhook.task_type.value) + if status == GeneratedTaskStatus.completed and result is not None: + response_type = response_type_map.get(task_type) if response_type: try: - parsed_result: Any = parse_json_or_text(webhook.result, response_type) - return TaskResult[Any]( + parsed_result: Any = parse_json_or_text(result, response_type) + return TaskResult[AdcpAsyncResponseData]( status=TaskStatus.COMPLETED, data=parsed_result, success=True, metadata={ - "task_id": webhook.task_id, - "operation_id": webhook.operation_id, - "timestamp": webhook.timestamp, - "message": webhook.message, + "task_id": task_id, + "operation_id": operation_id, + "timestamp": timestamp, + "message": message, }, ) except ValueError as e: @@ -881,8 +922,7 @@ def _parse_webhook_result(self, webhook: WebhookPayload) -> TaskResult[Any]: # Fall through to untyped result # Handle failed, input-required, or unparseable results - # Convert webhook status to core TaskStatus enum - # Map generated enum values to core enum values + # Convert status to core TaskStatus enum status_map = { GeneratedTaskStatus.completed: TaskStatus.COMPLETED, GeneratedTaskStatus.submitted: TaskStatus.SUBMITTED, @@ -890,77 +930,341 @@ def _parse_webhook_result(self, webhook: WebhookPayload) -> TaskResult[Any]: GeneratedTaskStatus.failed: TaskStatus.FAILED, GeneratedTaskStatus.input_required: TaskStatus.NEEDS_INPUT, } - task_status = status_map.get(webhook.status, TaskStatus.FAILED) - - return TaskResult[Any]( + task_status = status_map.get(status, TaskStatus.FAILED) + + # Extract error message from result.errors if present + error_message: str | None = None + if result is not None and hasattr(result, "errors"): + errors = getattr(result, "errors", None) + if errors and len(errors) > 0: + first_error = errors[0] + if hasattr(first_error, "message"): + error_message = first_error.message + + return TaskResult[AdcpAsyncResponseData]( status=task_status, - data=webhook.result, - success=webhook.status == GeneratedTaskStatus.completed, - error=webhook.error if isinstance(webhook.error, str) else None, + data=result, + success=status == GeneratedTaskStatus.completed, + error=error_message, metadata={ - "task_id": webhook.task_id, - "operation_id": webhook.operation_id, - "timestamp": webhook.timestamp, - "message": webhook.message, - "context_id": webhook.context_id, - "progress": webhook.progress, + "task_id": task_id, + "operation_id": operation_id, + "timestamp": timestamp, + "message": message, + "context_id": context_id, }, ) - async def handle_webhook( + async def _handle_mcp_webhook( self, payload: dict[str, Any], - signature: str | None = None, - ) -> TaskResult[Any]: + task_type: str, + operation_id: str, + signature: str | None, + timestamp: str | None = None, + ) -> TaskResult[AdcpAsyncResponseData]: """ - Handle incoming webhook and return typed result. - - This method: - 1. Verifies webhook signature (if provided) - 2. Validates payload against WebhookPayload schema - 3. Parses task-specific result data into typed response - 4. Emits activity for monitoring + Handle MCP webhook delivered via HTTP POST. Args: payload: Webhook payload dict - signature: Optional HMAC-SHA256 signature for verification + task_type: Task type from application routing + operation_id: Operation identifier from application routing + signature: Optional HMAC-SHA256 signature for verification (X-AdCP-Signature header) + timestamp: Optional timestamp for signature verification (X-AdCP-Timestamp header) Returns: TaskResult with parsed task-specific response data Raises: ADCPWebhookSignatureError: If signature verification fails - ValidationError: If payload doesn't match WebhookPayload schema - - Example: - >>> result = await client.handle_webhook(payload, signature) - >>> if result.success and isinstance(result.data, GetProductsResponse): - >>> print(f"Found {len(result.data.products)} products") + ValidationError: If payload doesn't match McpWebhookPayload schema """ - # Verify signature before processing - if signature and not self._verify_webhook_signature(payload, signature): + from adcp.types.generated_poc.core.mcp_webhook_payload import McpWebhookPayload + + # Verify signature before processing (requires both signature and timestamp) + if ( + signature + and timestamp + and not self._verify_webhook_signature(payload, signature, timestamp) + ): logger.warning( f"Webhook signature verification failed for agent {self.agent_config.id}" ) raise ADCPWebhookSignatureError("Invalid webhook signature") - # Validate and parse webhook payload - webhook = WebhookPayload.model_validate(payload) + # Validate and parse MCP webhook payload + webhook = McpWebhookPayload.model_validate(payload) + + # Emit activity for monitoring + self._emit_activity( + Activity( + type=ActivityType.WEBHOOK_RECEIVED, + operation_id=operation_id, + agent_id=self.agent_config.id, + task_type=task_type, + timestamp=datetime.now(timezone.utc).isoformat(), + metadata={"payload": payload, "protocol": "mcp"}, + ) + ) + + # Extract fields and parse result + return self._parse_webhook_result( + task_id=webhook.task_id, + task_type=task_type, + operation_id=operation_id, + status=webhook.status, + result=webhook.result, + timestamp=webhook.timestamp, + message=webhook.message, + context_id=webhook.context_id, + ) + + async def _handle_a2a_webhook( + self, payload: Task | TaskStatusUpdateEvent, task_type: str, operation_id: str + ) -> TaskResult[AdcpAsyncResponseData]: + """ + Handle A2A webhook delivered through Task or TaskStatusUpdateEvent. + + Per A2A specification: + - Terminated statuses (completed, failed): Payload is Task with artifacts[].parts[] + - Intermediate statuses (working, input-required, submitted): + Payload is TaskStatusUpdateEvent with status.message.parts[] + + Args: + payload: A2A Task or TaskStatusUpdateEvent object + task_type: Task type from application routing + operation_id: Operation identifier from application routing + + Returns: + TaskResult with parsed task-specific response data + + Note: + Signature verification is NOT applicable for A2A webhooks + as they arrive through authenticated A2A connections, not HTTP. + """ + from a2a.types import DataPart, TextPart + + adcp_data: Any = None + text_message: str | None = None + task_id: str + context_id: str | None + status_state: str + timestamp: datetime | str + + # Type detection and extraction based on payload type + if isinstance(payload, TaskStatusUpdateEvent): + # Intermediate status: Extract from status.message.parts[] + task_id = payload.task_id + context_id = payload.context_id + status_state = payload.status.state if payload.status else "failed" + timestamp = ( + payload.status.timestamp + if payload.status and payload.status.timestamp + else datetime.now(timezone.utc) + ) + + # Extract from status.message.parts[] + if payload.status and payload.status.message and payload.status.message.parts: + # Extract DataPart for structured AdCP payload + data_parts = [ + p.root for p in payload.status.message.parts if isinstance(p.root, DataPart) + ] + if data_parts: + # Use last DataPart as authoritative + last_data_part = data_parts[-1] + adcp_data = last_data_part.data + + # Unwrap {"response": {...}} wrapper if present (ADK pattern) + if isinstance(adcp_data, dict) and "response" in adcp_data: + if len(adcp_data) == 1: + adcp_data = adcp_data["response"] + else: + adcp_data = adcp_data["response"] + + # Extract TextPart for human-readable message + for part in payload.status.message.parts: + if isinstance(part.root, TextPart): + text_message = part.root.text + break + + else: + # Terminated status (Task): Extract from artifacts[].parts[] + task_id = payload.id + context_id = payload.context_id + status_state = payload.status.state if payload.status else "failed" + timestamp = ( + payload.status.timestamp + if payload.status and payload.status.timestamp + else datetime.now(timezone.utc) + ) + + # Extract from task.artifacts[].parts[] + # Following A2A spec: use last artifact, last DataPart is authoritative + if payload.artifacts: + # Use last artifact (most recent in streaming scenarios) + target_artifact = payload.artifacts[-1] + + if target_artifact.parts: + # Extract DataPart for structured AdCP payload + data_parts = [ + p.root for p in target_artifact.parts if isinstance(p.root, DataPart) + ] + if data_parts: + # Use last DataPart as authoritative + last_data_part = data_parts[-1] + adcp_data = last_data_part.data + + # Unwrap {"response": {...}} wrapper if present (ADK pattern) + if isinstance(adcp_data, dict) and "response" in adcp_data: + if len(adcp_data) == 1: + adcp_data = adcp_data["response"] + else: + adcp_data = adcp_data["response"] + + # Extract TextPart for human-readable message + for part in target_artifact.parts: + if isinstance(part.root, TextPart): + text_message = part.root.text + break + + # Map A2A status.state to GeneratedTaskStatus enum + status_map = { + "completed": GeneratedTaskStatus.completed, + "submitted": GeneratedTaskStatus.submitted, + "working": GeneratedTaskStatus.working, + "failed": GeneratedTaskStatus.failed, + "input-required": GeneratedTaskStatus.input_required, + "input_required": GeneratedTaskStatus.input_required, # Handle both formats + } + mapped_status = status_map.get(status_state, GeneratedTaskStatus.failed) # Emit activity for monitoring self._emit_activity( Activity( type=ActivityType.WEBHOOK_RECEIVED, - operation_id=webhook.operation_id or "unknown", + operation_id=operation_id, agent_id=self.agent_config.id, - task_type=webhook.task_type.value, + task_type=task_type, timestamp=datetime.now(timezone.utc).isoformat(), - metadata={"payload": payload}, + metadata={ + "task_id": task_id, + "protocol": "a2a", + "payload_type": ( + "TaskStatusUpdateEvent" + if isinstance(payload, TaskStatusUpdateEvent) + else "Task" + ), + }, ) ) - # Parse and return typed result - return self._parse_webhook_result(webhook) + # Parse and return typed result by passing extracted fields directly + return self._parse_webhook_result( + task_id=task_id, + task_type=task_type, + operation_id=operation_id, + status=mapped_status, + result=adcp_data, + timestamp=timestamp, + message=text_message, + context_id=context_id, + ) + + async def handle_webhook( + self, + payload: dict[str, Any] | Task | TaskStatusUpdateEvent, + task_type: str, + operation_id: str, + signature: str | None = None, + timestamp: str | None = None, + ) -> TaskResult[AdcpAsyncResponseData]: + """ + Handle incoming webhook and return typed result. + + This method provides a unified interface for handling webhooks from both + MCP and A2A protocols: + + - MCP Webhooks: HTTP POST with dict payload, optional HMAC signature + - A2A Webhooks: Task or TaskStatusUpdateEvent objects based on status + + The method automatically detects the protocol type and routes to the + appropriate handler. Both protocols return a consistent TaskResult + structure with typed AdCP response data. + + Args: + payload: Webhook payload - one of: + - dict[str, Any]: MCP webhook payload from HTTP POST + - Task: A2A webhook for terminated statuses (completed, failed) + - TaskStatusUpdateEvent: A2A webhook for intermediate statuses + (working, input-required, submitted) + task_type: Task type from application routing (e.g., "get_products"). + Applications should extract this from URL routing pattern: + /webhook/{task_type}/{agent_id}/{operation_id} + operation_id: Operation identifier from application routing. + Used to correlate webhook notifications with original task submission. + signature: Optional HMAC-SHA256 signature for MCP webhook verification + (X-AdCP-Signature header). Ignored for A2A webhooks. + timestamp: Optional timestamp for MCP webhook signature verification + (X-AdCP-Timestamp header). Required when signature is provided. + + Returns: + TaskResult with parsed task-specific response data. The structure + is identical regardless of protocol. + + Raises: + ADCPWebhookSignatureError: If MCP signature verification fails + ValidationError: If MCP payload doesn't match WebhookPayload schema + + Note: + task_type and operation_id were deprecated from the webhook payload + per AdCP specification. Applications must extract these from URL + routing and pass them explicitly. + + Examples: + MCP webhook (HTTP endpoint): + >>> @app.post("/webhook/{task_type}/{agent_id}/{operation_id}") + >>> async def webhook_handler(task_type: str, operation_id: str, request: Request): + >>> payload = await request.json() + >>> signature = request.headers.get("X-AdCP-Signature") + >>> timestamp = request.headers.get("X-AdCP-Timestamp") + >>> result = await client.handle_webhook( + >>> payload, task_type, operation_id, signature, timestamp + >>> ) + >>> if result.success: + >>> print(f"Task completed: {result.data}") + + A2A webhook with Task (terminated status): + >>> async def on_task_completed(task: Task): + >>> # Extract task_type and operation_id from your app's task tracking + >>> task_type = your_task_registry.get_type(task.id) + >>> operation_id = your_task_registry.get_operation_id(task.id) + >>> result = await client.handle_webhook( + >>> task, task_type, operation_id + >>> ) + >>> if result.success: + >>> print(f"Task completed: {result.data}") + + A2A webhook with TaskStatusUpdateEvent (intermediate status): + >>> async def on_task_update(event: TaskStatusUpdateEvent): + >>> # Extract task_type and operation_id from your app's task tracking + >>> task_type = your_task_registry.get_type(event.task_id) + >>> operation_id = your_task_registry.get_operation_id(event.task_id) + >>> result = await client.handle_webhook( + >>> event, task_type, operation_id + >>> ) + >>> if result.status == GeneratedTaskStatus.working: + >>> print(f"Task still working: {result.metadata.get('message')}") + """ + # Detect protocol type and route to appropriate handler + if isinstance(payload, (Task, TaskStatusUpdateEvent)): + # A2A webhook (Task or TaskStatusUpdateEvent) + return await self._handle_a2a_webhook(payload, task_type, operation_id) + else: + # MCP webhook (dict payload) + return await self._handle_mcp_webhook( + payload, task_type, operation_id, signature, timestamp + ) class ADCPMultiAgentClient: diff --git a/src/adcp/protocols/mcp.py b/src/adcp/protocols/mcp.py index ac6c16b2..c07f1e7a 100644 --- a/src/adcp/protocols/mcp.py +++ b/src/adcp/protocols/mcp.py @@ -136,9 +136,7 @@ def _log_cleanup_error(self, exc: BaseException, context: str) -> None: and ("cancel scope" in exc_str or "async context" in exc_str) ) or ( # HTTP errors during cleanup (if httpx is available) - HTTPX_AVAILABLE - and HTTPStatusError is not None - and isinstance(exc, HTTPStatusError) + HTTPX_AVAILABLE and HTTPStatusError is not None and isinstance(exc, HTTPStatusError) ) if is_known_cleanup_error: diff --git a/src/adcp/types/__init__.py b/src/adcp/types/__init__.py index a402b94d..e2904fb9 100644 --- a/src/adcp/types/__init__.py +++ b/src/adcp/types/__init__.py @@ -16,11 +16,9 @@ # Import all types from generated code from adcp.types._generated import ( - # Core request/response types ActivateSignalRequest, ActivateSignalResponse, AggregatedTotals, - # Assets Asset, AssetContentType, AssetSelectors, @@ -41,7 +39,6 @@ Colors, Contact, Country, - # Pricing options CpcPricingOption, CpcvPricingOption, CpmAuctionPricingOption, @@ -59,7 +56,6 @@ CreativeFilters, CreativeManifest, CreativePolicy, - # Enums and constants CreativeStatus, CssAsset, DaastTrackingEvent, @@ -140,7 +136,6 @@ ProductCardDetailed, ProductCatalog, ProductFilters, - Progress, PromotedOfferings, PromotedProducts, Property, @@ -194,79 +189,56 @@ VideoAsset, ViewThreshold, WebhookAsset, - WebhookPayload, WebhookResponseType, ) -from adcp.types._generated import ( - TaskStatus as GeneratedTaskStatus, -) -from adcp.types._generated import ( - _PackageFromPackage as Package, -) +from adcp.types._generated import TaskStatus as GeneratedTaskStatus +from adcp.types._generated import _PackageFromPackage as Package # Import semantic aliases for discriminated unions from adcp.types.aliases import ( - # Activation responses ActivateSignalErrorResponse, ActivateSignalSuccessResponse, - # Agent deployment aliases AgentDeployment, AgentDestination, - # Authorized agent variants AuthorizedAgent, AuthorizedAgentsByInlineProperties, AuthorizedAgentsByPropertyId, AuthorizedAgentsByPropertyTag, AuthorizedAgentsByPublisherProperties, - # Preview/render aliases BothPreviewRender, - # Build creative responses BuildCreativeErrorResponse, BuildCreativeSuccessResponse, - # Create media buy responses CreateMediaBuyErrorResponse, CreateMediaBuySuccessResponse, - # Deployment union Deployment, - # Destination union Destination, - # Preview renders HtmlPreviewRender, - # Asset aliases InlineDaastAsset, InlineVastAsset, - # SubAsset aliases MediaSubAsset, - # Platform deployment PlatformDeployment, PlatformDestination, - # Preview requests PreviewCreativeFormatRequest, PreviewCreativeInteractiveResponse, PreviewCreativeManifestRequest, PreviewCreativeStaticResponse, - # Publisher property selectors PropertyId, PropertyIdActivationKey, PropertyTag, PropertyTagActivationKey, ProvidePerformanceFeedbackErrorResponse, ProvidePerformanceFeedbackSuccessResponse, - # Publisher properties variants PublisherPropertiesAll, PublisherPropertiesById, PublisherPropertiesByTag, - # Sync responses + SyncCreativeResult, SyncCreativesErrorResponse, SyncCreativesSuccessResponse, - # Text subassets TextSubAsset, - # Update media buy variants UpdateMediaBuyErrorResponse, UpdateMediaBuyPackagesRequest, UpdateMediaBuyPropertiesRequest, UpdateMediaBuySuccessResponse, - # URL aliases UrlDaastAsset, UrlPreviewRender, UrlVastAsset, @@ -277,6 +249,9 @@ # Users should import TaskStatus from adcp.types.core directly if they need the core enum from adcp.types.core import AgentConfig, Protocol, TaskResult, WebhookMetadata +# Re-export webhook payload type for webhook handling +from adcp.types.generated_poc.core.mcp_webhook_payload import McpWebhookPayload + # Backward compatibility aliases AssetType = AssetContentType # Use AssetContentType instead @@ -325,7 +300,6 @@ "MediaBuyDelivery", "PreviewCreativeRequest", "PreviewCreativeResponse", - "Progress", "ProtocolEnvelope", "ProtocolResponse", "ProvidePerformanceFeedbackRequest", @@ -484,12 +458,13 @@ "UrlAsset", "VideoAsset", "WebhookAsset", - "WebhookPayload", # Core types "AgentConfig", "Protocol", "TaskResult", "WebhookMetadata", + # Webhook types + "McpWebhookPayload", # Semantic aliases for discriminated unions "ActivateSignalErrorResponse", "ActivateSignalSuccessResponse", @@ -528,6 +503,7 @@ "PublisherPropertiesByTag", "SyncCreativesErrorResponse", "SyncCreativesSuccessResponse", + "SyncCreativeResult", "TextSubAsset", "UpdateMediaBuyErrorResponse", "UpdateMediaBuyPackagesRequest", diff --git a/src/adcp/types/_generated.py b/src/adcp/types/_generated.py index 9397b2d1..a555dc4c 100644 --- a/src/adcp/types/_generated.py +++ b/src/adcp/types/_generated.py @@ -10,7 +10,7 @@ DO NOT EDIT MANUALLY. Generated from: https://github.com/adcontextprotocol/adcp/tree/main/schemas -Generation date: 2025-11-29 12:12:51 UTC +Generation date: 2025-12-18 20:00:25 UTC """ # ruff: noqa: E501, I001 @@ -28,18 +28,23 @@ Contact, Tags, ) -from adcp.types.generated_poc.core.activation_key import ActivationKey1, ActivationKey2 +from adcp.types.generated_poc.core.activation_key import ( + ActivationKey, + ActivationKey1, + ActivationKey2, +) from adcp.types.generated_poc.core.assets.audio_asset import AudioAsset from adcp.types.generated_poc.core.assets.css_asset import CssAsset -from adcp.types.generated_poc.core.assets.daast_asset import DaastAsset1, DaastAsset2 +from adcp.types.generated_poc.core.assets.daast_asset import DaastAsset, DaastAsset1, DaastAsset2 from adcp.types.generated_poc.core.assets.html_asset import HtmlAsset from adcp.types.generated_poc.core.assets.image_asset import ImageAsset from adcp.types.generated_poc.core.assets.javascript_asset import JavascriptAsset from adcp.types.generated_poc.core.assets.text_asset import TextAsset from adcp.types.generated_poc.core.assets.url_asset import UrlAsset -from adcp.types.generated_poc.core.assets.vast_asset import VastAsset1, VastAsset2 +from adcp.types.generated_poc.core.assets.vast_asset import VastAsset, VastAsset1, VastAsset2 from adcp.types.generated_poc.core.assets.video_asset import VideoAsset from adcp.types.generated_poc.core.assets.webhook_asset import Security, WebhookAsset +from adcp.types.generated_poc.core.async_response_data import AdcpAsyncResponseData from adcp.types.generated_poc.core.brand_manifest import ( Asset, BrandManifest, @@ -52,6 +57,7 @@ ProductCatalog, UpdateFrequency, ) +from adcp.types.generated_poc.core.brand_manifest_ref import BrandManifestReference from adcp.types.generated_poc.core.context import ContextObject from adcp.types.generated_poc.core.creative_asset import CreativeAsset, Input from adcp.types.generated_poc.core.creative_assignment import CreativeAssignment @@ -64,15 +70,15 @@ QuartileData, VenueBreakdownItem, ) -from adcp.types.generated_poc.core.deployment import Deployment1, Deployment2 -from adcp.types.generated_poc.core.destination import Destination1, Destination2 -from adcp.types.generated_poc.core.dimensions import Dimensions +from adcp.types.generated_poc.core.deployment import Deployment, Deployment1, Deployment2 +from adcp.types.generated_poc.core.destination import Destination, Destination1, Destination2 from adcp.types.generated_poc.core.error import Error from adcp.types.generated_poc.core.ext import ExtensionObject from adcp.types.generated_poc.core.format import ( AssetsRequired, AssetsRequired1, - Dimensions2, + Dimensions, + Dimensions1, Format, FormatCard, FormatCardDetailed, @@ -82,6 +88,7 @@ ) from adcp.types.generated_poc.core.format_id import FormatId from adcp.types.generated_poc.core.frequency_cap import FrequencyCap +from adcp.types.generated_poc.core.mcp_webhook_payload import McpWebhookPayload from adcp.types.generated_poc.core.measurement import Measurement from adcp.types.generated_poc.core.media_buy import MediaBuy from adcp.types.generated_poc.core.performance_feedback import ( @@ -90,6 +97,7 @@ Status, ) from adcp.types.generated_poc.core.placement import Placement +from adcp.types.generated_poc.core.pricing_option import PricingOption from adcp.types.generated_poc.core.product import ( DeliveryMeasurement, Product, @@ -114,6 +122,7 @@ from adcp.types.generated_poc.core.property_tag import PropertyTag from adcp.types.generated_poc.core.protocol_envelope import ProtocolEnvelope from adcp.types.generated_poc.core.publisher_property_selector import ( + PublisherPropertySelector, PublisherPropertySelector1, PublisherPropertySelector2, PublisherPropertySelector3, @@ -125,9 +134,9 @@ from adcp.types.generated_poc.core.reporting_capabilities import ReportingCapabilities from adcp.types.generated_poc.core.response import ProtocolResponse from adcp.types.generated_poc.core.signal_filters import SignalFilters -from adcp.types.generated_poc.core.sub_asset import SubAsset1, SubAsset2 +from adcp.types.generated_poc.core.start_timing import StartTiming +from adcp.types.generated_poc.core.sub_asset import SubAsset, SubAsset1, SubAsset2 from adcp.types.generated_poc.core.targeting import GeoCountryAnyOfItem, TargetingOverlay -from adcp.types.generated_poc.core.webhook_payload import Progress, WebhookPayload from adcp.types.generated_poc.creative.list_creative_formats_request import ( ListCreativeFormatsRequestCreativeAgent, Type, @@ -213,6 +222,16 @@ BuildCreativeResponse1, BuildCreativeResponse2, ) +from adcp.types.generated_poc.media_buy.create_media_buy_async_response_input_required import ( + CreateMediaBuyInputRequired, + Reason, +) +from adcp.types.generated_poc.media_buy.create_media_buy_async_response_submitted import ( + CreateMediaBuySubmitted, +) +from adcp.types.generated_poc.media_buy.create_media_buy_async_response_working import ( + CreateMediaBuyWorking, +) from adcp.types.generated_poc.media_buy.create_media_buy_request import ( CreateMediaBuyRequest, ReportingWebhook, @@ -236,6 +255,15 @@ ReportingPeriod, Totals, ) +from adcp.types.generated_poc.media_buy.get_products_async_response_input_required import ( + GetProductsInputRequired, +) +from adcp.types.generated_poc.media_buy.get_products_async_response_submitted import ( + GetProductsSubmitted, +) +from adcp.types.generated_poc.media_buy.get_products_async_response_working import ( + GetProductsWorking, +) from adcp.types.generated_poc.media_buy.get_products_request import GetProductsRequest from adcp.types.generated_poc.media_buy.get_products_response import GetProductsResponse from adcp.types.generated_poc.media_buy.list_authorized_properties_request import ( @@ -279,12 +307,30 @@ ProvidePerformanceFeedbackResponse1, ProvidePerformanceFeedbackResponse2, ) +from adcp.types.generated_poc.media_buy.sync_creatives_async_response_input_required import ( + SyncCreativesInputRequired, +) +from adcp.types.generated_poc.media_buy.sync_creatives_async_response_submitted import ( + SyncCreativesSubmitted, +) +from adcp.types.generated_poc.media_buy.sync_creatives_async_response_working import ( + SyncCreativesWorking, +) from adcp.types.generated_poc.media_buy.sync_creatives_request import SyncCreativesRequest from adcp.types.generated_poc.media_buy.sync_creatives_response import ( SyncCreativesResponse, SyncCreativesResponse1, SyncCreativesResponse2, ) +from adcp.types.generated_poc.media_buy.update_media_buy_async_response_input_required import ( + UpdateMediaBuyInputRequired, +) +from adcp.types.generated_poc.media_buy.update_media_buy_async_response_submitted import ( + UpdateMediaBuySubmitted, +) +from adcp.types.generated_poc.media_buy.update_media_buy_async_response_working import ( + UpdateMediaBuyWorking, +) from adcp.types.generated_poc.media_buy.update_media_buy_request import ( Packages, Packages1, @@ -344,9 +390,11 @@ "ActivateSignalResponse", "ActivateSignalResponse1", "ActivateSignalResponse2", + "ActivationKey", "ActivationKey1", "ActivationKey2", "AdcpAgentCardExtension", + "AdcpAsyncResponseData", "AdcpDomain", "AdvertisingChannels", "AggregatedTotals", @@ -370,6 +418,7 @@ "AuthorizedSalesAgents2", "AvailableMetric", "BrandManifest", + "BrandManifestReference", "BudgetRange", "BudgetRange1", "BuildCreativeRequest", @@ -389,10 +438,13 @@ "CpmFixedRatePricingOption", "CppPricingOption", "CpvPricingOption", + "CreateMediaBuyInputRequired", "CreateMediaBuyRequest", "CreateMediaBuyResponse", "CreateMediaBuyResponse1", "CreateMediaBuyResponse2", + "CreateMediaBuySubmitted", + "CreateMediaBuyWorking", "Creative", "CreativeAction", "CreativeAgent", @@ -405,6 +457,7 @@ "CreativeSortField", "CreativeStatus", "CssAsset", + "DaastAsset", "DaastAsset1", "DaastAsset2", "DaastTrackingEvent", @@ -415,13 +468,15 @@ "DeliveryMetrics", "DeliveryStatus", "DeliveryType", + "Deployment", "Deployment1", "Deployment2", + "Destination", "Destination1", "Destination2", "DimensionUnit", "Dimensions", - "Dimensions2", + "Dimensions1", "Disclaimer", "DoohMetrics", "Embedding", @@ -443,8 +498,11 @@ "GeoCountryAnyOfItem", "GetMediaBuyDeliveryRequest", "GetMediaBuyDeliveryResponse", + "GetProductsInputRequired", "GetProductsRequest", "GetProductsResponse", + "GetProductsSubmitted", + "GetProductsWorking", "GetSignalsRequest", "GetSignalsResponse", "HistoryEntryType", @@ -468,6 +526,7 @@ "ListCreativesResponse", "Logo", "MarkdownFlavor", + "McpWebhookPayload", "Measurement", "MeasurementPeriod", "MediaBuy", @@ -505,13 +564,13 @@ "PriceGuidance", "Pricing", "PricingModel", + "PricingOption", "PrimaryCountry", "Product", "ProductCard", "ProductCardDetailed", "ProductCatalog", "ProductFilters", - "Progress", "PromotedOfferings", "PromotedProducts", "Property", @@ -530,12 +589,14 @@ "ProvidePerformanceFeedbackResponse2", "PublisherDomain", "PublisherIdentifierTypes", + "PublisherPropertySelector", "PublisherPropertySelector1", "PublisherPropertySelector2", "PublisherPropertySelector3", "PushNotificationConfig", "QuartileData", "QuerySummary", + "Reason", "Renders", "Renders1", "ReportingCapabilities", @@ -557,14 +618,19 @@ "SortApplied", "SortDirection", "StandardFormatIds", + "StartTiming", "Status", "StatusSummary", + "SubAsset", "SubAsset1", "SubAsset2", + "SyncCreativesInputRequired", "SyncCreativesRequest", "SyncCreativesResponse", "SyncCreativesResponse1", "SyncCreativesResponse2", + "SyncCreativesSubmitted", + "SyncCreativesWorking", "Tags", "TargetingOverlay", "TaskStatus", @@ -573,15 +639,19 @@ "Totals", "Type", "UpdateFrequency", + "UpdateMediaBuyInputRequired", "UpdateMediaBuyRequest", "UpdateMediaBuyRequest1", "UpdateMediaBuyRequest2", "UpdateMediaBuyResponse", "UpdateMediaBuyResponse1", "UpdateMediaBuyResponse2", + "UpdateMediaBuySubmitted", + "UpdateMediaBuyWorking", "UrlAsset", "UrlAssetType", "ValidationMode", + "VastAsset", "VastAsset1", "VastAsset2", "VastTrackingEvent", @@ -593,7 +663,6 @@ "ViewThreshold", "ViewThreshold1", "WebhookAsset", - "WebhookPayload", "WebhookResponseType", "WebhookSecurityMethod", "_PackageFromPackage", diff --git a/src/adcp/types/aliases.py b/src/adcp/types/aliases.py index 3cc6b775..fd1220b2 100644 --- a/src/adcp/types/aliases.py +++ b/src/adcp/types/aliases.py @@ -106,6 +106,11 @@ # Import Package from _generated (still uses qualified name for internal reasons) from adcp.types._generated import _PackageFromPackage as Package +# Import nested types that aren't exported by _generated but are useful for type hints +from adcp.types.generated_poc.media_buy.sync_creatives_response import ( + Creative as SyncCreativeResultInternal, +) + # ============================================================================ # RESPONSE TYPE ALIASES - Success/Error Discriminated Unions # ============================================================================ @@ -148,6 +153,23 @@ SyncCreativesErrorResponse = SyncCreativesResponse2 """Error response - sync operation failed.""" +# Sync Creative Result (nested type from SyncCreativesResponse1.creatives[]) +SyncCreativeResult = SyncCreativeResultInternal +"""Result of syncing a single creative - indicates action taken (created, updated, failed, etc.) + +This is the item type from SyncCreativesSuccessResponse.creatives[]. In TypeScript, this would be: + type SyncCreativeResult = SyncCreativesSuccessResponse["creatives"][number] + +Example usage: + from adcp import SyncCreativeResult, SyncCreativesSuccessResponse + + def process_result(result: SyncCreativeResult) -> None: + if result.action == "created": + print(f"Created creative {result.creative_id}") + elif result.action == "failed": + print(f"Failed: {result.errors}") +""" + # Update Media Buy Response Variants UpdateMediaBuySuccessResponse = UpdateMediaBuyResponse1 """Success response - media buy updated successfully.""" @@ -716,6 +738,7 @@ def filter_products(props: PublisherProperties) -> None: # Sync creatives responses "SyncCreativesSuccessResponse", "SyncCreativesErrorResponse", + "SyncCreativeResult", # Update media buy requests "UpdateMediaBuyPackagesRequest", "UpdateMediaBuyPropertiesRequest", diff --git a/src/adcp/types/generated_poc/adagents.py b/src/adcp/types/generated_poc/adagents.py index 8e5d1adc..09701921 100644 --- a/src/adcp/types/generated_poc/adagents.py +++ b/src/adcp/types/generated_poc/adagents.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: adagents.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-18T20:00:24+00:00 from __future__ import annotations @@ -19,7 +19,7 @@ class AuthorizedSalesAgents1(AdCPBaseModel): field_schema: Annotated[ str | None, Field(alias='$schema', description='JSON Schema identifier for this adagents.json file'), - ] = 'https://adcontextprotocol.org/schemas/adagents.json' + ] = None authoritative_location: Annotated[ AnyUrl, Field( @@ -156,11 +156,7 @@ class AuthorizedAgents3(AdCPBaseModel): ), ] publisher_properties: Annotated[ - list[ - publisher_property_selector.PublisherPropertySelector1 - | publisher_property_selector.PublisherPropertySelector2 - | publisher_property_selector.PublisherPropertySelector3 - ], + list[publisher_property_selector.PublisherPropertySelector], Field( description='Properties from other publisher domains this agent is authorized for. Each entry specifies a publisher domain and which of their properties this agent can sell', min_length=1, @@ -202,7 +198,7 @@ class AuthorizedSalesAgents2(AdCPBaseModel): field_schema: Annotated[ str | None, Field(alias='$schema', description='JSON Schema identifier for this adagents.json file'), - ] = 'https://adcontextprotocol.org/schemas/adagents.json' + ] = None authorized_agents: Annotated[ list[AuthorizedAgents | AuthorizedAgents1 | AuthorizedAgents2 | AuthorizedAgents3], Field( @@ -242,12 +238,12 @@ class AuthorizedSalesAgents(RootModel[AuthorizedSalesAgents1 | AuthorizedSalesAg description='Declaration of authorized sales agents for advertising inventory. Hosted at /.well-known/adagents.json on publisher domains. Can either contain the full structure inline or reference an authoritative URL.', examples=[ { - '$schema': 'https://adcontextprotocol.org/schemas/adagents.json', + '$schema': '/schemas/latest/adagents.json', 'authoritative_location': 'https://cdn.example.com/adagents/v2/adagents.json', 'last_updated': '2025-01-15T10:00:00Z', }, { - '$schema': 'https://adcontextprotocol.org/schemas/adagents.json', + '$schema': '/schemas/latest/adagents.json', 'authorized_agents': [ { 'authorization_type': 'property_tags', @@ -273,7 +269,7 @@ class AuthorizedSalesAgents(RootModel[AuthorizedSalesAgents1 | AuthorizedSalesAg }, }, { - '$schema': 'https://adcontextprotocol.org/schemas/adagents.json', + '$schema': '/schemas/latest/adagents.json', 'authorized_agents': [ { 'authorization_type': 'property_tags', @@ -338,7 +334,7 @@ class AuthorizedSalesAgents(RootModel[AuthorizedSalesAgents1 | AuthorizedSalesAg }, }, { - '$schema': 'https://adcontextprotocol.org/schemas/adagents.json', + '$schema': '/schemas/latest/adagents.json', 'authorized_agents': [ { 'authorization_type': 'property_tags', @@ -366,7 +362,7 @@ class AuthorizedSalesAgents(RootModel[AuthorizedSalesAgents1 | AuthorizedSalesAg }, }, { - '$schema': 'https://adcontextprotocol.org/schemas/adagents.json', + '$schema': '/schemas/latest/adagents.json', 'authorized_agents': [ { 'authorization_type': 'publisher_properties', diff --git a/src/adcp/types/generated_poc/core/activation_key.py b/src/adcp/types/generated_poc/core/activation_key.py index 59698ca2..effa4ae0 100644 --- a/src/adcp/types/generated_poc/core/activation_key.py +++ b/src/adcp/types/generated_poc/core/activation_key.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/activation_key.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations from typing import Annotated, Literal from adcp.types.base import AdCPBaseModel -from pydantic import ConfigDict, Field +from pydantic import ConfigDict, Field, RootModel class ActivationKey1(AdCPBaseModel): @@ -28,3 +28,13 @@ class ActivationKey2(AdCPBaseModel): key: Annotated[str, Field(description='The targeting parameter key')] type: Annotated[Literal['key_value'], Field(description='Key-value pair based targeting')] value: Annotated[str, Field(description='The targeting parameter value')] + + +class ActivationKey(RootModel[ActivationKey1 | ActivationKey2]): + root: Annotated[ + ActivationKey1 | ActivationKey2, + Field( + description="Universal identifier for using a signal on a destination platform. Can be either a segment ID or a key-value pair depending on the platform's targeting mechanism.", + title='Activation Key', + ), + ] diff --git a/src/adcp/types/generated_poc/core/assets/daast_asset.py b/src/adcp/types/generated_poc/core/assets/daast_asset.py index 5c231bf8..c9c9a5be 100644 --- a/src/adcp/types/generated_poc/core/assets/daast_asset.py +++ b/src/adcp/types/generated_poc/core/assets/daast_asset.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/assets/daast_asset.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations from typing import Annotated, Literal from adcp.types.base import AdCPBaseModel -from pydantic import AnyUrl, ConfigDict, Field +from pydantic import AnyUrl, ConfigDict, Field, RootModel from ...enums import daast_tracking_event from ...enums import daast_version as daast_version_1 @@ -59,3 +59,13 @@ class DaastAsset2(AdCPBaseModel): list[daast_tracking_event.DaastTrackingEvent] | None, Field(description='Tracking events supported by this DAAST tag'), ] = None + + +class DaastAsset(RootModel[DaastAsset1 | DaastAsset2]): + root: Annotated[ + DaastAsset1 | DaastAsset2, + Field( + description='DAAST (Digital Audio Ad Serving Template) tag for third-party audio ad serving', + title='DAAST Asset', + ), + ] diff --git a/src/adcp/types/generated_poc/core/assets/image_asset.py b/src/adcp/types/generated_poc/core/assets/image_asset.py index 88d48df3..7ce67379 100644 --- a/src/adcp/types/generated_poc/core/assets/image_asset.py +++ b/src/adcp/types/generated_poc/core/assets/image_asset.py @@ -1,19 +1,23 @@ # generated by datamodel-codegen: # filename: core/assets/image_asset.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-18T20:00:24+00:00 from __future__ import annotations from typing import Annotated -from pydantic import AnyUrl, Field +from adcp.types.base import AdCPBaseModel +from pydantic import AnyUrl, ConfigDict, Field -from ..dimensions import Dimensions - -class ImageAsset(Dimensions): +class ImageAsset(AdCPBaseModel): + model_config = ConfigDict( + extra='forbid', + ) alt_text: Annotated[str | None, Field(description='Alternative text for accessibility')] = None format: Annotated[ str | None, Field(description='Image file format (jpg, png, gif, webp, etc.)') ] = None + height: Annotated[int, Field(description='Height in pixels', ge=1)] url: Annotated[AnyUrl, Field(description='URL to the image asset')] + width: Annotated[int, Field(description='Width in pixels', ge=1)] diff --git a/src/adcp/types/generated_poc/core/assets/vast_asset.py b/src/adcp/types/generated_poc/core/assets/vast_asset.py index e16547b3..71fa502c 100644 --- a/src/adcp/types/generated_poc/core/assets/vast_asset.py +++ b/src/adcp/types/generated_poc/core/assets/vast_asset.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/assets/vast_asset.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations from typing import Annotated, Literal from adcp.types.base import AdCPBaseModel -from pydantic import AnyUrl, ConfigDict, Field +from pydantic import AnyUrl, ConfigDict, Field, RootModel from ...enums import vast_tracking_event from ...enums import vast_version as vast_version_1 @@ -61,3 +61,13 @@ class VastAsset2(AdCPBaseModel): bool | None, Field(description='Whether VPAID (Video Player-Ad Interface Definition) is supported'), ] = None + + +class VastAsset(RootModel[VastAsset1 | VastAsset2]): + root: Annotated[ + VastAsset1 | VastAsset2, + Field( + description='VAST (Video Ad Serving Template) tag for third-party video ad serving', + title='VAST Asset', + ), + ] diff --git a/src/adcp/types/generated_poc/core/assets/video_asset.py b/src/adcp/types/generated_poc/core/assets/video_asset.py index 1135ef68..774c37ff 100644 --- a/src/adcp/types/generated_poc/core/assets/video_asset.py +++ b/src/adcp/types/generated_poc/core/assets/video_asset.py @@ -1,17 +1,19 @@ # generated by datamodel-codegen: # filename: core/assets/video_asset.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-18T20:00:24+00:00 from __future__ import annotations from typing import Annotated -from pydantic import AnyUrl, Field +from adcp.types.base import AdCPBaseModel +from pydantic import AnyUrl, ConfigDict, Field -from ..dimensions import Dimensions - -class VideoAsset(Dimensions): +class VideoAsset(AdCPBaseModel): + model_config = ConfigDict( + extra='forbid', + ) bitrate_kbps: Annotated[ int | None, Field(description='Video bitrate in kilobits per second', ge=1) ] = None @@ -21,4 +23,6 @@ class VideoAsset(Dimensions): format: Annotated[str | None, Field(description='Video file format (mp4, webm, mov, etc.)')] = ( None ) + height: Annotated[int, Field(description='Height in pixels', ge=1)] url: Annotated[AnyUrl, Field(description='URL to the video asset')] + width: Annotated[int, Field(description='Width in pixels', ge=1)] diff --git a/src/adcp/types/generated_poc/core/async_response_data.py b/src/adcp/types/generated_poc/core/async_response_data.py new file mode 100644 index 00000000..3393e9fd --- /dev/null +++ b/src/adcp/types/generated_poc/core/async_response_data.py @@ -0,0 +1,72 @@ +# generated by datamodel-codegen: +# filename: core/async_response_data.json +# timestamp: 2025-12-11T15:09:37+00:00 + +from __future__ import annotations + +from typing import Annotated + +from pydantic import Field, RootModel + +from ..media_buy import ( + create_media_buy_async_response_input_required, + create_media_buy_async_response_submitted, + create_media_buy_async_response_working, + create_media_buy_response, + get_products_async_response_input_required, + get_products_async_response_submitted, + get_products_async_response_working, + get_products_response, + sync_creatives_async_response_input_required, + sync_creatives_async_response_submitted, + sync_creatives_async_response_working, + sync_creatives_response, + update_media_buy_async_response_input_required, + update_media_buy_async_response_submitted, + update_media_buy_async_response_working, + update_media_buy_response, +) + + +class AdcpAsyncResponseData( + RootModel[ + get_products_response.GetProductsResponse + | get_products_async_response_working.GetProductsWorking + | get_products_async_response_input_required.GetProductsInputRequired + | get_products_async_response_submitted.GetProductsSubmitted + | create_media_buy_response.CreateMediaBuyResponse + | create_media_buy_async_response_working.CreateMediaBuyWorking + | create_media_buy_async_response_input_required.CreateMediaBuyInputRequired + | create_media_buy_async_response_submitted.CreateMediaBuySubmitted + | update_media_buy_response.UpdateMediaBuyResponse + | update_media_buy_async_response_working.UpdateMediaBuyWorking + | update_media_buy_async_response_input_required.UpdateMediaBuyInputRequired + | update_media_buy_async_response_submitted.UpdateMediaBuySubmitted + | sync_creatives_response.SyncCreativesResponse + | sync_creatives_async_response_working.SyncCreativesWorking + | sync_creatives_async_response_input_required.SyncCreativesInputRequired + | sync_creatives_async_response_submitted.SyncCreativesSubmitted + ] +): + root: Annotated[ + get_products_response.GetProductsResponse + | get_products_async_response_working.GetProductsWorking + | get_products_async_response_input_required.GetProductsInputRequired + | get_products_async_response_submitted.GetProductsSubmitted + | create_media_buy_response.CreateMediaBuyResponse + | create_media_buy_async_response_working.CreateMediaBuyWorking + | create_media_buy_async_response_input_required.CreateMediaBuyInputRequired + | create_media_buy_async_response_submitted.CreateMediaBuySubmitted + | update_media_buy_response.UpdateMediaBuyResponse + | update_media_buy_async_response_working.UpdateMediaBuyWorking + | update_media_buy_async_response_input_required.UpdateMediaBuyInputRequired + | update_media_buy_async_response_submitted.UpdateMediaBuySubmitted + | sync_creatives_response.SyncCreativesResponse + | sync_creatives_async_response_working.SyncCreativesWorking + | sync_creatives_async_response_input_required.SyncCreativesInputRequired + | sync_creatives_async_response_submitted.SyncCreativesSubmitted, + Field( + description='Union of all possible data payloads for async task webhook responses. For completed/failed statuses, use the main task response schema. For working/input-required/submitted, use the status-specific schemas.', + title='AdCP Async Response Data', + ), + ] diff --git a/src/adcp/types/generated_poc/core/brand_manifest_ref.py b/src/adcp/types/generated_poc/core/brand_manifest_ref.py new file mode 100644 index 00000000..850dba23 --- /dev/null +++ b/src/adcp/types/generated_poc/core/brand_manifest_ref.py @@ -0,0 +1,35 @@ +# generated by datamodel-codegen: +# filename: core/brand_manifest_ref.json +# timestamp: 2025-12-11T15:09:37+00:00 + +from __future__ import annotations + +from typing import Annotated + +from pydantic import AnyUrl, Field, RootModel + +from . import brand_manifest + + +class BrandManifestReference(RootModel[brand_manifest.BrandManifest | AnyUrl]): + root: Annotated[ + brand_manifest.BrandManifest | AnyUrl, + Field( + description='Brand manifest provided either as an inline object or a URL string pointing to a hosted manifest', + examples=[ + { + 'data': { + 'colors': {'primary': '#FF6B35'}, + 'name': 'ACME Corporation', + 'url': 'https://acmecorp.com', + }, + 'description': 'Inline brand manifest', + }, + { + 'data': 'https://cdn.acmecorp.com/brand-manifest.json', + 'description': 'URL string reference to hosted manifest', + }, + ], + title='Brand Manifest Reference', + ), + ] diff --git a/src/adcp/types/generated_poc/core/creative_asset.py b/src/adcp/types/generated_poc/core/creative_asset.py index 701a7f2c..cc0bd3f9 100644 --- a/src/adcp/types/generated_poc/core/creative_asset.py +++ b/src/adcp/types/generated_poc/core/creative_asset.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/creative_asset.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations @@ -59,12 +59,10 @@ class CreativeAsset(AdCPBaseModel): | html_asset.HtmlAsset | css_asset.CssAsset | javascript_asset.JavascriptAsset + | vast_asset.VastAsset + | daast_asset.DaastAsset | promoted_offerings.PromotedOfferings - | url_asset.UrlAsset - | vast_asset.VastAsset1 - | vast_asset.VastAsset2 - | daast_asset.DaastAsset1 - | daast_asset.DaastAsset2, + | url_asset.UrlAsset, ], Field(description='Assets required by the format, keyed by asset_role'), ] diff --git a/src/adcp/types/generated_poc/core/creative_manifest.py b/src/adcp/types/generated_poc/core/creative_manifest.py index 30ea288b..5f30d13b 100644 --- a/src/adcp/types/generated_poc/core/creative_manifest.py +++ b/src/adcp/types/generated_poc/core/creative_manifest.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/creative_manifest.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations @@ -37,17 +37,15 @@ class CreativeManifest(AdCPBaseModel): image_asset.ImageAsset | video_asset.VideoAsset | audio_asset.AudioAsset + | vast_asset.VastAsset | text_asset.TextAsset | url_asset.UrlAsset | html_asset.HtmlAsset | javascript_asset.JavascriptAsset | webhook_asset.WebhookAsset | css_asset.CssAsset - | promoted_offerings.PromotedOfferings - | vast_asset.VastAsset1 - | vast_asset.VastAsset2 - | daast_asset.DaastAsset1 - | daast_asset.DaastAsset2, + | daast_asset.DaastAsset + | promoted_offerings.PromotedOfferings, ], Field( description="Map of asset IDs to actual asset content. Each key MUST match an asset_id from the format's assets_required array (e.g., 'banner_image', 'clickthrough_url', 'video_file', 'vast_tag'). The asset_id is the technical identifier used to match assets to format requirements.\n\nIMPORTANT: Creative manifest validation MUST be performed in the context of the format specification. The format defines what type each asset_id should be, which eliminates any validation ambiguity." diff --git a/src/adcp/types/generated_poc/core/deployment.py b/src/adcp/types/generated_poc/core/deployment.py index a4e4520d..3af77f89 100644 --- a/src/adcp/types/generated_poc/core/deployment.py +++ b/src/adcp/types/generated_poc/core/deployment.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/deployment.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations from typing import Annotated, Literal from adcp.types.base import AdCPBaseModel -from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field +from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, RootModel from . import activation_key as activation_key_1 @@ -18,10 +18,9 @@ class Deployment1(AdCPBaseModel): ) account: Annotated[str | None, Field(description='Account identifier if applicable')] = None activation_key: Annotated[ - activation_key_1.ActivationKey1 | activation_key_1.ActivationKey2 | None, + activation_key_1.ActivationKey | None, Field( - description='The key to use for targeting. Only present if is_live=true AND requester has access to this deployment.', - title='Activation Key', + description='The key to use for targeting. Only present if is_live=true AND requester has access to this deployment.' ), ] = None deployed_at: Annotated[ @@ -51,10 +50,9 @@ class Deployment2(AdCPBaseModel): ) account: Annotated[str | None, Field(description='Account identifier if applicable')] = None activation_key: Annotated[ - activation_key_1.ActivationKey1 | activation_key_1.ActivationKey2 | None, + activation_key_1.ActivationKey | None, Field( - description='The key to use for targeting. Only present if is_live=true AND requester has access to this deployment.', - title='Activation Key', + description='The key to use for targeting. Only present if is_live=true AND requester has access to this deployment.' ), ] = None agent_url: Annotated[AnyUrl, Field(description='URL identifying the deployment agent')] @@ -76,3 +74,13 @@ class Deployment2(AdCPBaseModel): Literal['agent'], Field(description='Discriminator indicating this is an agent URL-based deployment'), ] + + +class Deployment(RootModel[Deployment1 | Deployment2]): + root: Annotated[ + Deployment1 | Deployment2, + Field( + description='A signal deployment to a specific deployment target with activation status and key', + title='Deployment', + ), + ] diff --git a/src/adcp/types/generated_poc/core/destination.py b/src/adcp/types/generated_poc/core/destination.py index 17497b85..ab1d1c5a 100644 --- a/src/adcp/types/generated_poc/core/destination.py +++ b/src/adcp/types/generated_poc/core/destination.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/destination.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations from typing import Annotated, Literal from adcp.types.base import AdCPBaseModel -from pydantic import AnyUrl, ConfigDict, Field +from pydantic import AnyUrl, ConfigDict, Field, RootModel class Destination1(AdCPBaseModel): @@ -41,3 +41,13 @@ class Destination2(AdCPBaseModel): Literal['agent'], Field(description='Discriminator indicating this is an agent URL-based deployment'), ] + + +class Destination(RootModel[Destination1 | Destination2]): + root: Annotated[ + Destination1 | Destination2, + Field( + description='A deployment target where signals can be activated (DSP, sales agent, etc.)', + title='Destination', + ), + ] diff --git a/src/adcp/types/generated_poc/core/dimensions.py b/src/adcp/types/generated_poc/core/dimensions.py deleted file mode 100644 index c6a080f7..00000000 --- a/src/adcp/types/generated_poc/core/dimensions.py +++ /dev/null @@ -1,18 +0,0 @@ -# generated by datamodel-codegen: -# filename: core/dimensions.json -# timestamp: 2025-11-29T12:00:45+00:00 - -from __future__ import annotations - -from typing import Annotated - -from adcp.types.base import AdCPBaseModel -from pydantic import ConfigDict, Field - - -class Dimensions(AdCPBaseModel): - model_config = ConfigDict( - extra='forbid', - ) - height: Annotated[int, Field(description='Height in pixels', ge=1)] - width: Annotated[int, Field(description='Width in pixels', ge=1)] diff --git a/src/adcp/types/generated_poc/core/format.py b/src/adcp/types/generated_poc/core/format.py index 70927420..e6fa8872 100644 --- a/src/adcp/types/generated_poc/core/format.py +++ b/src/adcp/types/generated_poc/core/format.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/format.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-18T20:00:24+00:00 from __future__ import annotations @@ -123,12 +123,12 @@ class Renders(AdCPBaseModel): ] -Dimensions2 = Dimensions +Dimensions1 = Dimensions class Renders1(AdCPBaseModel): dimensions: Annotated[ - Dimensions2 | None, Field(description='Dimensions for this rendered piece (in pixels)') + Dimensions1 | None, Field(description='Dimensions for this rendered piece (in pixels)') ] = None parameters_from_format_id: Annotated[ Literal[True], diff --git a/src/adcp/types/generated_poc/core/webhook_payload.py b/src/adcp/types/generated_poc/core/mcp_webhook_payload.py similarity index 55% rename from src/adcp/types/generated_poc/core/webhook_payload.py rename to src/adcp/types/generated_poc/core/mcp_webhook_payload.py index 46b438fa..986f348f 100644 --- a/src/adcp/types/generated_poc/core/webhook_payload.py +++ b/src/adcp/types/generated_poc/core/mcp_webhook_payload.py @@ -1,35 +1,20 @@ # generated by datamodel-codegen: -# filename: core/webhook_payload.json -# timestamp: 2025-11-29T12:00:45+00:00 +# filename: core/mcp_webhook_payload.json +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations -from typing import Annotated, Any +from typing import Annotated from adcp.types.base import AdCPBaseModel from pydantic import AwareDatetime, ConfigDict, Field from ..enums import adcp_domain, task_status from ..enums import task_type as task_type_1 +from . import async_response_data -class Progress(AdCPBaseModel): - model_config = ConfigDict( - extra='forbid', - ) - current_step: Annotated[ - str | None, Field(description='Current step or phase of the operation') - ] = None - percentage: Annotated[ - float | None, Field(description='Completion percentage (0-100)', ge=0.0, le=100.0) - ] = None - step_number: Annotated[int | None, Field(description='Current step number', ge=1)] = None - total_steps: Annotated[ - int | None, Field(description='Total number of steps in the operation', ge=1) - ] = None - - -class WebhookPayload(AdCPBaseModel): +class McpWebhookPayload(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) @@ -45,10 +30,6 @@ class WebhookPayload(AdCPBaseModel): description='AdCP domain this task belongs to. Helps classify the operation type at a high level.' ), ] = None - error: Annotated[ - str | None, - Field(description="Error message for failed tasks. Only present when status is 'failed'."), - ] = None message: Annotated[ str | None, Field( @@ -61,22 +42,16 @@ class WebhookPayload(AdCPBaseModel): description='Publisher-defined operation identifier correlating a sequence of task updates across webhooks.' ), ] = None - progress: Annotated[ - Progress | None, - Field( - description="Progress information for tasks still in 'working' state. Rarely seen in webhooks since 'working' tasks typically complete synchronously, but may appear if a task transitions from 'submitted' to 'working'." - ), - ] = None result: Annotated[ - dict[str, Any] | None, + async_response_data.AdcpAsyncResponseData | None, Field( - description='Task-specific payload for this status update. Validated against the appropriate response schema based on task_type.' + description='Task-specific payload matching the status. For completed/failed, contains the full task response. For working/input-required/submitted, contains status-specific data. This is the data layer that AdCP specs - same structure used in A2A status.message.parts[].data.' ), ] = None status: Annotated[ task_status.TaskStatus, Field( - description='Current task status. Webhooks are only triggered for status changes after initial submission (e.g., submitted → input-required, submitted → completed, submitted → failed).' + description='Current task status. Webhooks are triggered for status changes after initial submission.' ), ] task_id: Annotated[ diff --git a/src/adcp/types/generated_poc/core/pricing_option.py b/src/adcp/types/generated_poc/core/pricing_option.py new file mode 100644 index 00000000..2af906a5 --- /dev/null +++ b/src/adcp/types/generated_poc/core/pricing_option.py @@ -0,0 +1,51 @@ +# generated by datamodel-codegen: +# filename: core/pricing_option.json +# timestamp: 2025-12-11T15:09:37+00:00 + +from __future__ import annotations + +from typing import Annotated + +from pydantic import Field, RootModel + +from ..pricing_options import ( + cpc_option, + cpcv_option, + cpm_auction_option, + cpm_fixed_option, + cpp_option, + cpv_option, + flat_rate_option, + vcpm_auction_option, + vcpm_fixed_option, +) + + +class PricingOption( + RootModel[ + cpm_fixed_option.CpmFixedRatePricingOption + | cpm_auction_option.CpmAuctionPricingOption + | vcpm_fixed_option.VcpmFixedRatePricingOption + | vcpm_auction_option.VcpmAuctionPricingOption + | cpc_option.CpcPricingOption + | cpcv_option.CpcvPricingOption + | cpv_option.CpvPricingOption + | cpp_option.CppPricingOption + | flat_rate_option.FlatRatePricingOption + ] +): + root: Annotated[ + cpm_fixed_option.CpmFixedRatePricingOption + | cpm_auction_option.CpmAuctionPricingOption + | vcpm_fixed_option.VcpmFixedRatePricingOption + | vcpm_auction_option.VcpmAuctionPricingOption + | cpc_option.CpcPricingOption + | cpcv_option.CpcvPricingOption + | cpv_option.CpvPricingOption + | cpp_option.CppPricingOption + | flat_rate_option.FlatRatePricingOption, + Field( + description='A pricing model option offered by a publisher for a product. Each pricing model has its own schema with model-specific requirements.', + title='Pricing Option', + ), + ] diff --git a/src/adcp/types/generated_poc/core/product.py b/src/adcp/types/generated_poc/core/product.py index ea9638d7..e767c4e8 100644 --- a/src/adcp/types/generated_poc/core/product.py +++ b/src/adcp/types/generated_poc/core/product.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/product.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations @@ -10,22 +10,11 @@ from pydantic import AwareDatetime, ConfigDict, Field from ..enums import delivery_type as delivery_type_1 -from ..pricing_options import ( - cpc_option, - cpcv_option, - cpm_auction_option, - cpm_fixed_option, - cpp_option, - cpv_option, - flat_rate_option, - vcpm_auction_option, - vcpm_fixed_option, -) from . import creative_policy as creative_policy_1 from . import ext as ext_1 from . import format_id as format_id_1 from . import measurement as measurement_1 -from . import placement, publisher_property_selector +from . import placement, pricing_option, publisher_property_selector from . import reporting_capabilities as reporting_capabilities_1 @@ -124,17 +113,7 @@ class Product(AdCPBaseModel): ), ] = None pricing_options: Annotated[ - list[ - cpm_fixed_option.CpmFixedRatePricingOption - | cpm_auction_option.CpmAuctionPricingOption - | vcpm_fixed_option.VcpmFixedRatePricingOption - | vcpm_auction_option.VcpmAuctionPricingOption - | cpc_option.CpcPricingOption - | cpcv_option.CpcvPricingOption - | cpv_option.CpvPricingOption - | cpp_option.CppPricingOption - | flat_rate_option.FlatRatePricingOption - ], + list[pricing_option.PricingOption], Field(description='Available pricing models for this product', min_length=1), ] product_card: Annotated[ @@ -151,11 +130,7 @@ class Product(AdCPBaseModel): ] = None product_id: Annotated[str, Field(description='Unique identifier for the product')] publisher_properties: Annotated[ - list[ - publisher_property_selector.PublisherPropertySelector1 - | publisher_property_selector.PublisherPropertySelector2 - | publisher_property_selector.PublisherPropertySelector3 - ], + list[publisher_property_selector.PublisherPropertySelector], Field( description="Publisher properties covered by this product. Buyers fetch actual property definitions from each publisher's adagents.json and validate agent authorization. Selection patterns mirror the authorization patterns in adagents.json for consistency.", min_length=1, diff --git a/src/adcp/types/generated_poc/core/promoted_offerings.py b/src/adcp/types/generated_poc/core/promoted_offerings.py index 7077611d..5039e65e 100644 --- a/src/adcp/types/generated_poc/core/promoted_offerings.py +++ b/src/adcp/types/generated_poc/core/promoted_offerings.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/promoted_offerings.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations @@ -8,10 +8,9 @@ from typing import Annotated, Any from adcp.types.base import AdCPBaseModel -from pydantic import AnyUrl, ConfigDict, Field +from pydantic import ConfigDict, Field -from . import brand_manifest as brand_manifest_1 -from . import promoted_products +from . import brand_manifest_ref, promoted_products class AssetType(Enum): @@ -68,24 +67,9 @@ class PromotedOfferings(AdCPBaseModel): Field(description='Selectors to choose specific assets from the brand manifest'), ] = None brand_manifest: Annotated[ - brand_manifest_1.BrandManifest | AnyUrl, + brand_manifest_ref.BrandManifestReference, Field( - description='Brand information manifest containing assets, themes, and guidelines. Can be provided inline or as a URL reference to a hosted manifest.', - examples=[ - { - 'data': { - 'colors': {'primary': '#FF6B35'}, - 'name': 'ACME Corporation', - 'url': 'https://acmecorp.com', - }, - 'description': 'Inline brand manifest', - }, - { - 'data': 'https://cdn.acmecorp.com/brand-manifest.json', - 'description': 'URL string reference to hosted manifest', - }, - ], - title='Brand Manifest Reference', + description='Brand information manifest containing assets, themes, and guidelines. Can be provided inline or as a URL reference to a hosted manifest.' ), ] offerings: Annotated[ diff --git a/src/adcp/types/generated_poc/core/property.py b/src/adcp/types/generated_poc/core/property.py index db3af228..f59ea5a2 100644 --- a/src/adcp/types/generated_poc/core/property.py +++ b/src/adcp/types/generated_poc/core/property.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/property.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations @@ -11,6 +11,7 @@ from ..enums import identifier_types from ..enums import property_type as property_type_1 +from . import property_id as property_id_1 from . import property_tag @@ -39,12 +40,9 @@ class Property(AdCPBaseModel): ] name: Annotated[str, Field(description='Human-readable property name')] property_id: Annotated[ - str | None, + property_id_1.PropertyId | None, Field( - description='Unique identifier for this property (optional). Enables referencing properties by ID instead of repeating full objects.', - examples=['cnn_ctv_app', 'homepage', 'mobile_ios', 'instagram'], - pattern='^[a-z0-9_]+$', - title='Property ID', + description='Unique identifier for this property (optional). Enables referencing properties by ID instead of repeating full objects.' ), ] = None property_type: Annotated[ diff --git a/src/adcp/types/generated_poc/core/publisher_property_selector.py b/src/adcp/types/generated_poc/core/publisher_property_selector.py index 4cc7e373..b0ed5b28 100644 --- a/src/adcp/types/generated_poc/core/publisher_property_selector.py +++ b/src/adcp/types/generated_poc/core/publisher_property_selector.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/publisher_property_selector.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations from typing import Annotated, Literal from adcp.types.base import AdCPBaseModel -from pydantic import ConfigDict, Field +from pydantic import ConfigDict, Field, RootModel from . import property_id, property_tag @@ -73,3 +73,16 @@ class PublisherPropertySelector3(AdCPBaseModel): selection_type: Annotated[ Literal['by_tag'], Field(description='Discriminator indicating selection by property tags') ] + + +class PublisherPropertySelector( + RootModel[PublisherPropertySelector1 | PublisherPropertySelector2 | PublisherPropertySelector3] +): + root: Annotated[ + PublisherPropertySelector1 | PublisherPropertySelector2 | PublisherPropertySelector3, + Field( + description="Selects properties from a publisher's adagents.json. Used for both product definitions and agent authorization. Supports three selection patterns: all properties, specific IDs, or by tags.", + discriminator='selection_type', + title='Publisher Property Selector', + ), + ] diff --git a/src/adcp/types/generated_poc/core/push_notification_config.py b/src/adcp/types/generated_poc/core/push_notification_config.py index 2f47ac09..150ff343 100644 --- a/src/adcp/types/generated_poc/core/push_notification_config.py +++ b/src/adcp/types/generated_poc/core/push_notification_config.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/push_notification_config.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-18T20:00:24+00:00 from __future__ import annotations @@ -34,9 +34,6 @@ class Authentication(AdCPBaseModel): class PushNotificationConfig(AdCPBaseModel): - model_config = ConfigDict( - extra='forbid', - ) authentication: Annotated[ Authentication, Field(description='Authentication configuration for webhook delivery (A2A-compatible)'), diff --git a/src/adcp/types/generated_poc/core/start_timing.py b/src/adcp/types/generated_poc/core/start_timing.py new file mode 100644 index 00000000..1b9689a7 --- /dev/null +++ b/src/adcp/types/generated_poc/core/start_timing.py @@ -0,0 +1,18 @@ +# generated by datamodel-codegen: +# filename: core/start_timing.json +# timestamp: 2025-12-11T15:09:37+00:00 + +from __future__ import annotations + +from typing import Annotated + +from pydantic import AwareDatetime, Field, RootModel + + +class StartTiming(RootModel[str | AwareDatetime]): + root: Annotated[ + str | AwareDatetime, + Field( + description="Campaign start timing: 'asap' or ISO 8601 date-time", title='Start Timing' + ), + ] diff --git a/src/adcp/types/generated_poc/core/sub_asset.py b/src/adcp/types/generated_poc/core/sub_asset.py index 5e20b5f1..0b0e1d67 100644 --- a/src/adcp/types/generated_poc/core/sub_asset.py +++ b/src/adcp/types/generated_poc/core/sub_asset.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/sub_asset.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations from typing import Annotated, Literal from adcp.types.base import AdCPBaseModel -from pydantic import AnyUrl, ConfigDict, Field +from pydantic import AnyUrl, ConfigDict, Field, RootModel class SubAsset1(AdCPBaseModel): @@ -53,3 +53,13 @@ class SubAsset2(AdCPBaseModel): description='Text content for text-based assets like headlines, body text, CTA text, etc.' ), ] + + +class SubAsset(RootModel[SubAsset1 | SubAsset2]): + root: Annotated[ + SubAsset1 | SubAsset2, + Field( + description='Sub-asset for multi-asset creative formats, including carousel images and native ad template variables', + title='Sub-Asset', + ), + ] diff --git a/src/adcp/types/generated_poc/creative/preview_creative_response.py b/src/adcp/types/generated_poc/creative/preview_creative_response.py index 469970c5..97e691bd 100644 --- a/src/adcp/types/generated_poc/creative/preview_creative_response.py +++ b/src/adcp/types/generated_poc/creative/preview_creative_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: creative/preview_creative_response.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations @@ -50,11 +50,7 @@ class Preview(AdCPBaseModel): ] preview_id: Annotated[str, Field(description='Unique identifier for this preview variant')] renders: Annotated[ - list[ - preview_render.PreviewRender1 - | preview_render.PreviewRender2 - | preview_render.PreviewRender3 - ], + list[preview_render.PreviewRender], Field( description='Array of rendered pieces for this preview variant. Most formats render as a single piece. Companion ad formats (video + banner), multi-placement formats, and adaptive formats render as multiple pieces.', min_length=1, @@ -93,14 +89,7 @@ class PreviewCreativeResponse1(AdCPBaseModel): class Preview1(AdCPBaseModel): input: Input4 preview_id: str - renders: Annotated[ - list[ - preview_render.PreviewRender1 - | preview_render.PreviewRender2 - | preview_render.PreviewRender3 - ], - Field(min_length=1), - ] + renders: Annotated[list[preview_render.PreviewRender], Field(min_length=1)] class Response(AdCPBaseModel): diff --git a/src/adcp/types/generated_poc/creative/preview_render.py b/src/adcp/types/generated_poc/creative/preview_render.py index b9145f8a..a6c85236 100644 --- a/src/adcp/types/generated_poc/creative/preview_render.py +++ b/src/adcp/types/generated_poc/creative/preview_render.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: creative/preview_render.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations @@ -134,17 +134,9 @@ class PreviewRender3(AdCPBaseModel): ] -class PreviewRender( - RootModel[ - PreviewRender1 - | PreviewRender2 - | PreviewRender3 - ] -): +class PreviewRender(RootModel[PreviewRender1 | PreviewRender2 | PreviewRender3]): root: Annotated[ - PreviewRender1 - | PreviewRender2 - | PreviewRender3, + PreviewRender1 | PreviewRender2 | PreviewRender3, Field( description='A single rendered piece of a creative preview with discriminated output format', title='Preview Render', diff --git a/src/adcp/types/generated_poc/media_buy/create_media_buy_async_response_input_required.py b/src/adcp/types/generated_poc/media_buy/create_media_buy_async_response_input_required.py new file mode 100644 index 00000000..ec2aecff --- /dev/null +++ b/src/adcp/types/generated_poc/media_buy/create_media_buy_async_response_input_required.py @@ -0,0 +1,37 @@ +# generated by datamodel-codegen: +# filename: media_buy/create_media_buy_async_response_input_required.json +# timestamp: 2025-12-11T15:09:37+00:00 + +from __future__ import annotations + +from enum import Enum +from typing import Annotated + +from adcp.types.base import AdCPBaseModel +from pydantic import ConfigDict, Field + +from ..core import context as context_1 +from ..core import error +from ..core import ext as ext_1 + + +class Reason(Enum): + APPROVAL_REQUIRED = 'APPROVAL_REQUIRED' + BUDGET_EXCEEDS_LIMIT = 'BUDGET_EXCEEDS_LIMIT' + + +class CreateMediaBuyInputRequired(AdCPBaseModel): + model_config = ConfigDict( + extra='forbid', + ) + context: context_1.ContextObject | None = None + errors: Annotated[ + list[error.Error] | None, + Field( + description='Optional validation errors or warnings for debugging purposes. Helps explain why input is required.' + ), + ] = None + ext: ext_1.ExtensionObject | None = None + reason: Annotated[ + Reason | None, Field(description='Reason code indicating why input is needed') + ] = None diff --git a/src/adcp/types/generated_poc/media_buy/create_media_buy_async_response_submitted.py b/src/adcp/types/generated_poc/media_buy/create_media_buy_async_response_submitted.py new file mode 100644 index 00000000..5635c3f3 --- /dev/null +++ b/src/adcp/types/generated_poc/media_buy/create_media_buy_async_response_submitted.py @@ -0,0 +1,19 @@ +# generated by datamodel-codegen: +# filename: media_buy/create_media_buy_async_response_submitted.json +# timestamp: 2025-12-11T15:09:37+00:00 + +from __future__ import annotations + +from adcp.types.base import AdCPBaseModel +from pydantic import ConfigDict + +from ..core import context as context_1 +from ..core import ext as ext_1 + + +class CreateMediaBuySubmitted(AdCPBaseModel): + model_config = ConfigDict( + extra='forbid', + ) + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None diff --git a/src/adcp/types/generated_poc/media_buy/create_media_buy_async_response_working.py b/src/adcp/types/generated_poc/media_buy/create_media_buy_async_response_working.py new file mode 100644 index 00000000..79e4fc15 --- /dev/null +++ b/src/adcp/types/generated_poc/media_buy/create_media_buy_async_response_working.py @@ -0,0 +1,31 @@ +# generated by datamodel-codegen: +# filename: media_buy/create_media_buy_async_response_working.json +# timestamp: 2025-12-11T15:09:37+00:00 + +from __future__ import annotations + +from typing import Annotated + +from adcp.types.base import AdCPBaseModel +from pydantic import ConfigDict, Field + +from ..core import context as context_1 +from ..core import ext as ext_1 + + +class CreateMediaBuyWorking(AdCPBaseModel): + model_config = ConfigDict( + extra='forbid', + ) + context: context_1.ContextObject | None = None + current_step: Annotated[ + str | None, Field(description='Current step or phase of the operation') + ] = None + ext: ext_1.ExtensionObject | None = None + percentage: Annotated[ + float | None, Field(description='Completion percentage (0-100)', ge=0.0, le=100.0) + ] = None + step_number: Annotated[int | None, Field(description='Current step number', ge=1)] = None + total_steps: Annotated[ + int | None, Field(description='Total number of steps in the operation', ge=1) + ] = None diff --git a/src/adcp/types/generated_poc/media_buy/create_media_buy_request.py b/src/adcp/types/generated_poc/media_buy/create_media_buy_request.py index 5026c898..629b283e 100644 --- a/src/adcp/types/generated_poc/media_buy/create_media_buy_request.py +++ b/src/adcp/types/generated_poc/media_buy/create_media_buy_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/create_media_buy_request.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations @@ -8,11 +8,12 @@ from typing import Annotated from adcp.types.base import AdCPBaseModel -from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field +from pydantic import AwareDatetime, ConfigDict, Field -from ..core import brand_manifest as brand_manifest_1 +from ..core import brand_manifest_ref from ..core import context as context_1 from ..core import ext as ext_1 +from ..core import start_timing from ..core.push_notification_config import PushNotificationConfig from . import package_request @@ -55,24 +56,9 @@ class CreateMediaBuyRequest(AdCPBaseModel): extra='forbid', ) brand_manifest: Annotated[ - brand_manifest_1.BrandManifest | AnyUrl, + brand_manifest_ref.BrandManifestReference, Field( - description='Brand information manifest serving as the namespace and identity for this media buy. Provides brand context, assets, and product catalog. Can be provided inline or as a URL reference to a hosted manifest. Can be cached and reused across multiple requests.', - examples=[ - { - 'data': { - 'colors': {'primary': '#FF6B35'}, - 'name': 'ACME Corporation', - 'url': 'https://acmecorp.com', - }, - 'description': 'Inline brand manifest', - }, - { - 'data': 'https://cdn.acmecorp.com/brand-manifest.json', - 'description': 'URL string reference to hosted manifest', - }, - ], - title='Brand Manifest Reference', + description='Brand information manifest serving as the namespace and identity for this media buy. Provides brand context, assets, and product catalog. Can be provided inline or as a URL reference to a hosted manifest. Can be cached and reused across multiple requests.' ), ] buyer_ref: Annotated[str, Field(description="Buyer's reference identifier for this media buy")] @@ -86,9 +72,4 @@ class CreateMediaBuyRequest(AdCPBaseModel): ] po_number: Annotated[str | None, Field(description='Purchase order number for tracking')] = None reporting_webhook: ReportingWebhook | None = None - start_time: Annotated[ - str | AwareDatetime, - Field( - description="Campaign start timing: 'asap' or ISO 8601 date-time", title='Start Timing' - ), - ] + start_time: start_timing.StartTiming diff --git a/src/adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py b/src/adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py index 8c7f8bc2..38394697 100644 --- a/src/adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py +++ b/src/adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py @@ -1,11 +1,11 @@ # generated by datamodel-codegen: # filename: media_buy/get_media_buy_delivery_response.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations from enum import Enum -from typing import Annotated +from typing import Annotated, Any from adcp.types.base import AdCPBaseModel from pydantic import AwareDatetime, ConfigDict, Field @@ -72,6 +72,7 @@ class Totals(DeliveryMetrics): ge=0.0, ), ] = None + spend: Any class NotificationType(Enum): @@ -133,6 +134,7 @@ class ByPackageItem(DeliveryMetrics): ge=0.0, ), ] + spend: Any class MediaBuyDelivery(AdCPBaseModel): diff --git a/src/adcp/types/generated_poc/media_buy/get_products_async_response_input_required.py b/src/adcp/types/generated_poc/media_buy/get_products_async_response_input_required.py new file mode 100644 index 00000000..c173bd85 --- /dev/null +++ b/src/adcp/types/generated_poc/media_buy/get_products_async_response_input_required.py @@ -0,0 +1,38 @@ +# generated by datamodel-codegen: +# filename: media_buy/get_products_async_response_input_required.json +# timestamp: 2025-12-11T15:09:37+00:00 + +from __future__ import annotations + +from enum import Enum +from typing import Annotated + +from adcp.types.base import AdCPBaseModel +from pydantic import ConfigDict, Field + +from ..core import context as context_1 +from ..core import ext as ext_1 +from ..core import product + + +class Reason(Enum): + CLARIFICATION_NEEDED = 'CLARIFICATION_NEEDED' + BUDGET_REQUIRED = 'BUDGET_REQUIRED' + + +class GetProductsInputRequired(AdCPBaseModel): + model_config = ConfigDict( + extra='forbid', + ) + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + partial_results: Annotated[ + list[product.Product] | None, + Field(description='Partial product results that may help inform the clarification'), + ] = None + reason: Annotated[ + Reason | None, Field(description='Reason code indicating why input is needed') + ] = None + suggestions: Annotated[ + list[str] | None, Field(description='Suggested values or options for the required input') + ] = None diff --git a/src/adcp/types/generated_poc/media_buy/get_products_async_response_submitted.py b/src/adcp/types/generated_poc/media_buy/get_products_async_response_submitted.py new file mode 100644 index 00000000..f54963bd --- /dev/null +++ b/src/adcp/types/generated_poc/media_buy/get_products_async_response_submitted.py @@ -0,0 +1,24 @@ +# generated by datamodel-codegen: +# filename: media_buy/get_products_async_response_submitted.json +# timestamp: 2025-12-11T15:09:37+00:00 + +from __future__ import annotations + +from typing import Annotated + +from adcp.types.base import AdCPBaseModel +from pydantic import AwareDatetime, ConfigDict, Field + +from ..core import context as context_1 +from ..core import ext as ext_1 + + +class GetProductsSubmitted(AdCPBaseModel): + model_config = ConfigDict( + extra='forbid', + ) + context: context_1.ContextObject | None = None + estimated_completion: Annotated[ + AwareDatetime | None, Field(description='Estimated completion time for the search') + ] = None + ext: ext_1.ExtensionObject | None = None diff --git a/src/adcp/types/generated_poc/media_buy/get_products_async_response_working.py b/src/adcp/types/generated_poc/media_buy/get_products_async_response_working.py new file mode 100644 index 00000000..e77495e4 --- /dev/null +++ b/src/adcp/types/generated_poc/media_buy/get_products_async_response_working.py @@ -0,0 +1,35 @@ +# generated by datamodel-codegen: +# filename: media_buy/get_products_async_response_working.json +# timestamp: 2025-12-11T15:09:37+00:00 + +from __future__ import annotations + +from typing import Annotated + +from adcp.types.base import AdCPBaseModel +from pydantic import ConfigDict, Field + +from ..core import context as context_1 +from ..core import ext as ext_1 + + +class GetProductsWorking(AdCPBaseModel): + model_config = ConfigDict( + extra='forbid', + ) + context: context_1.ContextObject | None = None + current_step: Annotated[ + str | None, + Field( + description="Current step in the search process (e.g., 'searching_inventory', 'validating_availability')" + ), + ] = None + ext: ext_1.ExtensionObject | None = None + percentage: Annotated[ + float | None, + Field(description='Progress percentage of the search operation', ge=0.0, le=100.0), + ] = None + step_number: Annotated[int | None, Field(description='Current step number (1-indexed)')] = None + total_steps: Annotated[ + int | None, Field(description='Total number of steps in the search process') + ] = None diff --git a/src/adcp/types/generated_poc/media_buy/get_products_request.py b/src/adcp/types/generated_poc/media_buy/get_products_request.py index ce9d5242..31576dcb 100644 --- a/src/adcp/types/generated_poc/media_buy/get_products_request.py +++ b/src/adcp/types/generated_poc/media_buy/get_products_request.py @@ -1,15 +1,15 @@ # generated by datamodel-codegen: # filename: media_buy/get_products_request.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations from typing import Annotated from adcp.types.base import AdCPBaseModel -from pydantic import AnyUrl, ConfigDict, Field +from pydantic import ConfigDict, Field -from ..core import brand_manifest as brand_manifest_1 +from ..core import brand_manifest_ref from ..core import context as context_1 from ..core import ext as ext_1 from ..core import product_filters @@ -20,24 +20,9 @@ class GetProductsRequest(AdCPBaseModel): extra='forbid', ) brand_manifest: Annotated[ - brand_manifest_1.BrandManifest | AnyUrl | None, + brand_manifest_ref.BrandManifestReference | None, Field( - description='Brand information manifest providing brand context, assets, and product catalog. Can be provided inline or as a URL reference to a hosted manifest.', - examples=[ - { - 'data': { - 'colors': {'primary': '#FF6B35'}, - 'name': 'ACME Corporation', - 'url': 'https://acmecorp.com', - }, - 'description': 'Inline brand manifest', - }, - { - 'data': 'https://cdn.acmecorp.com/brand-manifest.json', - 'description': 'URL string reference to hosted manifest', - }, - ], - title='Brand Manifest Reference', + description='Brand information manifest providing brand context, assets, and product catalog. Can be provided inline or as a URL reference to a hosted manifest.' ), ] = None brief: Annotated[ diff --git a/src/adcp/types/generated_poc/media_buy/list_creatives_response.py b/src/adcp/types/generated_poc/media_buy/list_creatives_response.py index 12ea9200..b34a7280 100644 --- a/src/adcp/types/generated_poc/media_buy/list_creatives_response.py +++ b/src/adcp/types/generated_poc/media_buy/list_creatives_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/list_creatives_response.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations @@ -147,12 +147,10 @@ class Creative(AdCPBaseModel): | html_asset.HtmlAsset | css_asset.CssAsset | javascript_asset.JavascriptAsset + | vast_asset.VastAsset + | daast_asset.DaastAsset | promoted_offerings.PromotedOfferings - | url_asset.UrlAsset - | vast_asset.VastAsset1 - | vast_asset.VastAsset2 - | daast_asset.DaastAsset1 - | daast_asset.DaastAsset2, + | url_asset.UrlAsset, ] | None, Field(description='Assets for this creative, keyed by asset_role'), @@ -180,7 +178,7 @@ class Creative(AdCPBaseModel): creative_status.CreativeStatus, Field(description='Current approval status of the creative') ] sub_assets: Annotated[ - list[sub_asset.SubAsset1 | sub_asset.SubAsset2] | None, + list[sub_asset.SubAsset] | None, Field( description='Sub-assets for multi-asset formats (included when include_sub_assets=true)' ), diff --git a/src/adcp/types/generated_poc/media_buy/sync_creatives_async_response_input_required.py b/src/adcp/types/generated_poc/media_buy/sync_creatives_async_response_input_required.py new file mode 100644 index 00000000..b34fbad1 --- /dev/null +++ b/src/adcp/types/generated_poc/media_buy/sync_creatives_async_response_input_required.py @@ -0,0 +1,31 @@ +# generated by datamodel-codegen: +# filename: media_buy/sync_creatives_async_response_input_required.json +# timestamp: 2025-12-18T20:00:24+00:00 + +from __future__ import annotations + +from enum import Enum +from typing import Annotated + +from adcp.types.base import AdCPBaseModel +from pydantic import ConfigDict, Field + +from ..core import context as context_1 +from ..core import ext as ext_1 + + +class Reason(Enum): + APPROVAL_REQUIRED = 'APPROVAL_REQUIRED' + ASSET_CONFIRMATION = 'ASSET_CONFIRMATION' + FORMAT_CLARIFICATION = 'FORMAT_CLARIFICATION' + + +class SyncCreativesInputRequired(AdCPBaseModel): + model_config = ConfigDict( + extra='forbid', + ) + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + reason: Annotated[ + Reason | None, Field(description='Reason code indicating why buyer input is needed') + ] = None diff --git a/src/adcp/types/generated_poc/media_buy/sync_creatives_async_response_submitted.py b/src/adcp/types/generated_poc/media_buy/sync_creatives_async_response_submitted.py new file mode 100644 index 00000000..4458b6e0 --- /dev/null +++ b/src/adcp/types/generated_poc/media_buy/sync_creatives_async_response_submitted.py @@ -0,0 +1,19 @@ +# generated by datamodel-codegen: +# filename: media_buy/sync_creatives_async_response_submitted.json +# timestamp: 2025-12-11T15:09:37+00:00 + +from __future__ import annotations + +from adcp.types.base import AdCPBaseModel +from pydantic import ConfigDict + +from ..core import context as context_1 +from ..core import ext as ext_1 + + +class SyncCreativesSubmitted(AdCPBaseModel): + model_config = ConfigDict( + extra='forbid', + ) + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None diff --git a/src/adcp/types/generated_poc/media_buy/sync_creatives_async_response_working.py b/src/adcp/types/generated_poc/media_buy/sync_creatives_async_response_working.py new file mode 100644 index 00000000..bca867c4 --- /dev/null +++ b/src/adcp/types/generated_poc/media_buy/sync_creatives_async_response_working.py @@ -0,0 +1,37 @@ +# generated by datamodel-codegen: +# filename: media_buy/sync_creatives_async_response_working.json +# timestamp: 2025-12-11T15:09:37+00:00 + +from __future__ import annotations + +from typing import Annotated + +from adcp.types.base import AdCPBaseModel +from pydantic import ConfigDict, Field + +from ..core import context as context_1 +from ..core import ext as ext_1 + + +class SyncCreativesWorking(AdCPBaseModel): + model_config = ConfigDict( + extra='forbid', + ) + context: context_1.ContextObject | None = None + creatives_processed: Annotated[ + int | None, Field(description='Number of creatives processed so far', ge=0) + ] = None + creatives_total: Annotated[ + int | None, Field(description='Total number of creatives to process', ge=0) + ] = None + current_step: Annotated[ + str | None, Field(description='Current step or phase of the operation') + ] = None + ext: ext_1.ExtensionObject | None = None + percentage: Annotated[ + float | None, Field(description='Completion percentage (0-100)', ge=0.0, le=100.0) + ] = None + step_number: Annotated[int | None, Field(description='Current step number', ge=1)] = None + total_steps: Annotated[ + int | None, Field(description='Total number of steps in the operation', ge=1) + ] = None diff --git a/src/adcp/types/generated_poc/media_buy/update_media_buy_async_response_input_required.py b/src/adcp/types/generated_poc/media_buy/update_media_buy_async_response_input_required.py new file mode 100644 index 00000000..aa5a1a40 --- /dev/null +++ b/src/adcp/types/generated_poc/media_buy/update_media_buy_async_response_input_required.py @@ -0,0 +1,30 @@ +# generated by datamodel-codegen: +# filename: media_buy/update_media_buy_async_response_input_required.json +# timestamp: 2025-12-11T15:09:37+00:00 + +from __future__ import annotations + +from enum import Enum +from typing import Annotated + +from adcp.types.base import AdCPBaseModel +from pydantic import ConfigDict, Field + +from ..core import context as context_1 +from ..core import ext as ext_1 + + +class Reason(Enum): + APPROVAL_REQUIRED = 'APPROVAL_REQUIRED' + CHANGE_CONFIRMATION = 'CHANGE_CONFIRMATION' + + +class UpdateMediaBuyInputRequired(AdCPBaseModel): + model_config = ConfigDict( + extra='forbid', + ) + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + reason: Annotated[ + Reason | None, Field(description='Reason code indicating why input is needed') + ] = None diff --git a/src/adcp/types/generated_poc/media_buy/update_media_buy_async_response_submitted.py b/src/adcp/types/generated_poc/media_buy/update_media_buy_async_response_submitted.py new file mode 100644 index 00000000..bee573cd --- /dev/null +++ b/src/adcp/types/generated_poc/media_buy/update_media_buy_async_response_submitted.py @@ -0,0 +1,19 @@ +# generated by datamodel-codegen: +# filename: media_buy/update_media_buy_async_response_submitted.json +# timestamp: 2025-12-11T15:09:37+00:00 + +from __future__ import annotations + +from adcp.types.base import AdCPBaseModel +from pydantic import ConfigDict + +from ..core import context as context_1 +from ..core import ext as ext_1 + + +class UpdateMediaBuySubmitted(AdCPBaseModel): + model_config = ConfigDict( + extra='forbid', + ) + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None diff --git a/src/adcp/types/generated_poc/media_buy/update_media_buy_async_response_working.py b/src/adcp/types/generated_poc/media_buy/update_media_buy_async_response_working.py new file mode 100644 index 00000000..9b332c56 --- /dev/null +++ b/src/adcp/types/generated_poc/media_buy/update_media_buy_async_response_working.py @@ -0,0 +1,31 @@ +# generated by datamodel-codegen: +# filename: media_buy/update_media_buy_async_response_working.json +# timestamp: 2025-12-11T15:09:37+00:00 + +from __future__ import annotations + +from typing import Annotated + +from adcp.types.base import AdCPBaseModel +from pydantic import ConfigDict, Field + +from ..core import context as context_1 +from ..core import ext as ext_1 + + +class UpdateMediaBuyWorking(AdCPBaseModel): + model_config = ConfigDict( + extra='forbid', + ) + context: context_1.ContextObject | None = None + current_step: Annotated[ + str | None, Field(description='Current step or phase of the operation') + ] = None + ext: ext_1.ExtensionObject | None = None + percentage: Annotated[ + float | None, Field(description='Completion percentage (0-100)', ge=0.0, le=100.0) + ] = None + step_number: Annotated[int | None, Field(description='Current step number', ge=1)] = None + total_steps: Annotated[ + int | None, Field(description='Total number of steps in the operation', ge=1) + ] = None diff --git a/src/adcp/types/generated_poc/media_buy/update_media_buy_request.py b/src/adcp/types/generated_poc/media_buy/update_media_buy_request.py index ab18e046..96684286 100644 --- a/src/adcp/types/generated_poc/media_buy/update_media_buy_request.py +++ b/src/adcp/types/generated_poc/media_buy/update_media_buy_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/update_media_buy_request.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations @@ -13,7 +13,7 @@ from ..core import creative_asset, creative_assignment from ..core import ext as ext_1 from ..core import push_notification_config as push_notification_config_1 -from ..core import targeting +from ..core import start_timing, targeting from ..enums import pacing as pacing_1 @@ -137,12 +137,7 @@ class UpdateMediaBuyRequest1(AdCPBaseModel): description='Optional webhook configuration for async update notifications. Publisher will send webhook when update completes if operation takes longer than immediate response time.' ), ] = None - start_time: Annotated[ - str | AwareDatetime | None, - Field( - description="Campaign start timing: 'asap' or ISO 8601 date-time", title='Start Timing' - ), - ] = None + start_time: start_timing.StartTiming | None = None Packages2 = Packages @@ -177,12 +172,7 @@ class UpdateMediaBuyRequest2(AdCPBaseModel): description='Optional webhook configuration for async update notifications. Publisher will send webhook when update completes if operation takes longer than immediate response time.' ), ] = None - start_time: Annotated[ - str | AwareDatetime | None, - Field( - description="Campaign start timing: 'asap' or ISO 8601 date-time", title='Start Timing' - ), - ] = None + start_time: start_timing.StartTiming | None = None class UpdateMediaBuyRequest(RootModel[UpdateMediaBuyRequest1 | UpdateMediaBuyRequest2]): diff --git a/src/adcp/types/generated_poc/signals/activate_signal_request.py b/src/adcp/types/generated_poc/signals/activate_signal_request.py index 4d5ca236..75872d4a 100644 --- a/src/adcp/types/generated_poc/signals/activate_signal_request.py +++ b/src/adcp/types/generated_poc/signals/activate_signal_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: signals/activate_signal_request.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations @@ -20,7 +20,7 @@ class ActivateSignalRequest(AdCPBaseModel): ) context: context_1.ContextObject | None = None deployments: Annotated[ - list[destination.Destination1 | destination.Destination2], + list[destination.Destination], Field( description='Target deployment(s) for activation. If the authenticated caller matches one of these deployment targets, activation keys will be included in the response.', min_length=1, diff --git a/src/adcp/types/generated_poc/signals/activate_signal_response.py b/src/adcp/types/generated_poc/signals/activate_signal_response.py index 466973d4..cce93060 100644 --- a/src/adcp/types/generated_poc/signals/activate_signal_response.py +++ b/src/adcp/types/generated_poc/signals/activate_signal_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: signals/activate_signal_response.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations @@ -20,7 +20,7 @@ class ActivateSignalResponse1(AdCPBaseModel): ) context: context_1.ContextObject | None = None deployments: Annotated[ - list[deployment.Deployment1 | deployment.Deployment2], + list[deployment.Deployment], Field(description='Array of deployment results for each deployment target'), ] ext: ext_1.ExtensionObject | None = None diff --git a/src/adcp/types/generated_poc/signals/get_signals_request.py b/src/adcp/types/generated_poc/signals/get_signals_request.py index e44f368d..78844115 100644 --- a/src/adcp/types/generated_poc/signals/get_signals_request.py +++ b/src/adcp/types/generated_poc/signals/get_signals_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: signals/get_signals_request.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations @@ -27,7 +27,7 @@ class DeliverTo(AdCPBaseModel): list[Country], Field(description='Countries where signals will be used (ISO codes)') ] deployments: Annotated[ - list[destination.Destination1 | destination.Destination2], + list[destination.Destination], Field( description='List of deployment targets (DSPs, sales agents, etc.). If the authenticated caller matches one of these deployment targets, activation keys will be included in the response.', min_length=1, diff --git a/src/adcp/types/generated_poc/signals/get_signals_response.py b/src/adcp/types/generated_poc/signals/get_signals_response.py index 17325531..7e45606c 100644 --- a/src/adcp/types/generated_poc/signals/get_signals_response.py +++ b/src/adcp/types/generated_poc/signals/get_signals_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: signals/get_signals_response.json -# timestamp: 2025-11-29T12:00:45+00:00 +# timestamp: 2025-12-11T15:09:37+00:00 from __future__ import annotations @@ -32,8 +32,7 @@ class Signal(AdCPBaseModel): ] data_provider: Annotated[str, Field(description='Name of the data provider')] deployments: Annotated[ - list[deployment.Deployment1 | deployment.Deployment2], - Field(description='Array of deployment targets'), + list[deployment.Deployment], Field(description='Array of deployment targets') ] description: Annotated[str, Field(description='Detailed signal description')] name: Annotated[str, Field(description='Human-readable signal name')] diff --git a/src/adcp/utils/preview_cache.py b/src/adcp/utils/preview_cache.py index d8de899a..55548d9b 100644 --- a/src/adcp/utils/preview_cache.py +++ b/src/adcp/utils/preview_cache.py @@ -87,13 +87,15 @@ async def get_preview_data_for_manifest( first_render = preview.renders[0] if preview.renders else None if first_render: - has_url = hasattr(first_render, "preview_url") - preview_url = str(first_render.preview_url) if has_url else None + # PreviewRender is a RootModel, access attributes via .root + render = first_render.root + has_url = hasattr(render, "preview_url") + preview_url = str(render.preview_url) if has_url else None preview_data = { "preview_id": preview.preview_id, "preview_url": preview_url, - "preview_html": getattr(first_render, "preview_html", None), - "render_id": first_render.render_id, + "preview_html": getattr(render, "preview_html", None), + "render_id": render.render_id, "input": preview.input.model_dump(), "expires_at": str(result.data.expires_at), } diff --git a/src/adcp/webhooks.py b/src/adcp/webhooks.py new file mode 100644 index 00000000..c6c5095f --- /dev/null +++ b/src/adcp/webhooks.py @@ -0,0 +1,508 @@ +"""Webhook creation and signing utilities for AdCP agents.""" + +from __future__ import annotations + +import hashlib +import hmac +import json +from datetime import datetime, timezone +from typing import Any, cast + +from a2a.types import ( + Artifact, + DataPart, + Message, + Part, + Role, + Task, + TaskState, + TaskStatus, + TaskStatusUpdateEvent, +) + +from adcp.types import GeneratedTaskStatus +from adcp.types.base import AdCPBaseModel +from adcp.types.generated_poc.core.async_response_data import AdcpAsyncResponseData + + +def create_mcp_webhook_payload( + task_id: str, + status: GeneratedTaskStatus, + result: AdcpAsyncResponseData | dict[str, Any] | None = None, + timestamp: datetime | None = None, + task_type: str | None = None, + operation_id: str | None = None, + message: str | None = None, + context_id: str | None = None, + domain: str | None = None, +) -> dict[str, Any]: + """ + Create MCP webhook payload dictionary. + + This function helps agent implementations construct properly formatted + webhook payloads for sending to clients. + + Args: + task_id: Unique identifier for the task + status: Current task status + task_type: Optionally type of AdCP operation (e.g., "get_products", "create_media_buy") + timestamp: When the webhook was generated (defaults to current UTC time) + result: Task-specific payload (AdCP response data) + operation_id: Publisher-defined operation identifier (deprecated from payload, + should be in URL routing, but included for backward compatibility) + message: Human-readable summary of task state + context_id: Session/conversation identifier + domain: AdCP domain this task belongs to + + Returns: + Dictionary matching McpWebhookPayload schema, ready to be sent as JSON + + Examples: + Create a completed webhook with results: + >>> from adcp.webhooks import create_mcp_webhook_payload + >>> from adcp.types import GeneratedTaskStatus + >>> + >>> payload = create_mcp_webhook_payload( + ... task_id="task_123", + ... task_type="get_products", + ... status=GeneratedTaskStatus.completed, + ... result={"products": [...]}, + ... message="Found 5 products" + ... ) + + Create a failed webhook with error: + >>> payload = create_mcp_webhook_payload( + ... task_id="task_456", + ... task_type="create_media_buy", + ... status=GeneratedTaskStatus.failed, + ... result={"errors": [{"code": "INVALID_INPUT", "message": "..."}]}, + ... message="Validation failed" + ... ) + + Create a working status update: + >>> payload = create_mcp_webhook_payload( + ... task_id="task_789", + ... task_type="sync_creatives", + ... status=GeneratedTaskStatus.working, + ... message="Processing 3 of 10 creatives" + ... ) + """ + if timestamp is None: + timestamp = datetime.now(timezone.utc) + + # Convert status enum to string value + status_value = status.value if hasattr(status, "value") else str(status) + + # Build payload matching McpWebhookPayload schema + payload: dict[str, Any] = { + "task_id": task_id, + "task_type": task_type, + "status": status_value, + "timestamp": timestamp.isoformat() if isinstance(timestamp, datetime) else timestamp, + } + + # Add optional fields only if provided + if result is not None: + # Convert Pydantic model to dict if needed for JSON serialization + if hasattr(result, "model_dump"): + payload["result"] = result.model_dump(mode="json") + else: + payload["result"] = result + + if operation_id is not None: + payload["operation_id"] = operation_id + + if message is not None: + payload["message"] = message + + if context_id is not None: + payload["context_id"] = context_id + + if domain is not None: + payload["domain"] = domain + + return payload + + +def get_adcp_signed_headers_for_webhook( + headers: dict[str, Any], secret: str, timestamp: str, payload: dict[str, Any] | AdCPBaseModel +) -> dict[str, Any]: + """ + Generate AdCP-compliant signed headers for webhook delivery. + + This function creates a cryptographic signature that proves the webhook + came from an authorized agent and protects against replay attacks by + including a timestamp in the signed message. + + The function adds two headers to the provided headers dict: + - X-AdCP-Signature: HMAC-SHA256 signature in format "sha256=" + - X-AdCP-Timestamp: ISO 8601 timestamp used in signature generation + + The signing algorithm: + 1. Constructs message as "{timestamp}.{json_payload}" + 2. JSON-serializes payload with compact separators (no sorted keys for performance) + 3. UTF-8 encodes the message + 4. HMAC-SHA256 signs with the shared secret + 5. Hex-encodes and prefixes with "sha256=" + + Args: + headers: Existing headers dictionary to add signature headers to + secret: Shared secret key for HMAC signing + timestamp: ISO 8601 timestamp string (e.g., "2025-01-15T10:00:00Z") + payload: Webhook payload (dict or Pydantic model - will be JSON-serialized) + + Returns: + The modified headers dictionary with signature headers added + + Examples: + Sign and send an MCP webhook: + >>> from adcp.webhooks import create_mcp_webhook_payload get_adcp_signed_headers_for_webhook + >>> from datetime import datetime, timezone + >>> + >>> payload = create_mcp_webhook_payload( + ... task_id="task_123", + ... task_type="get_products", + ... status="completed", + ... result={"products": [...]} + ... ) + >>> headers = {"Content-Type": "application/json"} + >>> timestamp = datetime.now(timezone.utc).isoformat() + >>> signed_headers = get_adcp_signed_headers_for_webhook( + ... headers, secret="my-webhook-secret", timestamp=timestamp, payload=payload + ... ) + >>> + >>> # Send webhook with signed headers + >>> import httpx + >>> response = await httpx.post( + ... webhook_url, + ... json=payload, + ... headers=signed_headers + ... ) + + Headers will contain: + >>> print(signed_headers) + { + "Content-Type": "application/json", + "X-AdCP-Signature": "sha256=a1b2c3...", + "X-AdCP-Timestamp": "2025-01-15T10:00:00Z" + } + + Sign with Pydantic model directly: + >>> from adcp import GetMediaBuyDeliveryResponse + >>> from datetime import datetime, timezone + >>> + >>> response: GetMediaBuyDeliveryResponse = ... # From API call + >>> headers = {"Content-Type": "application/json"} + >>> timestamp = datetime.now(timezone.utc).isoformat() + >>> signed_headers = get_adcp_signed_headers_for_webhook( + ... headers, secret="my-webhook-secret", timestamp=timestamp, payload=response + ... ) + >>> # Pydantic model is automatically converted to dict for signing + """ + # Convert Pydantic model to dict if needed + # All AdCP types inherit from AdCPBaseModel (Pydantic BaseModel) + if hasattr(payload, "model_dump"): + payload_dict = payload.model_dump(mode="json") + else: + payload_dict = payload + + # Serialize payload to JSON with consistent formatting + # Note: sort_keys=False for performance (key order doesn't affect signature) + payload_bytes = json.dumps(payload_dict, separators=(",", ":"), sort_keys=False).encode("utf-8") + + # Construct signed message: timestamp.payload + # Including timestamp prevents replay attacks + signed_message = f"{timestamp}.{payload_bytes.decode('utf-8')}" + + # Generate HMAC-SHA256 signature over timestamp + payload + signature_hex = hmac.new( + secret.encode("utf-8"), signed_message.encode("utf-8"), hashlib.sha256 + ).hexdigest() + + # Add AdCP-compliant signature headers + headers["X-AdCP-Signature"] = f"sha256={signature_hex}" + headers["X-AdCP-Timestamp"] = timestamp + + return headers + + +def extract_webhook_result_data(webhook_payload: dict[str, Any]) -> AdcpAsyncResponseData | None: + """ + Extract result data from webhook payload (MCP or A2A format). + + This utility function handles webhook payloads from both MCP and A2A protocols, + extracting the result data regardless of the webhook format. Useful for quick + inspection, logging, or custom webhook routing logic without requiring full + client initialization. + + Protocol Detection: + - A2A Task: Has "artifacts" field (terminated statuses: completed, failed) + - A2A TaskStatusUpdateEvent: Has nested "status.message" structure (intermediate statuses) + - MCP: Has "result" field directly + + Args: + webhook_payload: Raw webhook dictionary from HTTP request (JSON-deserialized) + + Returns: + AdcpAsyncResponseData union type containing the extracted AdCP response, or None + if no result present. For A2A webhooks, unwraps data from artifacts/message parts + structure. For MCP webhooks, returns the result field directly. + + Examples: + Extract from MCP webhook: + >>> mcp_payload = { + ... "task_id": "task_123", + ... "task_type": "create_media_buy", + ... "status": "completed", + ... "timestamp": "2025-01-15T10:00:00Z", + ... "result": {"media_buy_id": "mb_123", "buyer_ref": "ref_123", "packages": []} + ... } + >>> result = extract_webhook_result_data(mcp_payload) + >>> print(result["media_buy_id"]) + mb_123 + + Extract from A2A Task webhook: + >>> a2a_task_payload = { + ... "id": "task_456", + ... "context_id": "ctx_456", + ... "status": {"state": "completed", "timestamp": "2025-01-15T10:00:00Z"}, + ... "artifacts": [ + ... { + ... "artifact_id": "artifact_456", + ... "parts": [ + ... { + ... "data": { + ... "media_buy_id": "mb_456", + ... "buyer_ref": "ref_456", + ... "packages": [] + ... } + ... } + ... ] + ... } + ... ] + ... } + >>> result = extract_webhook_result_data(a2a_task_payload) + >>> print(result["media_buy_id"]) + mb_456 + + Extract from A2A TaskStatusUpdateEvent webhook: + >>> a2a_event_payload = { + ... "task_id": "task_789", + ... "context_id": "ctx_789", + ... "status": { + ... "state": "working", + ... "timestamp": "2025-01-15T10:00:00Z", + ... "message": { + ... "message_id": "msg_789", + ... "role": "agent", + ... "parts": [ + ... {"data": {"current_step": "processing", "percentage": 50}} + ... ] + ... } + ... }, + ... "final": False + ... } + >>> result = extract_webhook_result_data(a2a_event_payload) + >>> print(result["percentage"]) + 50 + + Handle webhook with no result: + >>> empty_payload = {"task_id": "task_000", "status": "working", "timestamp": "..."} + >>> result = extract_webhook_result_data(empty_payload) + >>> print(result) + None + """ + # Detect A2A Task format (has "artifacts" field) + if "artifacts" in webhook_payload: + # Extract from task.artifacts[].parts[] + artifacts = webhook_payload.get("artifacts", []) + if not artifacts: + return None + + # Use last artifact (most recent) + target_artifact = artifacts[-1] + parts = target_artifact.get("parts", []) + if not parts: + return None + + # Find DataPart (skip TextPart) + for part in parts: + # Check if this part has "data" field (DataPart) + if "data" in part: + data = part["data"] + # Unwrap {"response": {...}} wrapper if present (A2A convention) + if isinstance(data, dict) and "response" in data and len(data) == 1: + return cast(AdcpAsyncResponseData, data["response"]) + return cast(AdcpAsyncResponseData, data) + + return None + + # Detect A2A TaskStatusUpdateEvent format (has nested "status.message") + status = webhook_payload.get("status") + if isinstance(status, dict): + message = status.get("message") + if isinstance(message, dict): + # Extract from status.message.parts[] + parts = message.get("parts", []) + if not parts: + return None + + # Find DataPart + for part in parts: + if "data" in part: + data = part["data"] + # Unwrap {"response": {...}} wrapper if present + if isinstance(data, dict) and "response" in data and len(data) == 1: + return cast(AdcpAsyncResponseData, data["response"]) + return cast(AdcpAsyncResponseData, data) + + return None + + # MCP format: result field directly + return cast(AdcpAsyncResponseData | None, webhook_payload.get("result")) + + +def create_a2a_webhook_payload( + task_id: str, + status: GeneratedTaskStatus, + context_id: str, + result: AdcpAsyncResponseData | dict[str, Any], + timestamp: datetime | None = None, +) -> Task | TaskStatusUpdateEvent: + """ + Create A2A webhook payload (Task or TaskStatusUpdateEvent). + + Per A2A specification: + - Terminated statuses (completed, failed): Returns Task with artifacts[].parts[] + - Intermediate statuses (working, input-required, submitted): Returns TaskStatusUpdateEvent + with status.message.parts[] + + This function helps agent implementations construct properly formatted A2A webhook + payloads for sending to clients. + + Args: + task_id: Unique identifier for the task + status: Current task status + context_id: Session/conversation identifier (required by A2A protocol) + timestamp: When the webhook was generated (defaults to current UTC time) + result: Task-specific payload (AdCP response data) + + Returns: + Task object for terminated statuses, TaskStatusUpdateEvent for intermediate statuses + + Examples: + Create a completed Task webhook: + >>> from adcp.webhooks import create_a2a_webhook_payload + >>> from adcp.types import GeneratedTaskStatus + >>> + >>> task = create_a2a_webhook_payload( + ... task_id="task_123", + ... status=GeneratedTaskStatus.completed, + ... result={"products": [...]}, + ... message="Found 5 products" + ... ) + >>> # task is a Task object with artifacts containing the result + + Create a working status update: + >>> event = create_a2a_webhook_payload( + ... task_id="task_456", + ... status=GeneratedTaskStatus.working, + ... message="Processing 3 of 10 items" + ... ) + >>> # event is a TaskStatusUpdateEvent with status.message + + Send A2A webhook via HTTP POST: + >>> import httpx + >>> from a2a.types import Task + >>> + >>> payload = create_a2a_webhook_payload(...) + >>> # Serialize to dict for JSON + >>> if isinstance(payload, Task): + ... payload_dict = payload.model_dump(mode='json') + ... else: + ... payload_dict = payload.model_dump(mode='json') + >>> + >>> response = await httpx.post(webhook_url, json=payload_dict) + """ + if timestamp is None: + timestamp = datetime.now(timezone.utc) + + # Convert datetime to ISO string for A2A protocol + timestamp_str = timestamp.isoformat() if isinstance(timestamp, datetime) else timestamp + + # Map GeneratedTaskStatus to A2A status state string + status_value = status.value if hasattr(status, "value") else str(status) + + # Map AdCP status to A2A status state + # Note: A2A uses "input-required" (hyphenated) while AdCP uses "input_required" (underscore) + status_mapping = { + "completed": "completed", + "failed": "failed", + "working": "working", + "submitted": "submitted", + "input_required": "input-required", + } + a2a_status_state = status_mapping.get(status_value, status_value) + + # Build parts for the message/artifact + parts: list[Part] = [] + + # Add DataPart + # Convert AdcpAsyncResponseData to dict if it's a Pydantic model + if hasattr(result, "model_dump"): + result_dict: dict[str, Any] = result.model_dump(mode="json") + else: + result_dict = result + + data_part = DataPart(data=result_dict) + parts.append(Part(root=data_part)) + + # Determine if this is a terminated status (Task) or intermediate (TaskStatusUpdateEvent) + is_terminated = status in [GeneratedTaskStatus.completed, GeneratedTaskStatus.failed] + + # Convert string to TaskState enum + task_state_enum = TaskState(a2a_status_state) + + if is_terminated: + # Create Task object with artifacts for terminated statuses + task_status = TaskStatus(state=task_state_enum, timestamp=timestamp_str) + + # Build artifact with parts + # Note: Artifact requires artifact_id, use task_id as prefix + if parts: + artifact = Artifact( + artifact_id=f"{task_id}_result", + parts=parts, + ) + artifacts = [artifact] + else: + artifacts = [] + + return Task( + id=task_id, + status=task_status, + artifacts=artifacts, + context_id=context_id, + ) + else: + # Create TaskStatusUpdateEvent with status.message for intermediate statuses + # Build message with parts + if parts: + message_obj = Message( + message_id=f"{task_id}_msg", + role=Role.agent, # Agent is responding + parts=parts, + ) + else: + message_obj = None + + task_status = TaskStatus( + state=task_state_enum, timestamp=timestamp_str, message=message_obj + ) + + return TaskStatusUpdateEvent( + task_id=task_id, + status=task_status, + context_id=context_id, + final=False, # Intermediate statuses are not final + ) diff --git a/tests/test_preview_html.py b/tests/test_preview_html.py index 8227e627..7dd601c7 100644 --- a/tests/test_preview_html.py +++ b/tests/test_preview_html.py @@ -100,8 +100,9 @@ async def test_preview_creative(): assert result.success assert result.data assert len(result.data.previews) == 1 + # PreviewRender is a RootModel, access attributes via .root assert ( - str(result.data.previews[0].renders[0].preview_url) + str(result.data.previews[0].renders[0].root.preview_url) == "https://preview.example.com/abc123" ) mock_call.assert_called_once() diff --git a/tests/test_webhook_handling.py b/tests/test_webhook_handling.py new file mode 100644 index 00000000..0b4dd8b1 --- /dev/null +++ b/tests/test_webhook_handling.py @@ -0,0 +1,1033 @@ +"""Tests for webhook handling (MCP and A2A protocols).""" + +from __future__ import annotations + +import json +from datetime import datetime, timezone +from unittest.mock import MagicMock, patch + +import pytest +from a2a.types import Artifact, DataPart, Message, Part, Role, Task, TaskState, TaskStatus as A2ATaskStatus, TaskStatusUpdateEvent, TextPart + +from adcp.client import ADCPClient +from adcp.exceptions import ADCPWebhookSignatureError +from adcp.types.core import AgentConfig, Protocol, TaskStatus +from adcp.webhooks import extract_webhook_result_data + + +class TestMCPWebhooks: + """Test MCP webhook handling (HTTP POST with dict payload).""" + + def setup_method(self): + """Set up test fixtures.""" + self.config = AgentConfig( + id="test_agent", + agent_uri="https://test.example.com", + protocol=Protocol.MCP, + ) + self.client = ADCPClient(self.config, webhook_secret="test_secret") + + @pytest.mark.asyncio + async def test_mcp_webhook_completed_success(self): + """Test MCP webhook with completed status and valid response.""" + payload = { + "task_id": "task_123", + "task_type": "create_media_buy", + "status": "completed", + "timestamp": "2025-01-15T10:00:00Z", + "result": { + "media_buy_id": "mb_123", + "buyer_ref": "ref_123", + "packages": [] + }, + "message": "Media buy created successfully", + } + + result = await self.client.handle_webhook( + payload, task_type="create_media_buy", operation_id="op_123" + ) + + assert result.success is True + assert result.status == TaskStatus.COMPLETED + assert result.data is not None + assert result.metadata["task_id"] == "task_123" + assert result.metadata["operation_id"] == "op_123" + + @pytest.mark.asyncio + async def test_mcp_webhook_completed_with_errors(self): + """Test MCP webhook with completed status but has errors in result.""" + payload = { + "task_id": "task_456", + "task_type": "create_media_buy", + "status": "completed", + "timestamp": "2025-01-15T10:00:00Z", + "result": { + "errors": [{"code": "NOT_FOUND", "message": "No matching inventory"}] + }, + "message": "No matching inventory found", + } + + result = await self.client.handle_webhook( + payload, task_type="create_media_buy", operation_id="op_456" + ) + + # Completed status + assert result.status == TaskStatus.COMPLETED + # Error is in structured data, not in error field + assert result.data is not None + + @pytest.mark.asyncio + async def test_mcp_webhook_failed_status(self): + """Test MCP webhook with failed status.""" + payload = { + "task_id": "task_789", + "task_type": "create_media_buy", + "status": "failed", + "timestamp": "2025-01-15T10:00:00Z", + "result": { + "errors": [ + { + "code": "INTERNAL_ERROR", + "message": "Database connection failed", + } + ] + }, + "message": "Task failed due to internal error", + } + + result = await self.client.handle_webhook( + payload, task_type="create_media_buy", operation_id="op_789" + ) + + assert result.success is False + assert result.status == TaskStatus.FAILED + assert result.data is not None # Errors in structured data + assert result.metadata["message"] == "Task failed due to internal error" + + @pytest.mark.asyncio + async def test_mcp_webhook_working_status(self): + """Test MCP webhook with working status (async in progress).""" + payload = { + "task_id": "task_111", + "task_type": "create_media_buy", + "status": "working", + "timestamp": "2025-01-15T10:00:00Z", + "result": None, # Working status may have no result yet + "message": "Processing request...", + } + + result = await self.client.handle_webhook( + payload, task_type="create_media_buy", operation_id="op_111" + ) + + assert result.status == TaskStatus.WORKING + assert result.success is False # Not completed yet + + @pytest.mark.asyncio + async def test_mcp_webhook_input_required_status(self): + """Test MCP webhook with input-required status.""" + payload = { + "task_id": "task_222", + "task_type": "create_media_buy", + "status": "input-required", + "timestamp": "2025-01-15T10:00:00Z", + "result": { + "errors": [ + { + "code": "APPROVAL_REQUIRED", + "field": "total_budget", + "message": "Budget exceeds auto-approval threshold", + } + ], + }, + "message": "Campaign budget $150K requires VP approval", + "context_id": "ctx_abc", + } + + result = await self.client.handle_webhook( + payload, task_type="create_media_buy", operation_id="op_222" + ) + + assert result.status == TaskStatus.NEEDS_INPUT + assert result.success is False + assert result.data is not None # Errors in structured data + assert result.metadata["context_id"] == "ctx_abc" + + @pytest.mark.asyncio + async def test_mcp_webhook_signature_verification_valid(self): + """Test signature verification with valid HMAC.""" + payload = { + "task_id": "task_333", + "task_type": "create_media_buy", + "status": "completed", + "timestamp": "2025-01-15T10:00:00Z", + "result": { + "media_buy_id": "mb_333", + "buyer_ref": "ref_333", + "packages": [] + }, + } + + # Generate valid signature using {timestamp}.{payload} format + # (matching get_adcp_signed_headers_for_webhook) + import hashlib + import hmac + + header_timestamp = "2025-01-15T10:00:00Z" + payload_bytes = json.dumps(payload, separators=(",", ":"), sort_keys=False).encode( + "utf-8" + ) + signed_message = f"{header_timestamp}.{payload_bytes.decode('utf-8')}" + signature = hmac.new( + "test_secret".encode("utf-8"), signed_message.encode("utf-8"), hashlib.sha256 + ).hexdigest() + + result = await self.client.handle_webhook( + payload, + task_type="create_media_buy", + operation_id="op_333", + signature=signature, + timestamp=header_timestamp, + ) + + assert result.status == TaskStatus.COMPLETED + + @pytest.mark.asyncio + async def test_mcp_webhook_signature_verification_invalid(self): + """Test signature verification with invalid HMAC.""" + payload = { + "task_id": "task_444", + "task_type": "create_media_buy", + "status": "completed", + "timestamp": "2025-01-15T10:00:00Z", + "result": { + "media_buy_id": "mb_444", + "buyer_ref": "ref_444", + "packages": [] + }, + } + + with pytest.raises(ADCPWebhookSignatureError): + await self.client.handle_webhook( + payload, + task_type="create_media_buy", + operation_id="op_444", + signature="invalid_signature", + timestamp="2025-01-15T10:00:00Z", + ) + + @pytest.mark.asyncio + async def test_mcp_webhook_missing_required_fields(self): + """Test MCP webhook with missing required fields.""" + payload = { + # Missing task_id and timestamp + "status": "completed", + "result": {"products": []}, + } + + with pytest.raises(Exception): # Pydantic ValidationError + await self.client.handle_webhook( + payload, task_type="create_media_buy", operation_id="op_555" + ) + + +class TestA2AWebhooks: + """Test A2A webhook handling (Task objects from TaskStatusUpdateEvent).""" + + def setup_method(self): + """Set up test fixtures.""" + self.config = AgentConfig( + id="test_agent", + agent_uri="https://test.example.com", + protocol=Protocol.A2A, + ) + self.client = ADCPClient(self.config) + + @pytest.mark.asyncio + async def test_a2a_webhook_completed_success(self): + """Test A2A Task with completed status and valid AdCP payload.""" + media_buy_data = { + "media_buy_id": "mb_123", + "buyer_ref": "ref_123", + "packages": [] + } + + task = Task( + id="task_123", + context_id="ctx_456", + status=A2ATaskStatus(state="completed", timestamp=datetime.now(timezone.utc).isoformat()), + artifacts=[ + Artifact( + artifact_id="artifact_123", + parts=[ + Part(root=DataPart(data=media_buy_data)), + Part(root=TextPart(text="Media buy created")), + ] + ) + ], + ) + + result = await self.client.handle_webhook( + task, task_type="create_media_buy", operation_id="op_123" + ) + + assert result.success is True + assert result.status == TaskStatus.COMPLETED + assert result.data is not None + assert result.metadata["task_id"] == "task_123" + assert result.metadata["operation_id"] == "op_123" + + @pytest.mark.asyncio + async def test_a2a_webhook_completed_with_errors(self): + """Test A2A Task with completed status but errors in AdCP result.""" + error_data = { + "errors": [{"code": "NOT_FOUND", "message": "No matching inventory"}], + } + + task = Task( + id="task_456", + context_id="ctx_789", + status=A2ATaskStatus(state="completed", timestamp=datetime.now(timezone.utc).isoformat()), + artifacts=[ + Artifact( + artifact_id="test_artifact", + parts=[Part(root=DataPart(data=error_data))] + ) + ], + ) + + result = await self.client.handle_webhook( + task, task_type="create_media_buy", operation_id="op_456" + ) + + assert result.status == TaskStatus.COMPLETED + assert result.data is not None # Errors in structured data + + @pytest.mark.asyncio + async def test_a2a_webhook_failed_status(self): + """Test A2A Task with failed status.""" + error_data = { + "errors": [ + { + "code": "INTERNAL_ERROR", + "message": "Database connection failed", + } + ] + } + + task = Task( + id="task_789", + context_id="ctx_111", + status=A2ATaskStatus(state="failed", timestamp=datetime.now(timezone.utc).isoformat()), + artifacts=[ + Artifact( + + artifact_id="test_artifact", + + parts=[ + Part(root=DataPart(data=error_data)), + Part(root=TextPart(text="Task failed due to internal error")), + ] + ) + ], + ) + + result = await self.client.handle_webhook( + task, task_type="create_media_buy", operation_id="op_789" + ) + + assert result.success is False + assert result.status == TaskStatus.FAILED + assert result.data is not None # Errors in structured data + + @pytest.mark.asyncio + async def test_a2a_webhook_working_status(self): + """Test A2A Task with working status (async in progress).""" + task = Task( + id="task_111", + context_id="ctx_222", + status=A2ATaskStatus(state="working", timestamp=datetime.now(timezone.utc).isoformat()), + artifacts=[ + Artifact( + artifact_id="test_artifact", + parts=[ + Part(root=TextPart(text="Processing request...")), + ] + ) + ], + ) + + result = await self.client.handle_webhook( + task, task_type="create_media_buy", operation_id="op_111" + ) + + assert result.status == TaskStatus.WORKING + assert result.success is False # Not completed yet + + @pytest.mark.asyncio + async def test_a2a_webhook_input_required_status(self): + """Test A2A Task with input-required status.""" + input_data = { + "reason": "APPROVAL_REQUIRED", + } + + task = Task( + id="task_222", + context_id="ctx_333", + status=A2ATaskStatus( + state="input-required", timestamp=datetime.now(timezone.utc).isoformat() + ), + artifacts=[ + Artifact( + + artifact_id="test_artifact", + + parts=[ + Part(root=DataPart(data=input_data)), + Part(root=TextPart(text="Campaign budget $150K requires VP approval")), + ] + ) + ], + ) + + result = await self.client.handle_webhook( + task, task_type="create_media_buy", operation_id="op_222" + ) + + assert result.status == TaskStatus.NEEDS_INPUT + assert result.success is False + assert result.data is not None # Errors in structured data + + @pytest.mark.asyncio + async def test_a2a_webhook_missing_artifacts(self): + """Test A2A Task with no artifacts array.""" + task = Task( + id="task_333", + context_id="ctx_444", + status=A2ATaskStatus(state="completed", timestamp=datetime.now(timezone.utc).isoformat()), + artifacts=[], # Empty artifacts + ) + + result = await self.client.handle_webhook( + task, task_type="create_media_buy", operation_id="op_333" + ) + + # Should still return result, but with None/empty data + assert result.status == TaskStatus.COMPLETED + + @pytest.mark.asyncio + async def test_a2a_webhook_missing_data_part(self): + """Test A2A Task with no DataPart in artifacts.""" + task = Task( + id="task_444", + context_id="ctx_555", + status=A2ATaskStatus(state="completed", timestamp=datetime.now(timezone.utc).isoformat()), + artifacts=[ + Artifact( + + artifact_id="test_artifact", + + parts=[ + Part(root=TextPart(text="Only text, no data")) # Only TextPart + ] + ) + ], + ) + + result = await self.client.handle_webhook( + task, task_type="create_media_buy", operation_id="op_444" + ) + + # Should still return result, but with None/empty data + assert result.status == TaskStatus.COMPLETED + + @pytest.mark.asyncio + async def test_a2a_webhook_malformed_adcp_data(self): + """Test A2A Task with minimal data that passes basic validation.""" + # Minimal valid data structure + minimal_data = {"errors": [{"code": "TEST", "message": "Test error"}]} + + task = Task( + id="task_555", + context_id="ctx_666", + status=A2ATaskStatus(state="completed", timestamp=datetime.now(timezone.utc).isoformat()), + artifacts=[ + Artifact( + artifact_id="test_artifact", + parts=[Part(root=DataPart(data=minimal_data))] + ) + ], + ) + + result = await self.client.handle_webhook( + task, task_type="create_media_buy", operation_id="op_555" + ) + + # Should handle error response + assert result.status == TaskStatus.COMPLETED + + @pytest.mark.asyncio + async def test_a2a_webhook_taskstatusupdateevent_working(self): + """Test A2A TaskStatusUpdateEvent with working status (correct intermediate payload).""" + progress_data = { + "current_step": "fetching_inventory", + "percentage": 50, + } + + # Intermediate status uses TaskStatusUpdateEvent, not Task + event = TaskStatusUpdateEvent( + task_id="task_777", + context_id="ctx_888", + status=A2ATaskStatus( + state=TaskState.working, + timestamp=datetime.now(timezone.utc).isoformat(), + message=Message( + message_id="msg_777", + role=Role.agent, + parts=[ + Part(root=DataPart(data=progress_data)), + Part(root=TextPart(text="Processing request...")), + ], + ), + ), + final=False, + ) + + result = await self.client.handle_webhook( + event, task_type="create_media_buy", operation_id="op_777" + ) + + assert result.status == TaskStatus.WORKING + assert result.success is False + assert result.data is not None + + @pytest.mark.asyncio + async def test_a2a_webhook_taskstatusupdateevent_input_required(self): + """Test A2A TaskStatusUpdateEvent with input-required status.""" + input_data = { + "reason": "APPROVAL_REQUIRED", + } + + event = TaskStatusUpdateEvent( + task_id="task_888", + context_id="ctx_999", + status=A2ATaskStatus( + state=TaskState("input-required"), + timestamp=datetime.now(timezone.utc).isoformat(), + message=Message( + message_id="msg_888", + role=Role.agent, + parts=[ + Part(root=DataPart(data=input_data)), + Part(root=TextPart(text="Campaign budget $150K requires VP approval")), + ], + ), + ), + final=False, + ) + + result = await self.client.handle_webhook( + event, task_type="create_media_buy", operation_id="op_888" + ) + + assert result.status == TaskStatus.NEEDS_INPUT + assert result.success is False + assert result.data is not None # Errors in structured data + assert result.metadata["context_id"] == "ctx_999" + + @pytest.mark.asyncio + async def test_a2a_webhook_taskstatusupdateevent_submitted(self): + """Test A2A TaskStatusUpdateEvent with submitted status.""" + event = TaskStatusUpdateEvent( + task_id="task_999", + context_id="ctx_000", + status=A2ATaskStatus( + state=TaskState.submitted, + timestamp=datetime.now(timezone.utc).isoformat(), + message=Message( + message_id="msg_999", + role=Role.agent, + parts=[ + Part(root=TextPart(text="Task submitted and queued for processing")), + ], + ), + ), + final=False, + ) + + result = await self.client.handle_webhook( + event, task_type="create_media_buy", operation_id="op_999" + ) + + assert result.status == TaskStatus.SUBMITTED + assert result.success is False + assert result.metadata["task_id"] == "task_999" + + @pytest.mark.asyncio + async def test_a2a_webhook_taskstatusupdateevent_no_message(self): + """Test A2A TaskStatusUpdateEvent with no status.message (edge case).""" + event = TaskStatusUpdateEvent( + task_id="task_1010", + context_id="ctx_1010", + status=A2ATaskStatus( + state=TaskState.working, + timestamp=datetime.now(timezone.utc).isoformat(), + message=None, # No message + ), + final=False, + ) + + result = await self.client.handle_webhook( + event, task_type="create_media_buy", operation_id="op_1010" + ) + + assert result.status == TaskStatus.WORKING + assert result.data is None # No data extracted + + @pytest.mark.asyncio + async def test_a2a_webhook_signature_not_required(self): + """Verify signature parameter is ignored for A2A webhooks.""" + task = Task( + id="task_666", + context_id="ctx_777", + status=A2ATaskStatus(state="completed", timestamp=datetime.now(timezone.utc).isoformat()), + artifacts=[ + Artifact( + artifact_id="test_artifact", + parts=[ + Part(root=DataPart(data={ + "media_buy_id": "mb_666", + "buyer_ref": "ref_666", + "packages": [] + })) + ] + ) + ], + ) + + # Signature should be ignored for A2A webhooks + result = await self.client.handle_webhook( + task, + task_type="create_media_buy", + operation_id="op_666", + signature="ignored_signature", + ) + + assert result.status == TaskStatus.COMPLETED + + +class TestUnifiedInterface: + """Test unified webhook interface across protocols.""" + + def setup_method(self): + """Set up test fixtures.""" + self.mcp_config = AgentConfig( + id="mcp_agent", + agent_uri="https://mcp.example.com", + protocol=Protocol.MCP, + ) + self.a2a_config = AgentConfig( + id="a2a_agent", + agent_uri="https://a2a.example.com", + protocol=Protocol.A2A, + ) + self.mcp_client = ADCPClient(self.mcp_config) + self.a2a_client = ADCPClient(self.a2a_config) + + @pytest.mark.asyncio + async def test_type_detection_mcp_dict(self): + """Verify dict payload routes to MCP handler.""" + payload = { + "task_id": "task_mcp", + "task_type": "create_media_buy", + "status": "completed", + "timestamp": "2025-01-15T10:00:00Z", + "result": { + "media_buy_id": "mb_mcp", + "buyer_ref": "ref_mcp", + "packages": [] + }, + } + + result = await self.mcp_client.handle_webhook( + payload, task_type="create_media_buy", operation_id="op_mcp" + ) + + assert result.status == TaskStatus.COMPLETED + assert result.metadata["task_id"] == "task_mcp" + + @pytest.mark.asyncio + async def test_type_detection_a2a_task(self): + """Verify Task object routes to A2A handler.""" + task = Task( + id="task_a2a", + context_id="ctx_a2a", + status=A2ATaskStatus(state="completed", timestamp=datetime.now(timezone.utc).isoformat()), + artifacts=[ + Artifact( + artifact_id="test_artifact", + parts=[Part(root=DataPart(data={ + "media_buy_id": "mb_a2a", + "buyer_ref": "ref_a2a", + "packages": [] + }))] + ) + ], + ) + + result = await self.a2a_client.handle_webhook( + task, task_type="create_media_buy", operation_id="op_a2a" + ) + + assert result.status == TaskStatus.COMPLETED + assert result.metadata["task_id"] == "task_a2a" + + @pytest.mark.asyncio + async def test_type_detection_a2a_taskstatusupdateevent(self): + """Verify TaskStatusUpdateEvent object routes to A2A handler.""" + event = TaskStatusUpdateEvent( + task_id="task_event", + context_id="ctx_event", + status=A2ATaskStatus( + state=TaskState.working, + timestamp=datetime.now(timezone.utc).isoformat(), + message=Message( + message_id="msg_event", + role=Role.agent, + parts=[Part(root=TextPart(text="Processing"))], + ), + ), + final=False, + ) + + result = await self.a2a_client.handle_webhook( + event, task_type="create_media_buy", operation_id="op_event" + ) + + assert result.status == TaskStatus.WORKING + assert result.metadata["task_id"] == "task_event" + + @pytest.mark.asyncio + async def test_consistent_result_format(self): + """Verify MCP and A2A return identical TaskResult structure.""" + media_buy_data = { + "media_buy_id": "mb_test", + "buyer_ref": "ref_test", + "packages": [] + } + + # MCP webhook + mcp_payload = { + "task_id": "task_1", + "task_type": "create_media_buy", + "status": "completed", + "timestamp": "2025-01-15T10:00:00Z", + "result": media_buy_data, + } + + # A2A webhook with same data + a2a_task = Task( + id="task_2", + context_id="ctx_2", + status=A2ATaskStatus(state="completed", timestamp=datetime.now(timezone.utc).isoformat()), + artifacts=[ + Artifact( + artifact_id="test_artifact", + parts=[ + Part(root=DataPart(data=media_buy_data)) + ] + ) + ], + ) + + mcp_result = await self.mcp_client.handle_webhook( + mcp_payload, task_type="create_media_buy", operation_id="op_1" + ) + a2a_result = await self.a2a_client.handle_webhook( + a2a_task, task_type="create_media_buy", operation_id="op_2" + ) + + # Both should return same structure + assert mcp_result.success == a2a_result.success + assert mcp_result.status == a2a_result.status + assert mcp_result.data is not None + assert a2a_result.data is not None + + +class TestExtractWebhookResultData: + """Test extract_webhook_result_data utility function.""" + + def test_extract_from_mcp_webhook(self): + """Test extracting result from MCP webhook payload.""" + mcp_payload = { + "task_id": "task_123", + "task_type": "create_media_buy", + "status": "completed", + "timestamp": "2025-01-15T10:00:00Z", + "result": { + "media_buy_id": "mb_123", + "buyer_ref": "ref_123", + "packages": [] + }, + } + + result = extract_webhook_result_data(mcp_payload) + + assert result is not None + assert result["media_buy_id"] == "mb_123" + assert result["buyer_ref"] == "ref_123" + assert result["packages"] == [] + + def test_extract_from_a2a_task_webhook(self): + """Test extracting result from A2A Task webhook payload.""" + media_buy_data = { + "media_buy_id": "mb_456", + "buyer_ref": "ref_456", + "packages": [] + } + + task = Task( + id="task_456", + context_id="ctx_456", + status=A2ATaskStatus(state="completed", timestamp=datetime.now(timezone.utc).isoformat()), + artifacts=[ + Artifact( + artifact_id="artifact_456", + parts=[ + Part(root=DataPart(data=media_buy_data)), + Part(root=TextPart(text="Media buy created")), + ] + ) + ], + ) + + # Convert to dict (simulating JSON deserialization) + task_dict = task.model_dump(mode='json') + result = extract_webhook_result_data(task_dict) + + assert result is not None + assert result["media_buy_id"] == "mb_456" + assert result["buyer_ref"] == "ref_456" + + def test_extract_from_a2a_taskstatusupdateevent_webhook(self): + """Test extracting result from A2A TaskStatusUpdateEvent webhook payload.""" + progress_data = { + "current_step": "fetching_inventory", + "percentage": 50, + } + + event = TaskStatusUpdateEvent( + task_id="task_777", + context_id="ctx_777", + status=A2ATaskStatus( + state=TaskState.working, + timestamp=datetime.now(timezone.utc).isoformat(), + message=Message( + message_id="msg_777", + role=Role.agent, + parts=[ + Part(root=DataPart(data=progress_data)), + Part(root=TextPart(text="Processing...")), + ], + ), + ), + final=False, + ) + + # Convert to dict (simulating JSON deserialization) + event_dict = event.model_dump(mode='json') + result = extract_webhook_result_data(event_dict) + + assert result is not None + assert result["current_step"] == "fetching_inventory" + assert result["percentage"] == 50 + + def test_extract_from_a2a_with_response_wrapper(self): + """Test extracting result from A2A payload with {"response": {...}} wrapper.""" + wrapped_data = { + "response": { + "media_buy_id": "mb_789", + "buyer_ref": "ref_789", + "packages": [] + } + } + + task = Task( + id="task_789", + context_id="ctx_789", + status=A2ATaskStatus(state="completed", timestamp=datetime.now(timezone.utc).isoformat()), + artifacts=[ + Artifact( + artifact_id="artifact_789", + parts=[Part(root=DataPart(data=wrapped_data))] + ) + ], + ) + + # Convert to dict + task_dict = task.model_dump(mode='json') + result = extract_webhook_result_data(task_dict) + + # Should unwrap the response wrapper + assert result is not None + assert "response" not in result # Unwrapped + assert result["media_buy_id"] == "mb_789" + + def test_extract_from_mcp_with_null_result(self): + """Test extracting from MCP webhook with None result.""" + mcp_payload = { + "task_id": "task_111", + "task_type": "create_media_buy", + "status": "working", + "timestamp": "2025-01-15T10:00:00Z", + "result": None, + } + + result = extract_webhook_result_data(mcp_payload) + + assert result is None + + def test_extract_from_a2a_with_empty_artifacts(self): + """Test extracting from A2A Task with empty artifacts array.""" + task = Task( + id="task_222", + context_id="ctx_222", + status=A2ATaskStatus(state="completed", timestamp=datetime.now(timezone.utc).isoformat()), + artifacts=[], + ) + + task_dict = task.model_dump(mode='json') + result = extract_webhook_result_data(task_dict) + + assert result is None + + def test_extract_from_a2a_with_no_data_part(self): + """Test extracting from A2A Task with only TextPart (no DataPart).""" + task = Task( + id="task_333", + context_id="ctx_333", + status=A2ATaskStatus(state="completed", timestamp=datetime.now(timezone.utc).isoformat()), + artifacts=[ + Artifact( + artifact_id="artifact_333", + parts=[Part(root=TextPart(text="Only text, no data"))] + ) + ], + ) + + task_dict = task.model_dump(mode='json') + result = extract_webhook_result_data(task_dict) + + assert result is None + + def test_extract_from_a2a_with_multiple_artifacts(self): + """Test extracting from A2A Task with multiple artifacts (should use last).""" + old_data = {"media_buy_id": "mb_old"} + new_data = {"media_buy_id": "mb_new"} + + task = Task( + id="task_444", + context_id="ctx_444", + status=A2ATaskStatus(state="completed", timestamp=datetime.now(timezone.utc).isoformat()), + artifacts=[ + Artifact( + artifact_id="artifact_old", + parts=[Part(root=DataPart(data=old_data))] + ), + Artifact( + artifact_id="artifact_new", + parts=[Part(root=DataPart(data=new_data))] + ), + ], + ) + + task_dict = task.model_dump(mode='json') + result = extract_webhook_result_data(task_dict) + + # Should use last artifact + assert result is not None + assert result["media_buy_id"] == "mb_new" + + def test_extract_from_a2a_taskstatusupdateevent_with_no_message(self): + """Test extracting from A2A TaskStatusUpdateEvent with no status.message.""" + event = TaskStatusUpdateEvent( + task_id="task_555", + context_id="ctx_555", + status=A2ATaskStatus( + state=TaskState.working, + timestamp=datetime.now(timezone.utc).isoformat(), + message=None, + ), + final=False, + ) + + event_dict = event.model_dump(mode='json') + result = extract_webhook_result_data(event_dict) + + assert result is None + + def test_extract_from_mcp_with_missing_result_field(self): + """Test extracting from MCP webhook without result field.""" + mcp_payload = { + "task_id": "task_666", + "task_type": "create_media_buy", + "status": "working", + "timestamp": "2025-01-15T10:00:00Z", + # No result field + } + + result = extract_webhook_result_data(mcp_payload) + + assert result is None + + def test_extract_from_a2a_with_nested_response_wrapper(self): + """Test that only single-key {"response": {...}} wrapper is unwrapped.""" + # Data with response wrapper but also other keys (should NOT unwrap) + data_with_extra_keys = { + "response": {"media_buy_id": "mb_777"}, + "other_key": "value" + } + + task = Task( + id="task_777", + context_id="ctx_777", + status=A2ATaskStatus(state="completed", timestamp=datetime.now(timezone.utc).isoformat()), + artifacts=[ + Artifact( + artifact_id="artifact_777", + parts=[Part(root=DataPart(data=data_with_extra_keys))] + ) + ], + ) + + task_dict = task.model_dump(mode='json') + result = extract_webhook_result_data(task_dict) + + # Should NOT unwrap (has multiple keys) + assert result is not None + assert "response" in result + assert "other_key" in result + + def test_extract_from_mcp_with_error_response(self): + """Test extracting from MCP webhook with error response.""" + mcp_payload = { + "task_id": "task_888", + "task_type": "create_media_buy", + "status": "failed", + "timestamp": "2025-01-15T10:00:00Z", + "result": { + "errors": [ + { + "code": "INTERNAL_ERROR", + "message": "Database connection failed", + } + ] + }, + } + + result = extract_webhook_result_data(mcp_payload) + + assert result is not None + assert "errors" in result + assert len(result["errors"]) == 1 + assert result["errors"][0]["code"] == "INTERNAL_ERROR"