Google connectors: one shared OAuth loopback, then Calendar + a selective Sheets reader
Added a shared Google OAuth2 (Desktop/loopback) module and two BYO connectors that merge into the same operating-memory graph: Calendar (read upcoming events + a gated quick-add) and a research-grounded SELECTIVE Sheets reader (one founder-chosen sheet via spreadsheets.readonly — deliberately NOT the CASA-reviewed drive.readonly). Compiles, 243 lib tests pass, renderer typechecks; live OAuth still pending the founder's first authorize.
Verified this turn:
cargo test --lib→ 243 passed, 0 failed (7 new across calendar/sheets/google_oauth);tsc -p apps/desktop/tsconfig.json --noEmit→ 0 errors. NOT yet verified: a real browser OAuth grant + live API calls (needs the founder to click Authorize once).
Why one shared OAuth module
Calendar, Sheets, and (later) Drive all want the same Desktop-app loopback dance: open the consent screen → catch the 127.0.0.1 redirect → exchange the code → keep the refresh token in the OS keychain. Rather than copy that ~150 lines per connector, it lives once in google_oauth.rs as authorize(scope, refresh_key) + access_token(refresh_key). Each connector is then just its scope + REST:
calendar.rs—calendar.eventsscope; reads upcoming events;calendar_quick_addis a WRITE gated behind a human approval receipt (same gate asconnector::send_reply).sheets.rs—spreadsheets.readonlyscope; reads ONE connected spreadsheet's rows.
The client id/secret come from ~/.secrets/api-keys.env (the established secret() pattern); refresh tokens never touch disk.
Why Sheets is deliberately selective
Research into solo-founder operations was blunt: the spreadsheet IS the founder's operating DB (runway/burn, customer list, metrics), so reading it is high value — but reading the whole Drive is both noise and a trap. drive.readonly is a restricted scope → CASA Tier 2/3 security review, weeks of latency, annual re-cert. So the connector reads exactly the one sheet the founder pastes a link to, via spreadsheets.readonly (a sheet-by-id scope that needs no security review). Selective by sheet; broad-by-range only for v1.
How they merge into memory
No new code path. Each connector exposes rows to connector.rs's existing data-driven dispatch (fetch_calendar_items / fetch_sheets_items), which maps them to the source-agnostic SourceItem → the same item_node → the same GraphStore. A calendar event lands as event/schedule; a sheet row as record/data. So they sort and search alongside Gmail and GitHub in one graph — the point being the unified memory, not the connector count.
What's next
These are READ (plus a gated calendar write). The differentiator — letting the founder's own Claude act in natural language (send a reply, add an event, set a "mail-from-X → this area" rule) — means exposing those gated actions as MCP tools, which is the next build.