Skip to content

Scoped agent access to the owner's Apps Script (code-management SA, no mail/Drive/run)

Why

The owner wants agents to be able to update the billing-email Apps Script's code (docs/apps-script/billing-email-push.gs) without hand-pasting it into script.google.com every time it changes β€” but only under access they can audit and scope. This came directly out of T-147 Phase 3: after the mailbox-pull credential was decommissioned for privacy, the only way to ship a script fix was copy-paste, and the owner asked whether an agent could do it instead.

The owner drew a precise line β€” a single master credential that everything shares and nobody can attribute is the thing they reject; a named, narrowly-scoped credential is fine. This task builds exactly that for one capability (Apps Script code), and deliberately stops short of anything that could read mail or run the script.

The boundary (deliberate)

Managing the script has two halves; only the first is granted here: - Update code β†’ granted to a scoped SA. Removes the copy-paste. - Run the script (runOnce) + its Gmail authorization β†’ stays the owner's click. We do NOT request the Gmail scope, so this credential can never read mail or run the script. The privacy model from T-147 (all mailbox capability lives in the owner's own account, gated by the owner's own consent) is preserved.

Honest scope caveat: the OAuth scope https://www.googleapis.com/auth/script.projects is code-management across the account's Apps Script projects β€” it can't be narrowed to a single script by scope alone, and it can't read mail/Drive or run anything. Far narrower than the old owner-grade service@ key, and every use is attributable to this one SA, but not literally one-script.

What's built (agent side β€” done)

  • SA: apps-script-manager@aote-pms.iam.gserviceaccount.com β€” created with zero IAM role bindings (verified). DWD needs no project roles.
  • client ID (uniqueId, for the delegation grant): 116845537588563115906
  • Key: in Secret Manager as APPS_SCRIPT_MANAGER_KEY (project aote-pms, automatic replication, labelled purpose=apps-script-code-mgmt). Local plaintext copies shredded; the key is never in the repo or chat.
  • Created by service@ (owner-grade, from .env.local GOOGLE_*) β€” the standing agent GCP identity per T-148. gcloud active account restored to me@jefferochan.com.

Pending β€” owner actions (Workspace admin + Apps Script settings)

  1. Enable the Apps Script API for the account that owns the script: https://script.google.com/home/usersettings β†’ turn ON "Google Apps Script API".
  2. Add the domain-wide-delegation grant in the admin console of the Workspace domain that owns the script (admin.google.com β†’ Security β†’ Access and data control β†’ API controls β†’ Domain-wide delegation β†’ Add new):
  3. Client ID: 116845537588563115906
  4. OAuth scope (exactly one): https://www.googleapis.com/auth/script.projects
  5. Nothing else β€” no Gmail, no Drive.
  6. Tell the agent: (a) the email of the Google account that owns the "EOP billing email push" project (the account you're signed into at script.google.com β€” this is the DWD subject the SA impersonates, and must be a Workspace account, not a personal @gmail.com), and (b) the Script ID (Project Settings β†’ IDs, or from the URL).

Then β€” agent finishes

Fetch APPS_SCRIPT_MANAGER_KEY, mint a JWT impersonating the owning account with scope script.projects, and push the repo's .gs via the Apps Script API projects.updateContent. Verify the pushed content round-trips (projects.getContent). From then on, a script change = edit the repo file β†’ agent pushes; the owner still runs it.

Blast radius

New standalone SA + one Secret Manager secret + (pending) one narrow DWD grant. No existing IAM changed, no code paths touched. Independent of the T-147 pipeline runtime β€” this only affects how the script source is delivered, not what it does.

2026-07-23 β€” Opened + agent-side built

  • Attestation: read AGENTS.md + scanned the board; new scope (no existing task covers "scoped SA for Apps Script code push"), so opened T-177 rather than fold into T-147.
  • Source: Service Account (Local) Β· local session e392d97a-fb85-4c88-b556-ec00580de1a8
  • Proposed by: Service Account (Local). Approved by: the owner.
  • Decisive owner words (verbatim, 2026-07-23): "I was against having one master access for everything and not know what agents have touched using that account, but I'm not against agents having access to my account when the access is properly scoped. Now help me with it" β€” and, choosing the mechanism from a 3-way prompt (scoped SA / clasp / keep manual), selected "Dedicated scoped SA".
  • Rationale: matches the owner's stated priority β€” a distinct, named identity limited to one narrow capability, so account activity is attributable. Consistent with the scoped-SA-per-workload model from T-148. clasp was declined because it acts as the owner's own identity (weak "which agent touched it" audit); manual was declined because the owner asked to remove the copy-paste.
  • Commits: (append-only) f6724d09 (open + agent-side build).

2026-07-23 β€” DONE: owner completed setup; first code-push verified end-to-end

  • Source: Service Account (Local) Β· local session e392d97a-fb85-4c88-b556-ec00580de1a8
  • Owner completed their side: Apps Script API enabled; DWD grant added (client 116845537588563115906, scope script.projects only) in the jefferochan.com admin console; script owner-account me@jefferochan.com, Script ID 1SbpVxWGtkSTcvT5DfWTNLDeCdgMjP5v1tZC_hvdwTy5b-Eb40KBKF1bx.
  • Verified working: minted a delegated token (SA impersonating me@jefferochan.com, script.projects scope), read the project content, then pushed the repo's canonical billing-email-push.gs via projects.updateContent β€” round-trip match true, resetState now present (project code went 5577 β†’ 7058 chars, i.e. old β†’ current with the Phase-3 fix). One SERVER_JS file + manifest; manifest preserved untouched.
  • Boundary held (the point of the design): the same credential could NOT read mail/Drive or run the script β€” no Gmail scope was ever requested. Running resetState/runOnce remains the owner's click.
  • Outcome vs plan: exactly as planned β€” the copy-paste is eliminated; future script changes = edit the repo .gs, agent pushes, owner runs. No IAM changed; new SA carries zero roles.
  • Blast radius for other agents: a new reusable capability exists β€” to update docs/apps-script/billing-email-push.gs in the owner's account, fetch APPS_SCRIPT_MANAGER_KEY from Secret Manager and updateContent (recipe in AGENTS.md). Do NOT broaden this SA's scope or repurpose it; new capability β†’ new scoped SA.
  • Commits: (append-only) f6724d09 Β· this close-out commit.