gaia.connectors package implements OAuth 2.0 PKCE
(RFC 7636/8252) for desktop authorization flows. This page describes the
threat model, what we protect against, and the residual risks a user or
operator should know about.
What we protect
Refresh tokens never leave the OS credential store. Thegaia.connectors.store module writes to:
- macOS Keychain (built-in)
- Windows Credential Locker (built-in)
- Linux SecretService (gnome-keyring or kwallet)
keyrings.alt.PlaintextKeyring,
EncryptedKeyring) are explicitly refused at the entry of every
save and load (plan amendment A4). A Linux user without SecretService
sees an actionable error pointing at this page rather than silently
writing tokens to disk.
Refresh tokens never cross a public API or response body. The public
gaia.connectors.list_connections() returns only metadata
(provider, account_email, scopes, connected_at). The FastAPI
router enforces the same boundary on every JSON response. The OpenAPI
schema is exercised in tests/unit/connectors/test_secret_hygiene.py
to enforce this in CI.
OAuth state parameter is per-flow random and compared in
constant time. state = secrets.token_urlsafe(32); the callback
handler compares received state with hmac.compare_digest. Mismatched
or missing state returns 400 with a static error page — no echoed user
input.
Loopback redirect on 127.0.0.1, not localhost. Prevents DNS
rebinding. Bound on an ephemeral port (port=0) per flow.
Two-layer authorization for get_access_token. Before any access
token is returned to a tool body:
- The named agent must have a per-agent grant covering the requested
scopes (in
~/.gaia/connectors/grants.json). - The stored OAuth connection must actually carry those scopes (the user authorized them).
AuthRequiredError(AGENT_NOT_GRANTED); a
missing OAuth scope raises AuthRequiredError(CONNECTION_MISSING_SCOPES).
Either is surfaced to the user; nothing falls through silently.
Eager client_id_hash tripwire. Every load of a stored connection
verifies that the OAuth client id under which it was issued matches
the current configuration. A mismatch (e.g. after rotation) clears the
stored entry and raises AuthRequiredError(REAUTH_REQUIRED). Users
reconnect explicitly; we never use a stale connection.
What we do not protect
A malicious agent that the user has explicitly granted a scope can use that scope. This is by design — the user gave the agent the keys. The grants ledger and the AgentUI consent dialog exist so that “explicitly” is a high bar. Operators concerned about prompt-injection- driven scope escalation should review:- The agent’s
REQUIRED_CONNECTORSdeclaration before granting (visible in the consent dialog). - The CLI grants list (
gaia connectors grants list google) at any time. - Periodic revocation of unused grants
(
gaia connectors grants revoke <provider> <agent_id>).
agent.py can call
get_access_token_sync directly. That call still goes through the
two-layer authorization check. To bypass it the agent would need to
forge an agent identity, which is why _agent_context is private (plan
amendment A9). The grant-ledger key for custom agents is
custom:<sha256-of-agent.py>:<id>, so a custom agent that changes its
code gets a new key and cannot inherit a previous grant.
An attacker with read access to the user’s home directory can read
~/.gaia/connectors/grants.json. Grants are per-agent scope
declarations only — they do NOT contain tokens. Tokens are in the OS
keychain, which is encrypted by the OS (Keychain on macOS, DPAPI on
Windows, SecretService on Linux). The grants file is mode 0600 and the
parent directory 0700 on POSIX systems.
An attacker with read access to the OS keychain. Refresh tokens are
visible to a process with the user’s keychain access (e.g. malware
running as the user). This is the limit of OS-level credential storage
and is the same posture as every other app that stores OAuth tokens
locally (browsers, native mail clients, etc.).
Concurrent processes refreshing the same provider’s token. Two GAIA
processes running as the same user (e.g. gaia chat --ui and
gaia connectors status) each maintain their own in-memory access-
token cache and share the keyring. If both refresh concurrently and
Google rotates the refresh token, one process may observe
invalid_grant and the user reconnects transparently. We do not yet
implement inter-process locking. Track this in the followup issues if
the failure becomes common.
Threats considered
Operator checklist
-
GAIA_GOOGLE_CLIENT_IDis set in every environment that runs GAIA. - OS credential store is configured (built-in on macOS/Windows;
gnome-keyringorkwalleton Linux). - Production users do not see
keyrings.altplaintext fallback — if they do, the connections module raisesConnectorsErrorwith this page in the message. - Periodically review
~/.gaia/connectors/grants.jsonfor unexpected grants. The CLI commandgaia connectors grants list googleenumerates them. - Privacy policy is published and linked from the OAuth consent screen (required for Google verification).
- Sensitive-scope verification is submitted for the production client id (4–6 wk timeline).
See also
- Connections security model — credential storage, the grant + activation model, and revocation across every connector type.
- Connectors overview
- Google OAuth Client runbook