Status: planned · Scope: desktop onboarding "connect your context" import.
Ponytail note: this plan is deliberately reuse-first. It builds almost nothing new —
it wires the desktop to OAuth infra the backend already ships. Sections marked
ponytail: are corners cut on purpose, with the ceiling named.
Onboarding imports Gmail + Calendar by scraping browser cookies: it reads the
Chromium "Safe Storage" key from the macOS Keychain and decrypts the cookie DB
(BrowserGoogleSession + GmailReaderService / CalendarReaderService).
That path cannot be made silent or durable:
- macOS mandates a Keychain prompt to read another app's item; no entitlement removes the first prompt.
- It drifts every macOS version (Tahoe broke
/usr/bin/securityreads) and every browser version (cookie formats v10→v20 app-bound, keys migrating to iCloud Keychain).
Google OAuth is the only path that is silent, automatic, and version-proof.
The fetch layer is cleanly separable. Only these two functions change:
GmailReaderService.readRecentEmails() -> [GmailEmail]CalendarReaderService.readEvents() -> [CalendarEvent]
Everything downstream is source-agnostic and stays byte-for-byte:
readRecentEmails()/readEvents() ← REPLACE (cookies → OAuth)
↓ [GmailEmail]/[CalendarEvent]
synthesizeFrom*() (LLM) ← unchanged
↓ ImportEvidenceBatchItem
OnboardingImportEvidenceService.save()
↓ POST /v3/memory-imports/batch ← unchanged
ImportEvidenceBatchItem is the contract; keep producing it and nothing else moves.
| Piece | Where | Reuse |
|---|---|---|
| Google OAuth flow (PKCE, callback) | backend/routers/auth.py (scopes: openid email profile) |
extend |
| Google token refresh + retry | backend/utils/retrieval/tools/google_utils.py |
copy as-is |
| OAuth callback + Firestore token storage | backend/routers/integrations.py → users/{uid}/integrations/{app_key} |
clone |
| Live Google Calendar OAuth integration | backend/routers/integrations.py (google-calendar) |
wire desktop to it |
| Full OAuth+sync connector template | backend/utils/x_connector.py |
mirror for Gmail |
| Onboarding state machine | backend/routers/calendar_onboarding.py |
copy → gmail |
| Desktop loopback OAuth callback | desktop/macos/.../AuthService.swift |
reuse |
| Gmail API client + Gmail scope | — | build (only real new work) |
- Backend-mediated. Desktop calls the backend; the backend holds the token and
calls Google. Keeps the client secret server-side, centralizes refresh via
google_utils, and makes mobile/web free later. - Separate Google connector, not incremental-auth-on-sign-in. Users who signed in with Apple have no Google token; a standalone connector works regardless of sign-in method (and matches how Calendar already works).
- Gmail scope:
gmail.readonly(matches today's subject+snippet fidelity).⚠️ This is a Google restricted scope → heightened verification + a likely annual third-party security assessment (CASA). Budget it. Calendar's scope is only "sensitive" and is already verified/live — which is why Calendar goes first.
The backend Google Calendar integration already exists AND already reads events from
the Google API: backend/utils/retrieval/tools/calendar_tools.py:290 calls
googleapis.com/calendar/v3/.../events with the stored OAuth token (refreshed via
google_utils). So Phase 0 is thin wiring, not new OAuth:
- Backend: expose a read endpoint (e.g.
GET /v1/calendar/events?days_back&days_forward) as a thin wrapper over the existingcalendar_toolslist-events call. Hermetic test mocks the Google layer and asserts the response maps to the desktopCalendarEvent. - Desktop:
CalendarReaderService.readEvents()→ call that endpoint (with the user's Firebase session) instead of scraping cookies. - Trigger the existing
/v1/integrations/google-calendarOAuth-url flow from the onboarding step when not yet connected; reuseAuthServiceloopback callback. - Reuse
calendar_onboarding.pystatus/skip/reset as-is. - Result: Calendar import needs no Keychain prompt. Days, not weeks.
- Add
gmail.readonlyto a Google connector (clone the X/Calendar callback +google_utilsrefresh; store underusers/{uid}/integrations/gmail). - New Gmail API client + endpoint: list/get messages via
gmail.googleapis.com/gmail/v1/users/me/messages, refresh-on-401. - New
gmail_onboarding.py(copycalendar_onboarding.py). - Compliance gate: restricted-scope verification / CASA. Start early; it's the long pole, not the code.
- Put both readers behind a
useOAuthflag at the seam; on failure fall back to the (now-fixed) cookie path during rollout.
- Free once backend-mediated. ponytail: not designed here until Phase 2 lands.
- No new token-encryption scheme. Reuse the existing
integrations/{app_key}storage (plaintext at app layer, GCP-encrypted at rest), same as Calendar/X. Ceiling: if integration-token sensitivity policy changes, migrate all connectors together, not just Gmail. - No MCP-style multi-client OAuth tables (
mcp_oauth.py). YAGNI — one grant per user per provider viaintegrations/{app_key}. Ceiling: only needed if third-party clients must hold Gmail grants. - No incremental-auth-on-sign-in. Standalone connector instead (works for Apple sign-in users).
- Do not rip out the cookie path. It stays as the Phase 2 fallback and covers any surface OAuth hasn't reached. The two committed cookie fixes (in-process Keychain read + unknown-version skip) remain.
- Confirm the exact current Google restricted-scope verification requirements before committing Gmail work (policy shifts; verify against live Google docs).
- Map
calendar_toolslist-events output → desktopCalendarEvent(fields align; confirm all-day + attendee shapes).
These gate a working, verified migration and are outside code:
- Google OAuth client credentials (
GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET) in a runnable/deployed backend — needed to exercise any live OAuth flow. - A Google-connected test account to verify the Calendar/Gmail read path end to end.
- Google Cloud consent-screen scope config: adding
gmail.readonly(restricted) and the Calendar scope to the app's OAuth consent screen. - Gmail restricted-scope verification / CASA — a Google-side review measured in weeks.
Implication: backend/desktop code can be written with hermetic (mocked-Google) tests, but the live OAuth path cannot be exercised until the above are in place. Do not mark the feature "done" on hermetic tests alone.