@topy-ai/maggie 0.7.6 → 0.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -60,6 +60,18 @@ Maggie keeps the existing project foundation and asks for decisions before
60
60
  shared routes, analytics, or publishing boundaries change. The current
61
61
  package ships 18 installable skills and a local-first MaggieDash foundation.
62
62
 
63
+ For Google integrations, validate a redacted provider matrix before reporting
64
+ access. The command fails closed on unknown scopes, missing Ads prerequisites,
65
+ duplicate provider resources, and unverified edit/publish claims:
66
+
67
+ ```bash
68
+ maggie ops google-capabilities --project . \
69
+ --report .maggie/google-capability-input.json
70
+ ```
71
+
72
+ Read the shipped [Google integrations runbook](https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/main/references/google-integrations-runbook.md)
73
+ for the exact scope allowlist and provider-specific preflight rules.
74
+
63
75
  For a genuinely empty project, create the host framework first, then bootstrap
64
76
  Maggie in this order:
65
77
 
@@ -206,8 +218,8 @@ artifact schemas.
206
218
  Recommended upgrade sequence for the current release:
207
219
 
208
220
  ```bash
209
- npx @topy-ai/maggie@0.7.6 update --project . --force
210
- npx @topy-ai/maggie@0.7.6 cleanup --project .
221
+ npx @topy-ai/maggie@0.7.8 update --project . --force
222
+ npx @topy-ai/maggie@0.7.8 cleanup --project .
211
223
  ```
212
224
 
213
225
  Maintainers should pass npm credentials through the repository helper, never
@@ -217,7 +229,8 @@ as a command-line argument:
217
229
  node scripts/publish-npm.mjs --maggie-env-file ../.env
218
230
  ```
219
231
 
220
- The 0.7.6 workflow adds the installable MaggieDash admin distribution and
232
+ The 0.7.8 workflow adds the shared Google integrations runbook and
233
+ fail-closed provider capability matrix, alongside the installable MaggieDash admin distribution and
221
234
  audited CMS operations (`cms revisions`,
222
235
  `trash`, `restore`, `schedule`, `publish-due`, `duplicate`, `redirect`, and
223
236
  signed `preview`). It also adds a read-only MaggieDash `--diff`/`--dry-run`
@@ -455,6 +468,7 @@ python3 tools/clis/maggie_design.py rebrand \
455
468
  | `maggie-feedback` | Collect redacted feedback drafts and explicitly submit them to the NoBlox feedback endpoint |
456
469
  | `maggie-auth-reference` | Generate and validate traditional email/password auth with secure server-side sessions |
457
470
  | `maggie-blog` | Run a provider-neutral blog lifecycle with stable identity, topics, feeds, settings, and rollback |
471
+ | `maggie-google-capabilities` | Validate redacted Google provider evidence and separate read/report/edit/publish capability states |
458
472
 
459
473
  `maggie-design` can initialize native blog and service UI plans from a local,
460
474
  read-only structural reference:
package/README.zh-TW.md CHANGED
@@ -8,7 +8,7 @@ Codex、Claude Code 與相容的 coding agents。
8
8
  ## 安裝
9
9
 
10
10
  ```bash
11
- npx @topy-ai/maggie@0.7.6 init --agent all
11
+ npx @topy-ai/maggie@0.7.8 init --agent all
12
12
  npx @topy-ai/maggie doctor --project .
13
13
  ```
14
14
 
package/bin/maggie.js CHANGED
@@ -70,7 +70,7 @@ Usage:
70
70
  maggie dash status --project PATH
71
71
  maggie dash migrate --project PATH --confirm
72
72
  maggie content FILE --source PROVIDER --project PATH --confirm
73
- maggie agent-content write --url URL --token-env ENV --payload FILE
73
+ maggie agent-content write --url URL --allowed-origin ORIGIN --token-env ENV --payload FILE
74
74
  maggie bootstrap phase <start|pass|fail|status> <phase> [project]
75
75
  maggie remove [SKILL ...] [--project PATH] [--agent codex|claude|all]
76
76
  maggie cleanup --project PATH [--confirm]
@@ -120,7 +120,8 @@ Usage:
120
120
  maggie site-audit URL --crawl --baseline FILE
121
121
  maggie browser-audit URL --browse PATH --output DIR --required SELECTOR [--sticky SELECTOR]
122
122
  maggie verification coverage --contract FILE --evidence FILE
123
- maggie ops audit|preflight|verify|lockfiles|seed-manifest --project PATH
123
+ maggie ops audit|preflight|verify|lockfiles|seed-manifest|google-capabilities --project PATH
124
+ maggie ops google-capabilities --project PATH --report FILE [--output FILE]
124
125
  maggie ops preflight --project PATH --write
125
126
 
126
127
  Examples:
@@ -0,0 +1,87 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://maggie.noblox.app/contracts/google-integrations/capability-report-v1.json",
4
+ "title": "Maggie Google integrations capability report",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "generatedAt", "providerResults", "passed", "mutationsAllowed"],
8
+ "properties": {
9
+ "schemaVersion": { "const": "maggie-google-capability-report.v1" },
10
+ "generatedAt": { "type": "string", "minLength": 1 },
11
+ "providerResults": {
12
+ "type": "array",
13
+ "minItems": 1,
14
+ "items": { "$ref": "#/$defs/providerResult" }
15
+ },
16
+ "passed": { "type": "boolean" },
17
+ "mutationsAllowed": { "const": false }
18
+ },
19
+ "$defs": {
20
+ "providerResult": {
21
+ "type": "object",
22
+ "additionalProperties": false,
23
+ "required": [
24
+ "provider", "resource", "authMode", "scopes", "productRole",
25
+ "evidence", "capabilities", "nextAction"
26
+ ],
27
+ "properties": {
28
+ "provider": { "enum": ["gsc", "ga4", "gtm", "google-ads", "firebase"] },
29
+ "resource": { "type": "string", "minLength": 1, "maxLength": 200 },
30
+ "authMode": { "enum": ["desktop-oauth", "service-account-impersonation", "none"] },
31
+ "scopes": {
32
+ "type": "array",
33
+ "items": { "type": "string", "minLength": 1 },
34
+ "uniqueItems": true
35
+ },
36
+ "productRole": { "type": "string", "minLength": 1, "maxLength": 200 },
37
+ "evidence": { "$ref": "#/$defs/evidence" },
38
+ "capabilities": { "$ref": "#/$defs/capabilities" },
39
+ "nextAction": { "type": "string", "minLength": 1, "maxLength": 500 }
40
+ }
41
+ },
42
+ "evidence": {
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "required": ["verified", "endpoint", "statusCode", "readOnly"],
46
+ "properties": {
47
+ "verified": { "type": "boolean" },
48
+ "endpoint": { "type": "string", "minLength": 1, "maxLength": 500 },
49
+ "statusCode": { "type": "integer", "minimum": 100, "maximum": 599 },
50
+ "readOnly": { "type": "boolean" },
51
+ "writeTest": { "$ref": "#/$defs/writeTest" },
52
+ "adsPrerequisites": { "$ref": "#/$defs/adsPrerequisites" }
53
+ }
54
+ },
55
+ "writeTest": {
56
+ "type": "object",
57
+ "additionalProperties": false,
58
+ "required": ["explicitConfirmation", "statusCode", "readBack"],
59
+ "properties": {
60
+ "explicitConfirmation": { "const": true },
61
+ "statusCode": { "type": "integer", "minimum": 200, "maximum": 299 },
62
+ "readBack": { "const": true }
63
+ }
64
+ },
65
+ "adsPrerequisites": {
66
+ "type": "object",
67
+ "additionalProperties": false,
68
+ "required": ["developerTokenPresent", "customerIdPresent"],
69
+ "properties": {
70
+ "developerTokenPresent": { "type": "boolean" },
71
+ "customerIdPresent": { "type": "boolean" },
72
+ "loginCustomerIdPresent": { "type": "boolean" }
73
+ }
74
+ },
75
+ "capabilities": {
76
+ "type": "object",
77
+ "additionalProperties": false,
78
+ "required": ["read", "report", "edit", "publish"],
79
+ "properties": {
80
+ "read": { "enum": ["verified", "not_tested", "not_available", "blocked"] },
81
+ "report": { "enum": ["verified", "not_tested", "not_available", "blocked"] },
82
+ "edit": { "enum": ["verified", "not_tested", "not_available", "blocked"] },
83
+ "publish": { "enum": ["verified", "not_tested", "not_available", "blocked"] }
84
+ }
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,133 @@
1
+ # Google integrations runbook
2
+
3
+ This is the shared authorization and capability contract for Google Search
4
+ Console (GSC), Google Analytics 4 (GA4), Google Tag Manager (GTM), Google Ads,
5
+ and Firebase. Analytics, Ops, SEO/GEO, deployment, and project-context skills
6
+ must link here instead of inventing provider-specific OAuth guidance.
7
+
8
+ The runbook is deliberately read-first and fail-closed. A successful account
9
+ listing is not proof of reporting access, editing access, or publishing access.
10
+ Never print, persist, or put an access token, refresh token, private key, or
11
+ credential file in a prompt, `.env`, screenshot, feedback payload, or report.
12
+
13
+ ## Choose the authentication mode
14
+
15
+ | Mode | Use when | Boundary |
16
+ |---|---|---|
17
+ | Desktop OAuth | A person is interactively connecting a property or account | Request only the exact scopes needed for the selected read/report task; keep the resulting credential in the local credential store. |
18
+ | Service-account impersonation | A scheduled or server-side workflow needs repeatable access | The caller must have `roles/iam.serviceAccountTokenCreator` on the target service account; the target service account receives product-level access separately. |
19
+ | None | The user has not authorized a provider | Report `blocked` or `not_tested`; do not guess from project IDs or account listings. |
20
+
21
+ For ADC-based local checks, use a short-lived token only in the command
22
+ pipeline. When a non-Cloud API scope is needed, validate the scope first and
23
+ use the supported ADC command shape:
24
+
25
+ ```bash
26
+ gcloud auth application-default print-access-token \
27
+ --impersonate-service-account=SERVICE_ACCOUNT \
28
+ --scopes=EXACT_SCOPE
29
+ ```
30
+
31
+ Do not use `gcloud auth print-access-token` as a substitute for an ADC flow
32
+ when the requested API scope matters. Do not add `--no-launch-browser` to an
33
+ ADC command unless the installed gcloud version explicitly supports that flag;
34
+ use `--no-browser` for the current ADC login flow when a headless device is
35
+ required.
36
+
37
+ ## Exact scope allowlist
38
+
39
+ Only the following scopes may appear in a Maggie capability report. A scope
40
+ that is not listed is rejected, including whitespace-split or guessed Tag
41
+ Manager scopes.
42
+
43
+ | Provider | Read/report | Edit | Publish or special access |
44
+ |---|---|---|---|
45
+ | GSC | `https://www.googleapis.com/auth/webmasters.readonly` | Not applicable to this read/report contract | Full read/write `https://www.googleapis.com/auth/webmasters` is allowed only for an explicitly confirmed mutation workflow. |
46
+ | GA4 | `https://www.googleapis.com/auth/analytics.readonly` | `https://www.googleapis.com/auth/analytics.edit` | Publishing is not an Analytics Admin capability. |
47
+ | GTM | `https://www.googleapis.com/auth/tagmanager.readonly` | `https://www.googleapis.com/auth/tagmanager.edit.containers` | `https://www.googleapis.com/auth/tagmanager.publish`; version management is `https://www.googleapis.com/auth/tagmanager.edit.containerversions`. |
48
+ | Google Ads | `https://www.googleapis.com/auth/adwords` | Same OAuth scope, plus product prerequisites | Same OAuth scope, plus product prerequisites and explicit mutation/read-back evidence. |
49
+ | Firebase | `https://www.googleapis.com/auth/cloud-platform` | Same scope, plus Firebase/Cloud IAM permissions | Publishing is not a Firebase Management capability in this contract. |
50
+
51
+ The validator accepts the complete provider allowlist, but a capability is
52
+ verified only when its corresponding required scope is present. Scope presence
53
+ alone never proves product access.
54
+
55
+ ## Provider setup and read-only preflight
56
+
57
+ 1. Select one provider and one resource. Enable only that provider API in the
58
+ Google Cloud project.
59
+ 2. Select Desktop OAuth for a human connection or service-account
60
+ impersonation for automation. Record the mode, exact scopes, resource, and
61
+ product role; never record credential values.
62
+ 3. Verify product-level access, not just Cloud IAM:
63
+ - **GSC:** the user/service principal can discover the property and run a
64
+ Search Analytics query.
65
+ - **GA4:** the principal can discover the account/property and run a Data
66
+ API report; Admin API discovery and Data API reporting are separate
67
+ checks.
68
+ - **GTM:** the principal can list the target account/container. Treat edit
69
+ and publish as separate, untested capabilities until deliberately tested.
70
+ - **Google Ads:** verify the customer account and include the developer
71
+ token and customer ID prerequisites. A manager-account flow also needs a
72
+ `login-customer-id`; remove hyphens from customer IDs in API headers.
73
+ - **Firebase:** verify the Cloud project and Firebase resource with the
74
+ required IAM permissions. Firebase access is not implied by GA4 access.
75
+ 4. Use an HTTPS provider endpoint and capture only status code, endpoint, and
76
+ redacted result metadata. A read/report check must be read-only and return
77
+ a successful 2xx status.
78
+ 5. Generate a capability report and validate it before an agent claims access:
79
+
80
+ ```bash
81
+ maggie ops google-capabilities \
82
+ --project . \
83
+ --report .maggie/google-capability-input.json \
84
+ --output .maggie/google-capability-report.json
85
+ ```
86
+
87
+ The report contract is
88
+ [`capability-report-v1.json`](../bundled-contracts/google-integrations/capability-report-v1.json).
89
+ The CLI writes a normalized result and never copies arbitrary input fields.
90
+
91
+ ## Capability matrix
92
+
93
+ Every provider/resource row must expose this shape:
94
+
95
+ | Field | Meaning |
96
+ |---|---|
97
+ | `provider` / `resource` | The Google product and scoped resource being checked |
98
+ | `authMode` | `desktop-oauth`, `service-account-impersonation`, or `none` |
99
+ | `scopes` | Exact allowlisted OAuth scopes, never token values |
100
+ | `productRole` | Role granted in the product, distinct from Cloud IAM |
101
+ | `evidence` | Redacted HTTPS endpoint, HTTP status, verification state, and read-only state |
102
+ | `capabilities.read` | Property/account/resource discovery status |
103
+ | `capabilities.report` | Reporting/data-read status |
104
+ | `capabilities.edit` | Edit status; `verified` requires explicit mutation and read-back evidence |
105
+ | `capabilities.publish` | Publish status; `verified` requires explicit mutation and read-back evidence |
106
+ | `nextAction` | One concrete missing prerequisite or safe follow-up |
107
+
108
+ Use exactly one of `verified`, `not_tested`, `not_available`, or `blocked` for
109
+ each capability. A report should normally say `not_tested` for GTM edit/publish
110
+ and Ads mutations when only read-only checks were authorized. Never infer a
111
+ write capability from a successful list request.
112
+
113
+ ## Mutation boundary
114
+
115
+ The shared validator does not execute provider mutations. A provider adapter
116
+ may test an edit or publish operation only after the user explicitly confirms
117
+ the exact resource and operation. It must capture a successful status and
118
+ read-back evidence, then set the capability to `verified`. Otherwise use
119
+ `not_tested` or `blocked`.
120
+
121
+ Google Ads always remains a separate capability check because OAuth scope,
122
+ product account access, developer token, customer ID, and optional manager
123
+ headers are distinct prerequisites. Firebase similarly requires product/IAM
124
+ permissions; a Cloud project or service account alone is insufficient.
125
+
126
+ ## Authoritative references
127
+
128
+ - [Search Console API authorization](https://developers.google.com/webmaster-tools/v1/how-tos/authorizing)
129
+ - [Google Analytics Admin API scopes](https://developers.google.com/analytics/devguides/config/admin/v1/rpc/google.analytics.admin.v1beta)
130
+ - [Tag Manager API authorization](https://developers.google.com/tag-platform/tag-manager/api/v2/authorization)
131
+ - [Google Ads authorization and headers](https://developers.google.com/google-ads/api/rest/auth)
132
+ - [Firebase IAM permissions](https://firebase.google.com/docs/projects/iam/permissions)
133
+ - [ADC access-token command](https://cloud.google.com/sdk/gcloud/reference/auth/application-default/print-access-token)
@@ -13,6 +13,7 @@ Skills are the agent-facing workflows. They compose with the tools in
13
13
  | `maggie-template` | Discover, recommend, inspect, and apply a selected Astro homepage template | marketplace catalog, bootstrap state, template `DESIGN.md` |
14
14
  | `maggie-design` | Fully clone authorized interior pages, then reconcile them to the homepage header/footer shell | browser MCP, clone planner CLI, completed Maggie homepage |
15
15
  | `maggie-ops` | Build, connect, operate, upgrade, and verify the private blog operations dashboard | Ops dashboard contract, authenticated API bridge, host tests |
16
+ | `maggie-google-capabilities` | Validate a redacted Google provider capability matrix without inferring write access | Google integrations runbook, provider evidence |
16
17
  | `maggie-deployment` | Deploy and verify a dynamic Maggie blog, Cloudflare-first | Cloudflare Workers, D1, R2, KV, Wrangler |
17
18
  | `maggie-project-context` | Sync Project, voice, site and CTA context | project-context CLI/API |
18
19
  | `maggie-seo-geo` | Plan, audit, create/rewrite and measure SEO/GEO | visibility, SEO audit, GSC/GA4, content quality |
@@ -57,6 +57,12 @@ This command never deploys. Production execution still requires the shared
57
57
  decision loop, explicit confirmation, migration/rollback details, and the
58
58
  provider-specific deploy command.
59
59
 
60
+ When deployment verification depends on Google properties or accounts, use
61
+ the shared [Google integrations runbook](../../references/google-integrations-runbook.md).
62
+ Keep provider auth and capability evidence separate from deployment health;
63
+ never infer Ads, GTM, GSC, GA4, or Firebase write access from a successful
64
+ read-only check.
65
+
60
66
  Generate a reviewable VPS plan and configuration artifacts locally:
61
67
 
62
68
  ```bash
@@ -2,7 +2,7 @@
2
2
  name: maggie-ops
3
3
  description: Build, connect, operate, upgrade, and verify a private Maggie Ops dashboard for blog content, API Pull, sitemap matching, rewrite approvals, reports, and integrations. Use when the user asks for Ops/admin functionality or lifecycle operations rather than public blog pages.
4
4
  metadata:
5
- version: 1.0.0
5
+ version: 1.1.0
6
6
  ---
7
7
 
8
8
  # Maggie Ops
@@ -56,6 +56,22 @@ If the user does not name a mode, inspect the project and propose the smallest
56
56
  mode that satisfies the request. Do not rebuild the public blog or change its
57
57
  framework just to add Ops.
58
58
 
59
+ ## Google integrations
60
+
61
+ Use the shared [Google integrations runbook](../../references/google-integrations-runbook.md)
62
+ for GSC, GA4, GTM, Google Ads, and Firebase authorization. Do not invent
63
+ scopes or treat an account listing as proof of reporting, edit, or publish
64
+ access. After a redacted provider preflight, validate the capability matrix:
65
+
66
+ ```bash
67
+ maggie ops google-capabilities --project . \
68
+ --report .maggie/google-capability-input.json
69
+ ```
70
+
71
+ The result separates `read`, `report`, `edit`, and `publish` for each
72
+ provider/resource. Write and publish remain `not_tested` until the operator
73
+ explicitly confirms the exact mutation and a successful read-back is captured.
74
+
59
75
  ## Required preflight
60
76
 
61
77
  1. Inspect `.maggie/analysis.json` and `.maggie/bootstrap-state.json`.
@@ -53,6 +53,11 @@ Do not export team, uploads, testimonials, market research, internal workflow
53
53
  state, API keys, or OAuth tokens into public source-of-truth files. Missing or
54
54
  stale context blocks automatic publishing and falls back to a preview.
55
55
 
56
+ For Google provider connections used by context or reporting workflows, follow
57
+ the shared [Google integrations runbook](../../references/google-integrations-runbook.md)
58
+ and keep the normalized capability report separate from generated public
59
+ context. Product roles, OAuth scopes, and Cloud IAM are distinct evidence.
60
+
56
61
  For the combined API Pull lifecycle, use the repository CLI in dry-run first:
57
62
 
58
63
  ```bash
@@ -112,6 +112,12 @@ Follow [Maggie Memory Hook](../../references/memory-hook.md) at invocation and c
112
112
 
113
113
  For state-changing audits, rewrites, or publishing, follow the shared [Maggie decision loop](../../references/decision-loop.md) and confirm policy and scope before external writes.
114
114
 
115
+ For GSC/GA4 verification or any Google provider preflight, use the shared
116
+ [Google integrations runbook](../../references/google-integrations-runbook.md).
117
+ Report the verified property, endpoint, date window, and capability state;
118
+ account discovery alone is not evidence of Search Analytics or GA4 reporting
119
+ access, and read access never implies edit or publish access.
120
+
115
121
  Treat SEO and GEO as one measurable content system: people-first content,
116
122
  technical accessibility, extractable structure, evidence, authority, brand
117
123
  voice, CTA alignment, and observed search/AI outcomes.
@@ -14,6 +14,7 @@ from dependency_lock import audit as audit_lockfiles
14
14
  from seed_evidence import validate_manifest
15
15
  from agent_runtime import preflight as agent_preflight
16
16
  from restart_gate import validate as validate_restart_gate
17
+ from google_capabilities import validate_report as validate_google_capability_report
17
18
 
18
19
 
19
20
  REQUIRED_ROUTES = (
@@ -165,6 +166,21 @@ def command_restart_gate(args: argparse.Namespace) -> int:
165
166
  return 0 if result["passed"] else 1
166
167
 
167
168
 
169
+ def command_google_capabilities(args: argparse.Namespace) -> int:
170
+ report = read_json(Path(args.report).resolve())
171
+ result = validate_google_capability_report(report)
172
+ output = Path(args.output).resolve() if args.output else root(args) / ".maggie" / "google-capability-report.json"
173
+ write_json(output, result)
174
+ print(json.dumps({
175
+ "status": "passed" if result["passed"] else "failed",
176
+ "report": str(output),
177
+ "providerCount": len(result["providerResults"]),
178
+ "failedChecks": result["errors"],
179
+ "mutationsAllowed": False,
180
+ }, indent=2, ensure_ascii=False))
181
+ return 0 if result["passed"] else 1
182
+
183
+
168
184
  def main() -> int:
169
185
  parser = argparse.ArgumentParser(description=__doc__)
170
186
  parser.add_argument("--project", default=".")
@@ -183,6 +199,10 @@ def main() -> int:
183
199
  restart = sub.add_parser("restart-gate", help="validate restart-after-out-of-band-write evidence")
184
200
  restart.add_argument("--manifest", required=True)
185
201
  restart.set_defaults(func=command_restart_gate)
202
+ google = sub.add_parser("google-capabilities", help="validate a redacted Google provider capability matrix")
203
+ google.add_argument("--report", required=True)
204
+ google.add_argument("--output")
205
+ google.set_defaults(func=command_google_capabilities)
186
206
  record = sub.add_parser("record", help="record an explicitly authorised operation")
187
207
  record.add_argument("operation", choices=("sync", "sitemap-match", "rewrite-queue", "rewrite-approve", "publish", "migration"))
188
208
  record.add_argument("--dry-run", action="store_true")
@@ -1,5 +1,10 @@
1
1
  # GA4 and Google Search Console
2
2
 
3
+ Use the shared [Google integrations runbook](../../references/google-integrations-runbook.md)
4
+ for authorization, exact scopes, product-level roles, and capability
5
+ verification. This guide covers analytics instrumentation; it does not replace
6
+ the provider authentication matrix.
7
+
3
8
  ## GA4
4
9
 
5
10
  Use the host framework's supported Google tag integration. The minimum
@@ -0,0 +1,249 @@
1
+ """Fail-closed validation for redacted Google provider capability reports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+ from urllib.parse import urlsplit
8
+
9
+
10
+ SCHEMA_VERSION = "maggie-google-capability-report.v1"
11
+ CAPABILITY_NAMES = ("read", "report", "edit", "publish")
12
+ CAPABILITY_STATES = {"verified", "not_tested", "not_available", "blocked"}
13
+ PROVIDERS = {"gsc", "ga4", "gtm", "google-ads", "firebase"}
14
+ AUTH_MODES = {"desktop-oauth", "service-account-impersonation", "none"}
15
+
16
+ SUPPORTED_SCOPES = {
17
+ "gsc": {
18
+ "https://www.googleapis.com/auth/webmasters.readonly",
19
+ "https://www.googleapis.com/auth/webmasters",
20
+ },
21
+ "ga4": {
22
+ "https://www.googleapis.com/auth/analytics.readonly",
23
+ "https://www.googleapis.com/auth/analytics.edit",
24
+ },
25
+ "gtm": {
26
+ "https://www.googleapis.com/auth/tagmanager.readonly",
27
+ "https://www.googleapis.com/auth/tagmanager.edit.containers",
28
+ "https://www.googleapis.com/auth/tagmanager.delete.containers",
29
+ "https://www.googleapis.com/auth/tagmanager.edit.containerversions",
30
+ "https://www.googleapis.com/auth/tagmanager.publish",
31
+ "https://www.googleapis.com/auth/tagmanager.manage.users",
32
+ "https://www.googleapis.com/auth/tagmanager.manage.accounts",
33
+ },
34
+ "google-ads": {"https://www.googleapis.com/auth/adwords"},
35
+ "firebase": {"https://www.googleapis.com/auth/cloud-platform"},
36
+ }
37
+
38
+ READ_SCOPES = {
39
+ "gsc": "https://www.googleapis.com/auth/webmasters.readonly",
40
+ "ga4": "https://www.googleapis.com/auth/analytics.readonly",
41
+ "gtm": "https://www.googleapis.com/auth/tagmanager.readonly",
42
+ "google-ads": "https://www.googleapis.com/auth/adwords",
43
+ "firebase": "https://www.googleapis.com/auth/cloud-platform",
44
+ }
45
+
46
+ EDIT_SCOPES = {
47
+ "gsc": None,
48
+ "ga4": "https://www.googleapis.com/auth/analytics.edit",
49
+ "gtm": "https://www.googleapis.com/auth/tagmanager.edit.containers",
50
+ "google-ads": "https://www.googleapis.com/auth/adwords",
51
+ "firebase": "https://www.googleapis.com/auth/cloud-platform",
52
+ }
53
+
54
+ PUBLISH_SCOPES = {
55
+ "gsc": None,
56
+ "ga4": None,
57
+ "gtm": "https://www.googleapis.com/auth/tagmanager.publish",
58
+ "google-ads": "https://www.googleapis.com/auth/adwords",
59
+ "firebase": None,
60
+ }
61
+
62
+ ALLOWED_ENDPOINT_HOSTS = {
63
+ "searchconsole.googleapis.com",
64
+ "analyticsadmin.googleapis.com",
65
+ "analyticsdata.googleapis.com",
66
+ "tagmanager.googleapis.com",
67
+ "googleads.googleapis.com",
68
+ "firebase.googleapis.com",
69
+ }
70
+
71
+ NOT_APPLICABLE = {
72
+ "gsc": {"edit", "publish"},
73
+ "ga4": {"publish"},
74
+ "firebase": {"publish"},
75
+ }
76
+
77
+
78
+ def _string(value: Any, field: str, errors: list[str], *, max_length: int = 500) -> bool:
79
+ if not isinstance(value, str) or not value.strip() or len(value) > max_length:
80
+ errors.append(f"{field} must be a non-empty bounded string")
81
+ return False
82
+ return True
83
+
84
+
85
+ def _endpoint(value: Any, field: str, errors: list[str]) -> bool:
86
+ if not _string(value, field, errors):
87
+ return False
88
+ parts = urlsplit(value)
89
+ if parts.scheme != "https" or not parts.hostname or parts.username or parts.password or parts.query or parts.fragment:
90
+ errors.append(f"{field} must be an HTTPS URL without credentials, query, or fragment")
91
+ return False
92
+ if parts.hostname.lower() not in ALLOWED_ENDPOINT_HOSTS:
93
+ errors.append(f"{field} host is not an approved Google API host")
94
+ return False
95
+ return True
96
+
97
+
98
+ def _write_test(value: Any, field: str, errors: list[str]) -> bool:
99
+ if not isinstance(value, dict):
100
+ errors.append(f"{field} is required for a verified write capability")
101
+ return False
102
+ allowed = {"explicitConfirmation", "statusCode", "readBack"}
103
+ unknown = sorted(set(value) - allowed)
104
+ if unknown:
105
+ errors.append(f"{field} contains unsupported fields")
106
+ valid = value.get("explicitConfirmation") is True and value.get("readBack") is True
107
+ if not valid:
108
+ errors.append(f"{field} requires explicitConfirmation and readBack")
109
+ if not isinstance(value.get("statusCode"), int) or not 200 <= value["statusCode"] <= 299:
110
+ errors.append(f"{field}.statusCode must be a successful HTTP status")
111
+ valid = False
112
+ return valid and not unknown
113
+
114
+
115
+ def _required_scope(provider: str, capability: str) -> str | None:
116
+ if capability == "read" or capability == "report":
117
+ return READ_SCOPES[provider]
118
+ if capability == "edit":
119
+ return EDIT_SCOPES[provider]
120
+ return PUBLISH_SCOPES[provider]
121
+
122
+
123
+ def validate_report(report: Any) -> dict[str, Any]:
124
+ """Return safe validation output; never returns arbitrary input fields."""
125
+ errors: list[str] = []
126
+ if not isinstance(report, dict):
127
+ return {"schemaVersion": "maggie-google-capability-report-result.v1", "passed": False, "errors": ["report must be an object"], "providerResults": []}
128
+ if report.get("schemaVersion") != SCHEMA_VERSION:
129
+ errors.append("schemaVersion must be maggie-google-capability-report.v1")
130
+ if not _string(report.get("generatedAt"), "generatedAt", errors):
131
+ pass
132
+ if report.get("mutationsAllowed") is not False:
133
+ errors.append("mutationsAllowed must be false")
134
+ rows = report.get("providerResults")
135
+ if not isinstance(rows, list) or not rows:
136
+ errors.append("providerResults must be a non-empty array")
137
+ rows = []
138
+
139
+ normalized: list[dict[str, Any]] = []
140
+ seen: set[tuple[str, str]] = set()
141
+ for index, row in enumerate(rows):
142
+ prefix = f"providerResults[{index}]"
143
+ if not isinstance(row, dict):
144
+ errors.append(f"{prefix} must be an object")
145
+ continue
146
+ required = {"provider", "resource", "authMode", "scopes", "productRole", "evidence", "capabilities", "nextAction"}
147
+ unknown = sorted(set(row) - required)
148
+ if unknown:
149
+ errors.append(f"{prefix} contains unsupported fields")
150
+ provider = row.get("provider")
151
+ resource = row.get("resource")
152
+ if provider not in PROVIDERS:
153
+ errors.append(f"{prefix}.provider is unsupported")
154
+ continue
155
+ if not _string(resource, f"{prefix}.resource", errors, max_length=200):
156
+ continue
157
+ key = (provider, resource)
158
+ if key in seen:
159
+ errors.append(f"{prefix} duplicates provider/resource")
160
+ seen.add(key)
161
+ if row.get("authMode") not in AUTH_MODES:
162
+ errors.append(f"{prefix}.authMode is unsupported")
163
+ scopes = row.get("scopes")
164
+ valid_scope_values = isinstance(scopes, list) and all(isinstance(scope, str) and bool(scope) and not re.search(r"\s", scope) for scope in scopes)
165
+ if not valid_scope_values or len(scopes) != len(set(scopes)):
166
+ errors.append(f"{prefix}.scopes must be a unique array of exact scope strings")
167
+ scopes = []
168
+ unknown_scopes = sorted(set(scopes) - SUPPORTED_SCOPES[provider])
169
+ if unknown_scopes:
170
+ errors.append(f"{prefix}.scopes contains an unsupported scope")
171
+ if not _string(row.get("productRole"), f"{prefix}.productRole", errors, max_length=200):
172
+ pass
173
+ if not _string(row.get("nextAction"), f"{prefix}.nextAction", errors):
174
+ pass
175
+ evidence = row.get("evidence")
176
+ if not isinstance(evidence, dict):
177
+ errors.append(f"{prefix}.evidence must be an object")
178
+ evidence = {}
179
+ else:
180
+ evidence_allowed = {"verified", "endpoint", "statusCode", "readOnly", "writeTest", "adsPrerequisites"}
181
+ if set(evidence) - evidence_allowed:
182
+ errors.append(f"{prefix}.evidence contains unsupported fields")
183
+ verified_evidence = evidence.get("verified") is True
184
+ if not _endpoint(evidence.get("endpoint"), f"{prefix}.evidence.endpoint", errors):
185
+ pass
186
+ status_code = evidence.get("statusCode")
187
+ if not isinstance(status_code, int) or not 100 <= status_code <= 599:
188
+ errors.append(f"{prefix}.evidence.statusCode must be an HTTP status")
189
+ status_code = 0
190
+ if not isinstance(evidence.get("readOnly"), bool):
191
+ errors.append(f"{prefix}.evidence.readOnly must be boolean")
192
+ capabilities = row.get("capabilities")
193
+ if not isinstance(capabilities, dict):
194
+ errors.append(f"{prefix}.capabilities must be an object")
195
+ capabilities = {}
196
+ else:
197
+ if set(capabilities) != set(CAPABILITY_NAMES):
198
+ errors.append(f"{prefix}.capabilities must contain read, report, edit, and publish only")
199
+ safe_capabilities: dict[str, str] = {}
200
+ for capability in CAPABILITY_NAMES:
201
+ state = capabilities.get(capability)
202
+ if state not in CAPABILITY_STATES:
203
+ errors.append(f"{prefix}.capabilities.{capability} is unsupported")
204
+ state = "blocked"
205
+ safe_capabilities[capability] = state
206
+ if capability in NOT_APPLICABLE.get(provider, set()) and state != "not_available":
207
+ errors.append(f"{prefix}.capabilities.{capability} must be not_available for {provider}")
208
+ if state == "verified":
209
+ needed = _required_scope(provider, capability)
210
+ if needed is None:
211
+ errors.append(f"{prefix}.capabilities.{capability} cannot be verified for {provider}")
212
+ elif needed not in scopes:
213
+ errors.append(f"{prefix}.capabilities.{capability} requires its exact provider scope")
214
+ if not verified_evidence or not 200 <= status_code <= 299:
215
+ errors.append(f"{prefix}.capabilities.{capability} requires verified 2xx evidence")
216
+ if capability in {"read", "report"} and evidence.get("readOnly") is not True:
217
+ errors.append(f"{prefix}.capabilities.{capability} requires readOnly evidence")
218
+ if capability in {"edit", "publish"} and not _write_test(evidence.get("writeTest"), f"{prefix}.evidence.writeTest", errors):
219
+ pass
220
+ prerequisites = evidence.get("adsPrerequisites")
221
+ if provider == "google-ads" and any(state == "verified" for state in safe_capabilities.values()):
222
+ if not isinstance(prerequisites, dict) or prerequisites.get("developerTokenPresent") is not True or prerequisites.get("customerIdPresent") is not True:
223
+ errors.append(f"{prefix}.evidence.adsPrerequisites requires developer token and customer ID presence")
224
+ elif prerequisites is not None:
225
+ errors.append(f"{prefix}.evidence.adsPrerequisites is only valid for google-ads")
226
+ normalized.append({
227
+ "provider": provider,
228
+ "resource": resource,
229
+ "authMode": row.get("authMode"),
230
+ "scopes": sorted(scopes),
231
+ "productRole": row.get("productRole"),
232
+ "evidence": {
233
+ "verified": verified_evidence,
234
+ "endpoint": evidence.get("endpoint"),
235
+ "statusCode": status_code,
236
+ "readOnly": evidence.get("readOnly"),
237
+ "writeTested": isinstance(evidence.get("writeTest"), dict),
238
+ },
239
+ "capabilities": safe_capabilities,
240
+ "nextAction": row.get("nextAction"),
241
+ })
242
+
243
+ return {
244
+ "schemaVersion": "maggie-google-capability-report-result.v1",
245
+ "passed": not errors,
246
+ "errors": sorted(set(errors)),
247
+ "providerResults": normalized,
248
+ "mutationsAllowed": False,
249
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topy-ai/maggie",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
4
4
  "description": "Install and manage Maggie Skills for AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,133 @@
1
+ # Google integrations runbook
2
+
3
+ This is the shared authorization and capability contract for Google Search
4
+ Console (GSC), Google Analytics 4 (GA4), Google Tag Manager (GTM), Google Ads,
5
+ and Firebase. Analytics, Ops, SEO/GEO, deployment, and project-context skills
6
+ must link here instead of inventing provider-specific OAuth guidance.
7
+
8
+ The runbook is deliberately read-first and fail-closed. A successful account
9
+ listing is not proof of reporting access, editing access, or publishing access.
10
+ Never print, persist, or put an access token, refresh token, private key, or
11
+ credential file in a prompt, `.env`, screenshot, feedback payload, or report.
12
+
13
+ ## Choose the authentication mode
14
+
15
+ | Mode | Use when | Boundary |
16
+ |---|---|---|
17
+ | Desktop OAuth | A person is interactively connecting a property or account | Request only the exact scopes needed for the selected read/report task; keep the resulting credential in the local credential store. |
18
+ | Service-account impersonation | A scheduled or server-side workflow needs repeatable access | The caller must have `roles/iam.serviceAccountTokenCreator` on the target service account; the target service account receives product-level access separately. |
19
+ | None | The user has not authorized a provider | Report `blocked` or `not_tested`; do not guess from project IDs or account listings. |
20
+
21
+ For ADC-based local checks, use a short-lived token only in the command
22
+ pipeline. When a non-Cloud API scope is needed, validate the scope first and
23
+ use the supported ADC command shape:
24
+
25
+ ```bash
26
+ gcloud auth application-default print-access-token \
27
+ --impersonate-service-account=SERVICE_ACCOUNT \
28
+ --scopes=EXACT_SCOPE
29
+ ```
30
+
31
+ Do not use `gcloud auth print-access-token` as a substitute for an ADC flow
32
+ when the requested API scope matters. Do not add `--no-launch-browser` to an
33
+ ADC command unless the installed gcloud version explicitly supports that flag;
34
+ use `--no-browser` for the current ADC login flow when a headless device is
35
+ required.
36
+
37
+ ## Exact scope allowlist
38
+
39
+ Only the following scopes may appear in a Maggie capability report. A scope
40
+ that is not listed is rejected, including whitespace-split or guessed Tag
41
+ Manager scopes.
42
+
43
+ | Provider | Read/report | Edit | Publish or special access |
44
+ |---|---|---|---|
45
+ | GSC | `https://www.googleapis.com/auth/webmasters.readonly` | Not applicable to this read/report contract | Full read/write `https://www.googleapis.com/auth/webmasters` is allowed only for an explicitly confirmed mutation workflow. |
46
+ | GA4 | `https://www.googleapis.com/auth/analytics.readonly` | `https://www.googleapis.com/auth/analytics.edit` | Publishing is not an Analytics Admin capability. |
47
+ | GTM | `https://www.googleapis.com/auth/tagmanager.readonly` | `https://www.googleapis.com/auth/tagmanager.edit.containers` | `https://www.googleapis.com/auth/tagmanager.publish`; version management is `https://www.googleapis.com/auth/tagmanager.edit.containerversions`. |
48
+ | Google Ads | `https://www.googleapis.com/auth/adwords` | Same OAuth scope, plus product prerequisites | Same OAuth scope, plus product prerequisites and explicit mutation/read-back evidence. |
49
+ | Firebase | `https://www.googleapis.com/auth/cloud-platform` | Same scope, plus Firebase/Cloud IAM permissions | Publishing is not a Firebase Management capability in this contract. |
50
+
51
+ The validator accepts the complete provider allowlist, but a capability is
52
+ verified only when its corresponding required scope is present. Scope presence
53
+ alone never proves product access.
54
+
55
+ ## Provider setup and read-only preflight
56
+
57
+ 1. Select one provider and one resource. Enable only that provider API in the
58
+ Google Cloud project.
59
+ 2. Select Desktop OAuth for a human connection or service-account
60
+ impersonation for automation. Record the mode, exact scopes, resource, and
61
+ product role; never record credential values.
62
+ 3. Verify product-level access, not just Cloud IAM:
63
+ - **GSC:** the user/service principal can discover the property and run a
64
+ Search Analytics query.
65
+ - **GA4:** the principal can discover the account/property and run a Data
66
+ API report; Admin API discovery and Data API reporting are separate
67
+ checks.
68
+ - **GTM:** the principal can list the target account/container. Treat edit
69
+ and publish as separate, untested capabilities until deliberately tested.
70
+ - **Google Ads:** verify the customer account and include the developer
71
+ token and customer ID prerequisites. A manager-account flow also needs a
72
+ `login-customer-id`; remove hyphens from customer IDs in API headers.
73
+ - **Firebase:** verify the Cloud project and Firebase resource with the
74
+ required IAM permissions. Firebase access is not implied by GA4 access.
75
+ 4. Use an HTTPS provider endpoint and capture only status code, endpoint, and
76
+ redacted result metadata. A read/report check must be read-only and return
77
+ a successful 2xx status.
78
+ 5. Generate a capability report and validate it before an agent claims access:
79
+
80
+ ```bash
81
+ maggie ops google-capabilities \
82
+ --project . \
83
+ --report .maggie/google-capability-input.json \
84
+ --output .maggie/google-capability-report.json
85
+ ```
86
+
87
+ The report contract is
88
+ [`capability-report-v1.json`](../bundled-contracts/google-integrations/capability-report-v1.json).
89
+ The CLI writes a normalized result and never copies arbitrary input fields.
90
+
91
+ ## Capability matrix
92
+
93
+ Every provider/resource row must expose this shape:
94
+
95
+ | Field | Meaning |
96
+ |---|---|
97
+ | `provider` / `resource` | The Google product and scoped resource being checked |
98
+ | `authMode` | `desktop-oauth`, `service-account-impersonation`, or `none` |
99
+ | `scopes` | Exact allowlisted OAuth scopes, never token values |
100
+ | `productRole` | Role granted in the product, distinct from Cloud IAM |
101
+ | `evidence` | Redacted HTTPS endpoint, HTTP status, verification state, and read-only state |
102
+ | `capabilities.read` | Property/account/resource discovery status |
103
+ | `capabilities.report` | Reporting/data-read status |
104
+ | `capabilities.edit` | Edit status; `verified` requires explicit mutation and read-back evidence |
105
+ | `capabilities.publish` | Publish status; `verified` requires explicit mutation and read-back evidence |
106
+ | `nextAction` | One concrete missing prerequisite or safe follow-up |
107
+
108
+ Use exactly one of `verified`, `not_tested`, `not_available`, or `blocked` for
109
+ each capability. A report should normally say `not_tested` for GTM edit/publish
110
+ and Ads mutations when only read-only checks were authorized. Never infer a
111
+ write capability from a successful list request.
112
+
113
+ ## Mutation boundary
114
+
115
+ The shared validator does not execute provider mutations. A provider adapter
116
+ may test an edit or publish operation only after the user explicitly confirms
117
+ the exact resource and operation. It must capture a successful status and
118
+ read-back evidence, then set the capability to `verified`. Otherwise use
119
+ `not_tested` or `blocked`.
120
+
121
+ Google Ads always remains a separate capability check because OAuth scope,
122
+ product account access, developer token, customer ID, and optional manager
123
+ headers are distinct prerequisites. Firebase similarly requires product/IAM
124
+ permissions; a Cloud project or service account alone is insufficient.
125
+
126
+ ## Authoritative references
127
+
128
+ - [Search Console API authorization](https://developers.google.com/webmaster-tools/v1/how-tos/authorizing)
129
+ - [Google Analytics Admin API scopes](https://developers.google.com/analytics/devguides/config/admin/v1/rpc/google.analytics.admin.v1beta)
130
+ - [Tag Manager API authorization](https://developers.google.com/tag-platform/tag-manager/api/v2/authorization)
131
+ - [Google Ads authorization and headers](https://developers.google.com/google-ads/api/rest/auth)
132
+ - [Firebase IAM permissions](https://firebase.google.com/docs/projects/iam/permissions)
133
+ - [ADC access-token command](https://cloud.google.com/sdk/gcloud/reference/auth/application-default/print-access-token)