v0.10.0 — bring your own Google Cloud project (bundled OAuth retired)
What's New
This is a breaking release that resolves the OAuth verification saga for good, lays the foundation that powers AdLoop Cloud, and fixes three reported bugs.
⚠️ BREAKING — bundled OAuth credentials removed
AdLoop no longer ships a built-in OAuth client. The backstory: Google's verification review for the shared client never completed after 2+ months, and we learned that the 100-user limit on unverified apps is a lifetime cap per project — revoked users don't free up seats, and there is no way to reset the counter. Waiting was not a plan, so we removed the dependency entirely.
Every install now uses its own (free) Google Cloud project:
- New users: run
adloop init— the wizard walks you through the ~5-minute one-time setup (create project, enable three APIs, create an OAuth client). No shared caps, no dependency on anyone's verification review, your own consent screen. - Existing users on bundled credentials (≤ 0.9): your stored token can no longer be refreshed after upgrading. Run
adloop initonce to switch to your own project — your config, rules, and everything else carry over. - Prefer zero setup? AdLoop Cloud is the hosted version: connect Google in two clicks, no Cloud project, no developer token, EU-hosted. Free beta, limited seats.
Discussion #13 is closed accordingly — the "pending verification" state it tracked no longer exists.
Multi-tenant runtime foundation
The internals that let one AdLoop process serve many isolated tenants (this is what runs AdLoop Cloud):
- New
adloop.runtimemodule: request-scoped config + tenant context (contextvars), explicit deployment modes (stdio/server), and pluggable backends for credentials, plan storage, and audit logging. import adloop.serveris now side-effect-free — no config loading or file writes at import time.- Plan store keys plans per tenant; the audit sink is swappable (file-based remains the OSS default — behavior for normal stdio users is unchanged).
Security — SSRF guard on ad/sitelink URL validation
Reported in #41 (thanks for the detailed PoC): final_url validation used to fetch caller-controlled URLs with no restrictions. Now:
- http/https scheme allowlist (no more
file://existence oracle) - Private, loopback, link-local, and cloud-metadata addresses are rejected after DNS resolution
- Every redirect hop is re-checked through the same guard
draft_image_assetsrefuses local file reads when running in server mode
Bug fixes
attach_shared_set_to_campaigns/detach_...crashed at apply time (#40) —partial_failureis now set on the request object instead of being passed as an unsupported kwarg. Fix by @illia-sapryga (cherry-picked from PR #34 with attribution — thanks!).run_gaqlhid Google's real error message (#47) — errors now surface the structuredfailure.errors[]detail (exact code + Google's own message naming the field at fault), with the generic hint only appended as a suffix.run_gaqldropped nested message values (#47) — nested proto messages (targeting settings and the like) now serialize to plain dicts instead of collapsing to theirstr()repr. Text-carrying assets (RSA headlines etc.) still return plain strings.
Docs
- The orchestration rules gained an audience-type ↔ campaign-type compatibility matrix (#44) — including the explicit callouts that custom segments cannot attach to Search campaigns and that Performance Max only takes audience signals, never hard targeting.
Tests: 293 pass (was 262 in 0.9.0 — new runtime, GAQL-error, and shared-set suites).
Full Changelog: v0.9.0...v0.10.0