Files
gbrain/docs/guides/google-connect.md
Sina Matian 43597b19e5 v0.48.5.0 fix: community fix wave — 57 PR adoptions + 42 verified issue fixes + composite review rounds (#4954)
Every open community PR triaged and every open issue verified against master:
57 contributor pull requests adopted or reworked with credit, 42 verified issues fixed
directly (17 more closed by the adopted PRs), and a hostile review pass over the composed
branch (composite 8-lens review, /ship review army + Claude and Codex adversarial passes,
27-agent review of the CI fixes). Every adopted fix carries a regression test proven red
at its base. The #4807 slug-extension chain was reverted and returned to the maintainer.

Behavior changes: code sources sync as code (soft-delete window 72h); migration v146
adds the extract-atoms transcript-state tombstone table; serve refuses an unknown
GBRAIN_SOURCE at startup; think/graph-query resolve --source; recall composes since with
entity/session_id; delta cursors carry microseconds; sync --json is pure JSON; autopilot
skips unavailable paths; CJK chunk overlap applies to new pages only; import --source
requires a registered source; bootstrap harness validates --source and, under a live
PGLite serve, refuses without a token or wires hooks unpinned with --token.

See CHANGELOG.md 0.48.5.0 and PR #4954 for the per-theme list and contributor credit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: G0-0000 <G0-0000@users.noreply.github.com>
Co-Authored-By: Jey2311 <Jey2311@users.noreply.github.com>
Co-Authored-By: LongPV <LongPV@users.noreply.github.com>
Co-Authored-By: Masashi-Ono0611 <Masashi-Ono0611@users.noreply.github.com>
Co-Authored-By: Natetgmaxwell <Natetgmaxwell@users.noreply.github.com>
Co-Authored-By: NothoiMatt <NothoiMatt@users.noreply.github.com>
Co-Authored-By: Walliiee <Walliiee@users.noreply.github.com>
Co-Authored-By: afshaker <afshaker@users.noreply.github.com>
Co-Authored-By: amirelion <amirelion@users.noreply.github.com>
Co-Authored-By: arisgysel-design <arisgysel-design@users.noreply.github.com>
Co-Authored-By: armandovargash <armandovargash@users.noreply.github.com>
Co-Authored-By: awilhite <awilhite@users.noreply.github.com>
Co-Authored-By: bhattman-dev <bhattman-dev@users.noreply.github.com>
Co-Authored-By: gagecane <gagecane@users.noreply.github.com>
Co-Authored-By: hpamike <hpamike@users.noreply.github.com>
Co-Authored-By: hyunje-ethan-jang <hyunje-ethan-jang@users.noreply.github.com>
Co-Authored-By: javieraldape <javieraldape@users.noreply.github.com>
Co-Authored-By: jeanpierre121 <jeanpierre121@users.noreply.github.com>
Co-Authored-By: johnerik <johnerik@users.noreply.github.com>
Co-Authored-By: malachany <malachany@users.noreply.github.com>
Co-Authored-By: mariokarras <mariokarras@users.noreply.github.com>
Co-Authored-By: mike-tech-ship-it <mike-tech-ship-it@users.noreply.github.com>
Co-Authored-By: morven-ai <morven-ai@users.noreply.github.com>
Co-Authored-By: mvanhorn <mvanhorn@users.noreply.github.com>
Co-Authored-By: noelboss <noelboss@users.noreply.github.com>
Co-Authored-By: paul-0320 <paul-0320@users.noreply.github.com>
Co-Authored-By: proxynico <proxynico@users.noreply.github.com>
Co-Authored-By: spiky02plateau <spiky02plateau@users.noreply.github.com>
Co-Authored-By: tomatkins <tomatkins@users.noreply.github.com>
2026-09-07 23:34:38 -04:00

15 KiB

Connecting Google (Gmail, Calendar, Contacts)

gbrain's google connector ingests your Gmail threads, calendar events, and contacts into your brain and runs the open-loop engine on top: who is waiting on you, what you promised, and the context needed to respond.

Everything is bring-your-own OAuth: you create your own (free) Google Cloud OAuth client, so you own the app, the quota, and the tokens. Tokens live only in your local credential vault (~/.gbrain/credentials.json, mode 0600). The connector is read-only — it never writes to your Google account (gmail.readonly, calendar.readonly, contacts.readonly).

The fast path (one command)

gbrain google setup

setup walks the whole chain idempotently: guided credential intake → consent → source registration → a first sync (newest mail first, budgeted so it finishes fast; the deep backfill resumes automatically on later syncs) → your first gbrain waiting digest. Re-running it is always safe — it detects what's done and continues.

The pieces, if you want them separately:

gbrain google connect                 # credentials + consent only
gbrain sources add gmail-you --kind google --account you@example.com
gbrain sync --source gmail-you
gbrain waiting

One-time Google Cloud setup (~7 minutes)

You need a Desktop-app OAuth client in your own Google Cloud project. gbrain google connect prints this exact checklist when no credentials are on file:

  1. Create (or pick) a project: https://console.cloud.google.com/projectcreate
  2. Enable the three APIs (one click each):
  3. Configure the consent screen: https://console.cloud.google.com/auth/overview
    • Google Workspace account → user type Internal. Done — no verification, tokens never expire weekly.
    • Personal gmail.com → user type External, then BOTH: a. add your own email as a Test user (https://console.cloud.google.com/auth/audience), and b. click Publish app on that same page. Skipping this makes Google silently revoke your tokens every 7 days — the single most common failure in the wild.
  4. Create the OAuth client: https://console.cloud.google.com/auth/clients — application type Desktop app (NOT "Web application").
  5. Click Download JSON.

Then:

gbrain google connect --client-json ~/Downloads/client_secret_*.json

You can also paste the JSON contents on stdin (--client-json -), export GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET, or type the pair at the prompt. Pasted values are sanitized (smart quotes, stray whitespace) and validated by shape before anything talks to Google.

gbrain records the scopes Google actually granted (the consent screen lets you uncheck scopes), so a narrower-than-needed grant surfaces immediately as scope_missing with the reauth fix attached — never as opaque per-sweep 403s.

During consent Google shows "Google hasn't verified this app." That is YOUR app — click Advanced → Continue.

Headless / SSH / agent-on-another-machine

The connector auto-detects environments where a local browser can't open (SSH, WSL, containers, no display) and switches to paste-back mode: it prints the consent URL; you open it anywhere, approve, and the browser fails to load a http://127.0.0.1:41999/... page — that's expected. Copy that page's full address-bar URL and paste it back (interactive prompt), or complete non-interactively:

gbrain google connect --paste          # prints the URL, stores flow state
gbrain google connect --code "http://127.0.0.1:41999/?code=...&state=..."

Force it anytime with --paste or GBRAIN_FORCE_PASTE=1.

Note: Google's device-code flow is not an option — Gmail/Calendar/ Contacts scopes are excluded from it by Google. Loopback + paste-back is the supported path.

Multiple accounts

Repeat gbrain google connect --account work@yourco.com per account; each account becomes its own source (gbrain sources add gmail-work --kind google --account work@yourco.com) with independent sync cursors and locks.

Secondary calendars

The calendar sweep reads ONE calendar per source (so each keeps its own incremental sync token) and defaults to the account's primary calendar. Shared, subscribed, and secondary calendars the granted calendar.readonly scope already covers are ingested by pointing an additional source at them:

gbrain google calendars                 # list every calendar the account can
                                        # read (* marks the primary), with ids
gbrain google calendars --json          # { ok, status, account, calendars[],
                                        #   next_action.command } for agents
gbrain sources add family-cal --kind google --account you@example.com \
  --services calendar --calendar-id "family0123456789@group.calendar.google.com"

Say to your agent: "list the calendars my google account can read" — "ingest my family calendar into the brain" (your agent runs gbrain google calendars, then gbrain sources add … --calendar-id <id>).

Each source's incremental sync token is bound to the calendar it was minted for. Re-pointing an existing source at a different calendar (its g_calendar_id config key) is safe: the next sweep notices the change, logs [google] calendar changed (<old> → <new>), discards the old cursor, and re-lists the new calendar from a fresh window instead of replaying the old calendar's delta. Pages already imported from the previous calendar stay in the brain until you remove them — they are not reconciled automatically.

Continuous sync

Google sources are ordinary gbrain sources: gbrain sync --source <id>, gbrain sync --all, autopilot, and the dream cycle all pick them up. A bare un-targeted gbrain sync (repo mode) does not — target it or use --all. Health: gbrain google status (live refresh probe per account) and gbrain doctor (the google_oauth check warns once a Testing-mode account goes 5+ days without a successful refresh — note an account that refreshes daily gets no pre-warning before Google kills Testing-mode tokens at day 7; publishing to Production is the real fix). Once you publish the app to Production, record it by re-running consent — gbrain google connect --reauth <email> --consent-state production — so the weekly-expiry warning stops firing. Less-common flags (--via, --no-browser, --no-probe, --purge-client, and setup's --history-days / --sync-budget-ms): gbrain google --help. The --via hosted fast path (a verified OAuth client brokering consent, tokens still stored locally) is feature-gated off until the relay server exists; its full design lives at docs/designs/HOSTED_OAUTH_RELAY.md.

Sync freshness is honest by construction: the GMAIL sweep's success gates the source's synced stamp (it protects loop freshness — the thing gbrain waiting's staleness gate exists to guard); contacts/calendar failures mark the run partial without blocking it. A single thread that repeatedly fails to fetch is skipped after a few consecutive failures instead of wedging the sync forever; gbrain sync --source <id> --full retries skipped threads with a fresh ledger.

Rate limits during backfill

A large mailbox backfilling a wide --history-days window can trip Gmail's per-user rate limit in bursts — Google answers with HTTP 403 (rateLimitExceeded / userRateLimitExceeded) or 429, and it clears on its own within seconds to low minutes. The client retries a rate-limited request patiently — 6 attempts by default, exponential backoff with jitter capped at 60s, honoring Retry-After when Google sends one — before finally giving up and reporting rate_limited. That budget is deliberately much larger than the 2-attempt budget used for other retryable failures (like a 401 needing a token refresh): giving up too early used to mean a thread that would have succeeded a few seconds later was instead skipped for the rest of the sync.

Even when a thread's retry budget IS exhausted, a rate-limit failure is never counted toward the poison-skip threshold — unlike a genuine per-thread failure (a malformed message, a permissions edge case), a rate limit says nothing about that specific thread, so the sweep keeps retrying it on every future run instead of silently giving up on it.

Other ways to reach Google (no gbrain OAuth)

If your stack already holds Google access another way — a Google CLI with its own auth store, gcloud, or a credential gateway that can mint short-lived access tokens — the source can use it directly and skip gbrain's OAuth flow entirely. --account stays required as the IDENTITY (it drives "is this message mine" loop direction and the Gmail deep links' authuser); no credential is stored in gbrain for these modes.

Say to your agent: "connect my gmail through my existing Google CLI — your agent runs gbrain sources add <id> --kind google --access command --token-command \"<your token command>\" --account <email>"

# Any command that prints an access token (bare token, or JSON with a
# token/access_token field and optional expiry/expires_in). gbrain runs it
# at sync time and caches the token until it expires; it never stores it.
gbrain sources add gmail-work --kind google --account you@example.com \
  --access command --token-command "gcloud auth print-access-token"

# Or read a live token from an env var refreshed by something outside gbrain
# (a gateway sidecar, a cron job). The var NAME goes in config, never a value.
gbrain sources add gmail-work --kind google --account you@example.com \
  --access env --token-env GOOGLE_ACCESS_TOKEN

What changes vs the vault flow: gbrain google status's refresh probe and gbrain doctor's google_oauth check cover vault accounts only (your external tool owns token health); the scope preflight trusts --services (a token missing a scope surfaces as api_not_enabled/upstream per sweep instead of scope_missing); send-as aliases are fetched live when the token allows it, otherwise identity degrades to the account address alone. The token command runs locally at sync time with your shell — it lives in local source config, is never reachable over MCP, and is the same trust class as a recipe health-check command. Failures surface as access_command_failed / access_env_missing with the fix attached.

Troubleshooting

Every failure the connector can hit maps to a typed error with the fix attached. The catalog (also emitted as structured JSON with --json):

Code What happened Fix
client_json_wrong_type The downloaded JSON is a Web application client (top-level "web" key) Create a Desktop app client and download its JSON
client_json_unreadable The client JSON path doesn't exist or isn't the Google Cloud download Re-download from Credentials → your Desktop app client → Download JSON, pass with --client-json <path>
client_shape_invalid Pasted ID/secret malformed (smart quotes, truncation) Re-copy, or use --client-json
redirect_uri_mismatch Google rejected the redirect Almost always a Web-type client — use a Desktop app client
access_denied_test_user Consent blocked (External + Testing, you're not a test user — or you clicked Cancel) Add yourself under Audience → Test users, retry the same URL
pasted_wrong_url You pasted the consent-page URL Approve first, then paste the http://127.0.0.1... address-bar URL
state_mismatch The paste came from an older attempt Re-run connect, use the fresh URL
admin_policy_enforced Workspace admin blocks third-party apps (even your own client) Admin console → Security → API controls → trust the app; or make the consent screen Internal
wrong_account_consented A different Google account approved Re-run; the URL now pre-selects the right account
port_in_use Loopback port taken Re-run (fresh ephemeral port), --port <n>, or --paste
consent_timeout Consent never completed within 10 minutes Re-run connect
invalid_grant_testing_expiry Refresh token dead ≈7 days after connect Publish the app to Production, then gbrain google connect --reauth <email>
invalid_grant_revoked Access revoked (password change, manual revoke, client rotated) gbrain google connect --reauth <email>
invalid_grant_clock_skew Your system clock is off by >60s Fix time sync, retry
code_reused Authorization code used twice Re-run connect (codes are single-use)
invalid_client Client secret rotated/deleted in the console Download the current JSON, reconnect
no_refresh_token Google returned no refresh token Re-run connect; if persistent, revoke at https://myaccount.google.com/permissions and reconnect
api_not_enabled An API isn't enabled in your project The error carries the exact enable link (project pre-selected)
rate_limited Google quota hit Automatic backoff; nothing to do
scope_missing Connected with narrower --scopes than needed gbrain google connect --reauth <email>
relay_unreachable / relay_session_expired / claim_already_used / relay_disabled Hosted fast-path (gbrain.io relay) issues BYO connect always works: gbrain google connect
not_connected No vault entry for the account gbrain google connect
upstream Google returned an unexpected error Retry; if it persists, run gbrain google status --json and file the output
access_command_failed The --access command token command exited non-zero, timed out, or printed nothing token-shaped Run it by hand; it must print a bare token or JSON with token/access_token
access_env_missing The --access env variable is unset/blank in this process Export a live token into it (refresh externally), or switch back to the vault flow

Cursor expiries (historyId older than ~a week, calendar/contacts syncToken 410) are handled automatically with bounded re-lists — never user-facing.

Custody, privacy, spend

  • Tokens: local vault only, 0600, atomic writes. sources.config stores an account pointer, never a secret. gbrain creds list is always redacted.
  • Disconnect: gbrain google disconnect <email> removes local tokens; revoke Google-side at https://myaccount.google.com/permissions.
  • Upgrade/transfer: gbrain creds export produces a passphrase-encrypted bundle (a loud per-credential warning when a Testing-mode consent screen would travel with it — those tokens die within 7 days on the target).
  • LLM spend: commitment extraction sends recent email text (last 30 days, capped per sweep) to your configured chat provider. Kill switch: gbrain config set loops.extraction_enabled false. The deterministic unanswered-thread detector is free and always on.

For agents ([SHOW USER] protocol)

Every gbrain google/creds/waiting command supports --json and emits { ok, status, next_action: { command?, user_message? }, error? }. Human copy the harness should relay verbatim is fenced in [SHOW USER] blocks. The whole setup is exactly two user interactions: (1) the GCP checklist + client JSON hand-back, (2) one consent click. Never pass secrets via argv — use --client-json <path>, stdin, or env.