---
name: google-sync
description: Engine substrate — read-only sync from Google Calendar + People API into the knowledge graph. Phase 5 of the second-brain integration push.
title: Google Calendar + Contacts Sync
category: _meta
status: active
author: system
metadata:
  priority: 0
---
# Google Calendar + Contacts sync

This is **engine substrate**, not an agent skill. It is never injected into an agent prompt. It exists so the strategy of "what to sync, how often, which calendars" lives in YAML, never in Python.

## What it does

When the user has connected their Google account, a daily cron pulls:

- **Events** from the configured calendars (default: ``primary``) over a configurable window. Each event becomes an `event` entity in the registry; resolvable attendees become `(event, references, person)` triples that surface on the person's wiki page.
- **Contacts** from People API. Names that resolve to existing entities get auto-merged (the registry's fuzzy matcher already handles `"John Smith"` ↔ `"J. Smith"` ↔ `"Smith, John"` permutations). New names go through the contact-proposal approval queue at `/proposals` — privacy-respecting by design.

## Why read-only

The sync NEVER writes back to Google. Phase 5's scope is "ingest into the graph"; agent-driven event creation, RSVP handling, and contact mutation are deliberately out of scope for v1.

## Configuration

Every knob in [config/sync.yaml](config/sync.yaml). Edit and the next sync picks it up.

- `calendar.lookback_days` / `calendar.lookahead_days` — sync window (default 30/30)
- `calendar.calendars` — list of calendar IDs (default `["primary"]`)
- `contacts.batch_size` — page size for People API (default 200)
- `cadence_hours` — how often the cron fires (default 24h)

## Triple shape

Each resolved attendee → one triple:

```
(calendar:<google_event_id>, references, person_slug)
  authority         = "system"
  epistemic_status  = "believed"
  confidence        = 0.85
  knowledge_type    = "episodic"
  decay_condition   = "when:unaccessed:90d"
  valid_from        = event.start
  valid_until       = event.end
  situational_context = {
    context_type: "calendar",
    matched_text: <attendee name or email>,
    snippet:      "<event summary> — <RSVP status>",
    when:         <event start>,
    pass_name:    "calendar_attendee",
    google_event_id: ...,
    calendar_summary: ...,
    attendee_email: ...,
  }
```

Unresolved attendees don't get triples but are recorded on the source for later disambiguation.

## OAuth setup (one-time)

The user creates a Google Cloud project with OAuth credentials (Web application client, redirect URI `http://localhost:8000/api/integrations/google/oauth/callback`), and pastes the `client_id` + `client_secret` into Settings → Integrations → Google. From there, "Connect Google" opens the consent screen, the user grants the read-only scopes, Google redirects back, and tokens are stored in the encrypted `CredentialStore` under `service="google_oauth"`.

Refresh tokens stay encrypted at rest. Access tokens are refreshed automatically when within 60 seconds of expiry.

## What it does NOT do

- Writes to Google.
- Multi-account management (single Google account per AgntSpace install).
- Recurring-event expansion beyond what `singleEvents=true` returns.
- Calendar conflict detection / scheduling assistance.

These are real follow-ups; v1 is scoped tight on purpose.