Skip to content

feat(google-ads): budget type (daily vs CUSTOM_PERIOD total) get and set (#366)#371

Merged
hyoshi merged 6 commits into
mainfrom
feat/366-google-ads-budget-period
Jul 8, 2026
Merged

feat(google-ads): budget type (daily vs CUSTOM_PERIOD total) get and set (#366)#371
hyoshi merged 6 commits into
mainfrom
feat/366-google-ads-budget-period

Conversation

@hyoshi

@hyoshi hyoshi commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Part of #366 (task 3 — budget type get/set). Successor of #370, which GitHub auto-closed when its stacked base branch was deleted on the #369 merge; now based on main with #368/#369 already merged. Closes #366.

Summary

Google Ads budgets come in two types — DAILY and CUSTOM_PERIOD (campaign-lifetime total). mureo previously read/wrote only the daily amount. This PR:

Surface Change
google_ads_budget_get Now returns name, period (DAILY / CUSTOM_PERIOD), delivery_method, enum-mapped status, total_budget / total_amount_micros (null unless CUSTOM_PERIOD), reference_count — fixing the long-standing drift where the description promised these fields
google_ads_budget_update Accepts total_amount / total_amount_micros alongside (or instead of) the daily amount, with a dynamic update mask
google_ads_budget_create Accepts period + total amounts; required relaxed to ["name"] + anyOf so a pure total budget needs no daily amount

Design notes

  • period is create-only: the Google Ads API marks CampaignBudget.period Immutable (verified from the proto docstrings), so update_budget deliberately does not accept it, despite the issue text suggesting both. Symmetric fail-fast guards: a total without period='CUSTOM_PERIOD' and CUSTOM_PERIOD without a total are both ValueErrors before any API call.
  • Catastrophe guard composition: every amount path (daily + total, create + update) routes through the shared _resolve_amount_micros — positive, _MAX_BUDGET_AMOUNT_MICROS ceiling, bool/inf rejection, pair mutual exclusion. The create path's legacy bare int(amount_micros) (no ceiling) is gone.
  • Guardrail gate composition (feat(policy): built-in STRATEGY.md guardrail gate (OSS enforcement) #360/Meta Ads: add end_time (incl. 0=no-end) and lifetime_budget to meta_ads_ad_sets_update #367): max_lifetime_budget_per_campaign now catches Google totals in both spellings (total_amount currency units, total_amount_micros ÷1e6), and the daily cap additionally reads amount_micros — closing parameter-form bypasses found in security review.
  • BYOD: get_budget shape parity (period=DAILY, null totals).
  • Docs: _mureo-google-ads + _mureo-strategy skills (canonical + packaged byte-identical).

Test plan

  • Client: get_budget field mapping (CUSTOM_PERIOD + DAILY/null-total), total update exact micros / unit conversion / mutual exclusion / zero / ceiling, daily+total combined mask, create with period+total, contradictory combinations, invalid period, no-amount rejection — plus all 16 pre-existing budget tests unchanged
  • Gate: total_amount_micros and total_amount deny over cap / allow under; amount_micros hits the daily cap; no daily↔lifetime cross-application
  • MCP: schemas (anyOf), handler forwarding (total-only update, period+total create, legacy daily create unchanged)
  • ruff / black / mypy (CI flags) clean; full suite green except known local plugin-env noise

TDD (tests first). Reviews: security-reviewer (HIGH total_amount gate bypass → fixed → CONFIRMED) + python-reviewer (MEDIUM schema wording + 2 LOW → fixed → CONFIRM).

https://claude.ai/code/session_0195WBK6YopTveq9RAQE2xMz

hyoshi added 6 commits July 8, 2026 18:19
…ds_ad_sets_update (#367)

- Expose end_time (ISO 8601 string | UNIX timestamp | 0 to clear the end
  date per the Meta Marketing API convention) and lifetime_budget on the
  meta_ads_ad_sets_update tool inputSchema and forward them to
  update_ad_set.
- Normalize the string "0" to the integer 0 before the contradiction
  guard so a string-typed zero cannot sidestep it; reject bool, negative,
  and blank end_time values.
- Validate daily_budget/lifetime_budget mutual exclusion and refuse
  lifetime_budget combined with end_time=0 (a lifetime budget requires a
  schedule end).
- Route lifetime_budget through _validate_positive_money.
- StrategyPolicyGate: new max_lifetime_budget_per_campaign guardrail so
  lifetime-budget mutations cannot sidestep STRATEGY.md budget caps
  (fail-open contract preserved); documented in the strategy skill.

Closes #367
…sset retrievals (#366)

- list_demographic_criteria / list_audience_criteria on _TargetingMixin:
  GAQL reads over ad_group_criterion with whitelisted type filters,
  optional ad_group_id/campaign_id scoping via _validate_id, and an
  internal LIMIT 1000 cap for unscoped account-wide reads.
- list_image_assets on _MediaMixin: asset read with name, dimensions,
  mime type, file size, and serving URL; integer limit validated 1..1000.
- New MCP tools google_ads_demographic_targeting_list /
  google_ads_audience_targeting_list / google_ads_image_assets_list
  (tool count 83 -> 86).
- mappers.py: auto-generated criterion/asset enum maps + map_enum_name
  handling raw proto ints (use_proto_plus=False production path),
  'Enum.NAME' strings, and numeric strings, so real-API responses map
  to enum names instead of numeric codes.
- Docs: AGENTS.md tool table and _mureo-google-ads SKILL.md catalog.

Covers issue #366 tasks 1/2/4; task 3 (budget type get/set) ships
separately given the guardrail interaction.
…set (#366)

- get_budget now returns name, period (DAILY / CUSTOM_PERIOD),
  delivery_method, status (enum-mapped), total_budget /
  total_amount_micros (null unless CUSTOM_PERIOD), and reference_count.
- update_budget accepts total_amount / total_amount_micros alongside the
  daily amount, with a dynamic update mask. period is create-only — the
  API marks CampaignBudget.period immutable.
- create_budget accepts period (DAILY / CUSTOM_PERIOD whitelist) and
  total amounts; totals require CUSTOM_PERIOD and vice versa (fail fast).
- All amount paths (daily + total, create + update) route through the
  shared _resolve_amount_micros validator: positive, catastrophe ceiling,
  bool/inf rejection, pair mutual exclusion.
- StrategyPolicyGate: max_lifetime_budget_per_campaign now also catches
  Google total budgets in both spellings (total_amount, currency units;
  total_amount_micros, micros); the daily cap additionally reads
  amount_micros — closing parameter-form bypasses found in review.
- BYOD get_budget shape parity; budget tool schemas + handlers updated;
  _mureo-google-ads and _mureo-strategy skills documented.

Covers issue #366 task 3. Closes #366 together with #369.

Claude-Session: https://claude.ai/code/session_0195WBK6YopTveq9RAQE2xMz
…udget-period

# Conflicts:
#	mureo/_data/skills/_mureo-strategy/SKILL.md
#	mureo/google_ads/mappers.py
#	mureo/policy/strategy_gate.py
#	skills/_mureo-strategy/SKILL.md
#	tests/test_mcp_tools_google_ads.py
#	tests/test_strategy_gate.py
@hyoshi
hyoshi merged commit 0eff7f0 into main Jul 8, 2026
9 checks passed
@hyoshi
hyoshi deleted the feat/366-google-ads-budget-period branch July 8, 2026 11:53
@hyoshi hyoshi mentioned this pull request Jul 9, 2026
hyoshi added a commit that referenced this pull request Jul 9, 2026
Google Ads read-only demographic/audience criteria + image assets (#369),
budget type get/set (#371); Meta Ads ad-set end_time + lifetime_budget
(#368); Search Console cross-client isolation / site_url tenant allow-list
(#375, security); About Installed-packages consistency (#365); Meta budget
zero-decimal currency fix (#372).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Google Ads: add demographic/audience retrieval, total-budget type get/set, image-asset name retrieval

1 participant