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(projectaote-pms, automatic replication, labelledpurpose=apps-script-code-mgmt). Local plaintext copies shredded; the key is never in the repo or chat. - Created by
service@(owner-grade, from.env.localGOOGLE_*) β the standing agent GCP identity per T-148. gcloud active account restored tome@jefferochan.com.
Pending β owner actions (Workspace admin + Apps Script settings)¶
- Enable the Apps Script API for the account that owns the script: https://script.google.com/home/usersettings β turn ON "Google Apps Script API".
- 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):
- Client ID:
116845537588563115906 - OAuth scope (exactly one):
https://www.googleapis.com/auth/script.projects - Nothing else β no Gmail, no Drive.
- 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, scopescript.projectsonly) in thejefferochan.comadmin console; script owner-accountme@jefferochan.com, Script ID1SbpVxWGtkSTcvT5DfWTNLDeCdgMjP5v1tZC_hvdwTy5b-Eb40KBKF1bx. - Verified working: minted a delegated token (SA impersonating
me@jefferochan.com,script.projectsscope), read the project content, then pushed the repo's canonicalbilling-email-push.gsviaprojects.updateContentβ round-trip match true,resetStatenow 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/runOnceremains 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.gsin the owner's account, fetchAPPS_SCRIPT_MANAGER_KEYfrom Secret Manager andupdateContent(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.