Skip to content

feat(gtm): opt-in write tools — tags, triggers, delete, publish (replaces #35) - #69

Open
illia-sapryga wants to merge 5 commits into
kLOsk:mainfrom
illia-sapryga:feat/gtm-write-v2
Open

illia-sapryga wants to merge 5 commits into
kLOsk:mainfrom
illia-sapryga:feat/gtm-write-v2

Conversation

@illia-sapryga

Copy link
Copy Markdown
Contributor

Replaces #35. Rebuilt from fresh commits on current main (on top of the #29 read layer you brought up), so the diff is only the write surface: no old history, no business data in fixtures.

Your July blockers → what changed

# Blocker Resolution
1 _apply_* handlers called load_config() Handlers take the config confirm_and_apply passes. GTM plans dispatch from _execute_plan via a gtm_ prefix, like Reddit, before the Ads client is built.
2 Custom HTML + publish = arbitrary JS to production, guarded only by warnings New gtm.allow_custom_html gate, off by default, enforced not advisory. It refuses to create or edit an html tag, and refuses to publish a workspace that adds or changes one, including HTML someone added by hand in the GTM UI. It's re-checked at dry run and at apply, so flipping the flag off after drafting still blocks. Pausing or deleting an HTML tag is always allowed.
3 Write scopes in _ALL_SCOPES The edit and publish scopes are requested only when gtm.write_enabled: true (default off) via _requested_scopes(config). Enabling it on an existing token triggers the existing stale-scope re-consent. There's a separate gtm_write_credentials provider hook, so Cloud gets a capability error (not write tokens) until you implement it.
— Apply handlers had zero test coverage New tests/test_gtm_write.py (65 tests) runs every apply path end to end against an in-memory Tag Manager fake. No source-grep dispatch tests.
A1 Stale _config All four wrappers use current_config(). The static test passes.
A2 Scope-migration UX Missing-scope 403s during apply map to a "set write_enabled, delete token.json, re-consent, and check container Edit/Publish permission" message. The README and rules document the setup.
A3 Test fixture used _pending_plans set_plan_store(InMemoryPlanStore())
A4 List params _DictListOpt / _StrListOpt, with a test that goes through mcp.call_tool using JSON-string lists (#28).
A5 Tag update wiped priority, consentSettings, … Updates are a read-modify-write that starts from the live resource, so every field the tool doesn't manage round-trips. Tests assert on priority, consentSettings, tagFiringOption, schedule*, parentFolderId, monitoringMetadata and setupTag, and fail if the body is rebuilt from a field list. On update, parameters merge by key.
A6 Long-form tag aliases; awcr vs sp Aliases dropped. Only canonical template IDs are accepted (sp for Ads remarketing, matching read.py), plus cvt_<id> for Gallery templates. The trigger types had the same class of bug (dom_ready, scroll_depth, … aren't API values). They now use the API's camelCase enum.

Other safety additions

  • No stale writes. Updates and deletes pin the draft-time fingerprint, and apply passes it to GTM and refuses if it moved. Publish pins a digest of the workspace's pending changes, so if anyone edits the workspace between preview and apply, nothing is published.
  • The publish preview lists every pending change, including other people's UI edits, since the user is approving all of them. Publishing refuses on merge conflicts or compilerError. compilerError is a boolean on CreateContainerVersionResponse; the old code treated it as a list.
  • The dry run runs a GTM preflight (Tag Manager has no validate-only mode). It reuses the existing Reddit branch in confirm_and_apply, generalised by platform label.
  • The workspace is resolved at draft time and pinned in the plan, so the preview names the exact workspace. If there are several workspaces and none is called "Default Workspace", it asks for an explicit workspace_id instead of guessing.
  • Deleting a trigger that a tag still references is refused up front, with the referencing tags listed.
  • Per-op names for blocked_operations: gtm_create_tag, gtm_update_tag, gtm_delete_tag, gtm_create_trigger, gtm_update_trigger, gtm_delete_trigger, gtm_publish_workspace.

Footprint

Four tools instead of the seven in #35. Create and update share one tool per entity, and the two deletes are one tool. All four are tagged gtm, and they refuse with setup guidance until write_enabled is set. I updated the gtm description in TOOLSETS to mention writes, and the slug list is unchanged. Flagging it since the dashboard pins that contract.

791 tests passing locally.

Tag Manager edit + publish scopes are requested only when the user sets
gtm.write_enabled, so audit-only users never grant publish authority.
Enabling it on an existing read-only token triggers the usual re-consent
(stored scopes no longer cover the requested set).

Adds gtm.allow_custom_html (enforced by the write module), a separate
gtm_write_credentials provider hook so hosted providers get a capability
error until they opt in, and strict parsing for both flags so a quoted
"false" can't switch a safety gate on.
Four tools under the gtm toolset, all draft -> preview -> confirm_and_apply:
draft_gtm_tag / draft_gtm_trigger (create, or update by id),
draft_delete_gtm_entity, draft_publish_gtm_workspace.

- Apply handlers take the config confirm_and_apply passes (no load_config)
- Updates are read-modify-write from the live resource, so unmanaged
  fields (priority, consentSettings, tagFiringOption, schedule, folder,
  setupTag, ...) round-trip; parameters merge by key
- Updates/deletes pin the draft-time fingerprint; publish pins a digest
  of the workspace's pending changes — apply refuses if either moved
- Custom HTML create/edit, and publishing a workspace that adds/changes
  one, is refused unless gtm.allow_custom_html; pause/delete always allowed
- Canonical template IDs only (sp for Ads remarketing, cvt_ for gallery);
  trigger types use the API's camelCase enum
- Publish refuses on merge conflicts and on compilerError (a boolean)
- Dry run runs a GTM preflight (Tag Manager has no validate-only mode)
- Missing-scope 403s map to an actionable re-consent message
- List params use the JSON-string-tolerant aliases (kLOsk#28)
Covers the opt-in and scope handling, template/trigger validation, the
Custom HTML gate (draft, apply-time recheck, UI-made HTML in publish),
field preservation on update, fingerprint and workspace-digest refusal,
compiler-error publish, dry-run preflight, audit logging, and JSON-string
list coercion through the MCP layer.
destructive_hint only exists on newer SDKs; destructiveHint works on both
(matching test_reddit_write.py).

This branch has not been deployed

No deployments
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.

1 participant