One connected Google account covers Gmail and Sheets: the assistant can read spreadsheets and send mail as the logged-in user. The assistant never holds the user's real access token; see Google token flow for how.
What the user is trying to do
A typical turn is "make a chart of this sheet and email it to …". Recipients see mail from the
logged-in user's own Gmail address, not a shared service account. An optional PNG comes from a
prior rasterize_chart call, not from bytes the model generated.
1. Log in to harnesi
The chat UI authenticates the same way as every other page: Django session cookie
(django_auth on the ninja routers). That identity is request.user. It is not a Google
identity yet. Superuser vs ordinary user only matters for saving the shared OAuth app
(client ID / secret) on the Tools page; it does not widen the agent sandbox.
2. One-time: save the Google OAuth app
A superuser pastes a Google Cloud Web application client ID and secret into Tools → Google
(chat/vault_api.py). Those values live encrypted in VaultSecret, not in .env. The redirect
URI that Google must allow is:
http://localhost:8000/api/integrations/google/callback
(local ./dev.sh; must match GOOGLE_OAUTH_REDIRECT_URI / the vault override exactly). The
consent scopes include Sheets, Drive metadata, gmail.send, and userinfo.email
(chat/google_oauth_api.py).
Until both client ID and secret are stored, Connect Google account stays disabled —
GET /api/integrations/google/status reports configured: false.
3. Connect this user's Google account
The user clicks Connect. The browser goes to /api/integrations/google/authorize, then to
Google's consent screen. Google redirects to the backend callback. CSRF/state is a signed
payload (user id + return path + SPA origin), not the Django session cookie — Google's redirect
hits port 8000, which often has no SPA cookie.
On success, GoogleCredential is upserted for that user: refresh token, a cached access token,
and the Gmail address. That row is what lets session start attach the gmail and sheets
proxy routes. chat/nono_profile.py's entitlements_for records the Google credential; it does
not write an MCP config.
Disconnect revokes at Google when it can and deletes the row. A later Connect needs prompt=consent
so Google issues a new refresh token.
Failure modes that look like "email doesn't work"
| What you see | Usual cause |
|---|---|
| Connect disabled | OAuth app client ID/secret not saved, or vault could not encrypt them |
redirect_uri_mismatch | URI not on this Web client in Google Cloud (must match the Client ID on the Tools page, exactly) |
| Tool error: connect Google and start a new Session | No GoogleCredential, or a warm session started before Connect |
Gmail API 401 on send | Access token expired (~1 hour); start a new Session so the warm process is recreated |
| Chart missing | chart_attachment was paraphrased; it must be the exact rasterize_chart filename |