ADR-0002: The profile email is persisted identity metadata — outside the lock, outside gws¶
Status: Accepted Date: 2026-07-24 Decision makers: Thomas (PO)
TL;DR — The email address of each connected Google account is now remembered in a small text file written at connection time, instead of being fetched by re-running the Google tool on every read — so a "locked" account (whose data access is blocked until a human reopens it) can still display its email for setup diagnostics, without ever running the Google tool on its credentials while it is locked.
Context¶
A profile's lock (.locked) means "data access on demand":
every data call is refused until a human unlock. Yet setup_status
(backlog item 0009) needs the email of every account — locked ones included —
to check the IAM role and propose the exact gcloud add-iam-policy-binding
command (guided onboarding, ADR-0001).
The initial implementation fetched this email by running gws auth status
as a direct subprocess (GOOGLE_WORKSPACE_CLI_CONFIG_DIR=… gws …) from
gateway/profiles.py::_profile_email:
- Outside the broker — whose contract is "the only process that runs gws" on the gateway side; and precisely the pattern the threat model asks us to block in agents.
- On a locked profile — the branch ran only in that case
(
list_profilesonly reads the email when unlocked): running gws means touching the encrypted credentials during the lock. - Inconsistent —
profiles_listhid the email of locked profiles,setup_statusdisplayed it.
The product posture is already written down (SECURITY.md): "A locked profile
refuses all data access […] The only metadata that stays readable: the account
email, for diagnostics (setup_status)."
Decision¶
Own the exception — the email stays readable on a locked profile — but change its nature: it is persisted metadata, no longer the result of a gws execution.
- At connection time (
mag add, human action), the email is written in clear to<profile>/.email.mag listand the admin backfill profiles created before this decision, as soon as they read a non-empty email. - All surfaces (
profiles_list,setup_status, admin, mag) read this file. The gateway no longer launches any gws subprocess for this. - The invariant becomes sharp and testable: lock ⇒ zero gws executions with
the profile's credentials, and
gateway/contains noGOOGLE_WORKSPACE_CLI_CONFIG_DIRoutsidebroker_server.py(static test). - File missing (legacy profile): empty email, IAM "unknown", and
next_actionssuggestsmag list(the backfill is a human pass). Content that is not an email is ignored (no blind trust).
Options considered¶
Option A — Own the exception via the broker (derogation from the lock)¶
| Dimension | Assessment |
|---|---|
| Consistency | Broker as sole executor: yes — at the price of an "auth status even when locked" derogation |
| Security | gws still runs on a locked profile (credentials touched) |
| Complexity | Dedicated broker command + carve-out in require_unlocked |
Pros: centralizes and logs the exception. Cons: installs a derogation from the lock in the most sensitive component, for a piece of data that does not need to be recomputed on every read. Rejected.
Option B — Align setup_status with profiles_list (no email when locked)¶
| Dimension | Assessment |
|---|---|
| Consistency | Total, by leveling down |
| Security | The strictest |
| Onboarding UX | Broken in the recommended posture: locked profile ⇒ no more IAM check or remediation |
Pros: simple, nothing to write. Cons: contradicts the raison d'être of
setup_status (ADR-0001: give Desktop the visibility of
provision-gcp.sh status) and the sentence already published in SECURITY.md.
Rejected.
Option C — Persisted .email metadata (chosen)¶
| Dimension | Assessment |
|---|---|
| Consistency | All surfaces read the same source; broker is once again the sole executor |
| Security | Lock ⇒ zero gws executions; the email is captured at the human add action |
| Onboarding UX | Preserved (IAM + remediation even when locked) |
| Bonus | No more gws subprocess (15 s timeout) per profile in profiles_list |
Pros: delivers the result of A with a stronger invariant than B asked
for. Cons: one more file per profile + backfill of existing
profiles — accepted (the folder already contains client_secret.json in clear,
far more sensitive than an email).
Consequences¶
- Becomes simpler:
setup_status(no more special case), the doctrine ("the lock blocks all gws execution"), the hermetic tests (the email is tested without a gws binary). - Becomes more demanding:
.emailcan drift if someone runsgws auth loginby hand outsidemag(unsupported path) — themag add/mag listpasses re-synchronize it. - SECURITY.md stays true word for word; threat model updated (section "Email = identity metadata").