Connectors Framework
⚠️ Partially superseded by Agent UI v2 (agent-ui.mdx + agent-ui-agent-capabilities-plan.md §0). The connector-consumption model changes: v2 agents run as out-of-process sidecars that no longer readgrants.jsondirectly. Instead the host stays the single writer/refresher and forwards short-lived access tokens out to the sidecar (§0.6 OAuth forward-out); the sidecar never sees the refresh token. The registry, Settings UI, andoauth_pkce/mcp_servertypes below still hold — only the grant-delivery path is superseded. Read v2 §0.6 for the current model.
Target: v0.18.x | Status: Spec approved; implementation underway | Priority: High
Date: 2026-04-30 Status: Active spec — implementation in flight on PR #926 (baseline of #915). Live tracking issue: #927 — the GitHub issue body is the canonical, continuously-updated spec; this document is a stable snapshot for in-repo discovery. Related issues: #915 (OAuth PKCE for Google — first concrete connector); #735 / #736 / #737 / #738 / #740 (Connector Hub track — supersede-vs-children call pending @kovtcharov-amd; see #927’s Coordination block). Related plans: Agent UI, Security Model. Scope: This spec promotes the OAuth-onlygaia.connectorslibrary shipped in #915 into a generalized Connectors framework with aSettings → Connectorspage modeled on Claude desktop’s native UI. The existing 22-entry MCP server catalog (today read-only in Settings) is unified into the same surface. v1 ships the framework with two implemented types (oauth_pkce+mcp_server); other types (api_token,composite_form,local_extension) are framework-shaped but follow-up.
TL;DR
GAIA already has two parallel mechanisms for “user wires up an external service”: MCP server installs (today via~/.gaia/mcp_servers.json, read-only Settings panel) and OAuth via gaia.connectors (just shipped in #915). This spec collapses both into one typed connector registry with a single Settings → Connectors page.
Each connector tile knows how to configure itself (OAuth flow, env-block paste form, future API-token paste, …). Per-agent grants gate every credential read, regardless of type. Settings becomes a navigable page (no more modal-on-modal); clicking a tile drills in-place to a ConnectorDetailView. The framework’s UI is type-driven, not connector-driven — adding a new connector costs one ConnectorSpec row, never new React.
v1 ships:
- Framework:
ConnectorSpecregistry,ConnectorHandlerProtocol, publicget_credential(connector_id, agent_id)API, FastAPI router with CSRF guard,gaia connectorsCLI, master-detail UI. - Two implemented types:
oauth_pkce(Google, refactored from #915),mcp_server(the 22 entries fromsrc/gaia/ui/routers/mcp.py:_CATALOGmigrated toConnectorSpecrows; secret env stored as$keyringreferences thatMCPClientresolves at spawn). - Three follow-up types:
api_token,composite_form,local_extension— shape exists, handlers defer to v2 child issues.
Why now
Building the framework now — before either auth pattern grows further — avoids two divergent UX patterns and lets follow-up integrations (GitHub PAT, Anthropic key management, Jira credentials, Microsoft 365) plug into a single typed registry instead of inventing a third pattern. User-confirmed direction (post-meeting on 2026-04-30):- Unify with MCP — one Settings → Connectors page; the MCP read-only panel goes away.
- Framework + Google only in v1 — first PR ships the framework with #915’s OAuth refactored under it. Other connectors are follow-up issues.
- Per-agent grants for every connector type — same
~/.gaia/connectors/grants.jsonledger gatesget_credential(connector_id, agent_id)regardless of type. - Rename
gaia.connections→gaia.connectors,~/.gaia/connections/→~/.gaia/connectors/,ConnectionsSection.tsx→ConnectorsSection.tsx,gaia connectionsCLI →gaia connectors. Since #915 is unmerged, no migration shims are needed — direct rename only.
User experience
- User opens AgentUI → Settings → Connectors and sees a grid of tiles: Google (OAuth), Mermaid Chart (MCP), Supabase (MCP), … with status chips (“Not configured” / “Connected” / “Running”).
- Clicking Configure on a tile drills into a type-specific detail view (in-place within the Settings page — no nested modal):
- OAuth tile → “Connect” button → system browser → consent → SSE updates UI to “Connected as
<email>” within 2 seconds (the existing #915 flow). - MCP-server tile → form rendered from the connector’s
config_schema(the env-block fields the MCP server needs). Paste API key → click Test → spinner → “Connected, 4 tools detected” → Save.
- OAuth tile → “Connect” button → system browser → consent → SSE updates UI to “Connected as
- The detail view also shows a Per-agent grants subsection: a list of installed agents whose
REQUIRED_CONNECTORSmatch this connector, each with a toggle. The user grants individual agents access; no agent can read a connector’s credentials without an explicit grant. - Disconnect / Disable from the same view clears the credential and the per-agent grants in one click. SSE refreshes the UI.
- If a refresh token is revoked or an MCP server’s API key is rotated remotely, the next agent run hits the failure, AgentUI shows the existing reauth banner, and the user re-configures from the same tile.
Connector type taxonomy
After adversarial review (full panel output in the implementation playbook), the v1 framework supports two types — proving extensibility without shipping unused stubs:
Three additional types —
api_token, composite_form, local_extension — are framework-shaped (the registry and dispatcher accept them) but not implemented in v1. They land in follow-up child issues when concrete catalog entries demand them.
Module layout
New module atsrc/gaia/connectors/:
gaia.connections (NOT renamed to gaia.connectors or gaia.connectors.<id>) — internal constant, not user-visible, and renaming it would orphan dev keyring entries from #915 with no benefit.
Public Python API
ConnectorHandler is a Protocol (not an ABC) matching OAuthProvider and EventEmitter from #915 — keeps duck-typed mixin convention consistent.
Storage layout
API endpoints
src/gaia/ui/routers/connectors.py:
Depends(_require_ui_header) (the existing X-Gaia-UI: 1 CSRF check at src/gaia/ui/routers/agents.py:58). The same guard is backfilled on the legacy /api/mcp/servers mutating routes — they’re missing it today.
Routers accept connector_id (a lookup key into the registry) only — they never accept command, args, mcp_command, or test_endpoint from the request body. The catalog is frozen at module import.
AgentUI surfaces
Settings is a page, not a modal
Today’sSettingsModal (a fullscreen overlay rendered on top of the chat view) is replaced by SettingsPage — a top-level navigable destination that replaces the chat view rather than overlaying it.
- Entry: clicking the gear icon (currently
setShowSettings(true)) navigates to the Settings page. - Exit: a back arrow (← top-left) and/or a close (✕) button returns the user to the chat view. State (chat session, scroll position) is preserved across the round-trip.
- No modal stack anywhere in the connectors flow. Drill-in within Settings is master-detail in-place, not a nested modal — the original
ConfigureModal-on-SettingsModalpattern is eliminated. - Other Settings sections (System Status, Custom Agents, Privacy & Data) remain as stacked sections in v1. A Claude-style sidebar nav across all of Settings (General / Connectors / Privacy / Account / etc.) is out of scope for v1 — this PR only converts the modal shell to a page and adds master-detail within Connectors. The sidebar nav is a follow-up child issue.
Connectors master-detail navigation
- Default Settings page renders all Settings sections stacked, including the Connectors section: a grid of
ConnectorTiles (icon, display name, status chip —Not configured/Connected as <email>/Running— and a Configure / Disconnect button). - Click a tile → the Connectors section content swaps in-place to the connector’s detail view (rest of the page is replaced or, implementer’s choice, scroll-locks behind it; the rule is “no modal”).
- Detail view header: connector icon + display name + external-link icon (jumps to the provider’s product page via
ConnectorSpec.product_url) + Disconnect button. - Detail view body: dispatched by
spec.type—OAuthConfigureBody(lifted from #915’sConnectorsSection),MCPServerConfigureBody(env form + Test button), and (for v2)ApiTokenConfigureBody,CompositeFormConfigureBody,LocalExtensionConfigureBody. - Per-agent grants subsection rendered below the body: lists agents whose
REQUIRED_CONNECTORSmatch this connector, with type-aware controls —oauth_pkceshows per-scope toggles,mcp_servershows a single “use this server” toggle (per-tool toggles are a v2 follow-up; the storage shape already supports them). ← All connectorslink in the detail-view header returns to the tile grid without losing scroll position.
UI extensibility model
The framework’s UI is type-driven, not connector-driven. New connectors do NOT introduce custom React components.- Adding a new connector (e.g. GitHub PAT) costs one
ConnectorSpecrow insrc/gaia/connectors/catalog/<id>.py— icon, display name, instructions_md, config_schema, optional product_url. Zero new React. - Adding a new type (e.g.
api_tokenin v2) costs one new handler class + one new ConfigureBody component, shared by every connector of that type. Tile grid, status chips, master-detail navigation, per-agent grants subsection, SSE plumbing — all reused.
ConnectorSpec rows — id="github" (api_token) and id="github-oauth" (oauth_pkce) — NOT one connector with custom UI. If a future connector cannot be expressed via the existing types, the right move is to add a new type, not to inject connector-specific React.
The Claude desktop “Tool permissions” matrix (per-tool allow/ask/deny within a connector) is out of scope for v1. Its data model is already supported by the existing grants.json scope-list shape (store ["search_repos", "create_issue"] instead of ["use"]), so the v2 follow-up only adds UI without a storage migration.
Markdown rendering policy
instructions_md (rendered in connector detail view) goes through react-markdown with disallowedElements=['script','iframe','object','embed','style'], urlTransform filtering to https: / http: / mailto: only, no rehype-raw. No dangerouslySetInnerHTML anywhere in the connector pipeline. This is a framework invariant — documented in docs/sdk/infrastructure/connectors.mdx so follow-up PRs cannot regress it.
MCP unification
The 22 entries insrc/gaia/ui/routers/mcp.py:_CATALOG (lines 22–231) become 22 ConnectorSpec(type="mcp_server", …) rows in src/gaia/connectors/catalog/mcp_servers.py. Each requires_config becomes a config_schema of secret fields.
McpServerHandler.configure(config) writes secret env to keyring, plain env + the connector entry to state.json, and writes a corresponding entry into ~/.gaia/mcp_servers.json where the env block contains keyring references ({"$keyring": "<service>:<key>"}) for any secret value. MCPClient resolves these references in-memory at spawn time — no secret env value lives plaintext on disk.
MCPClient fails closed when a $keyring reference can’t be resolved at spawn time (deleted entry, wrong service:key, locked keychain): it raises ConnectorsError naming the missing service:key tuple and refuses to spawn the server — never silently spawns with empty env.
After every configure() write, the handler calls MCPClient.reload() (a new method) so a freshly-configured server’s tools materialize without a GAIA restart.
gaia.mcp.client.config.MCPConfig becomes read-only: its add_server, remove_server, and _save methods are removed (or hidden behind a deprecated prefix that emits DeprecationWarning). The connectors framework is the sole writer to mcp_servers.json; the file write itself uses tempfile.mkstemp + os.replace to match the atomicity guarantees of grants.json.
The MCP read-only Settings panel is deleted in this PR — users see MCP servers as tiles in the unified Connectors page.
Naming convention: MCP-backed connectors use the id-suffix -mcp (e.g. id="github-mcp") so a future id="github" API-token connector won’t collide. ConnectorRegistry.__init__ validates id uniqueness at module-import time.
Out of scope for v1
Everything below becomes child issues under #927 once the framework lands:api_token,composite_form,local_extensionhandler types — framework-shaped but not implemented in v1.- GitHub PAT, Anthropic API key, OpenAI API key, Hugging Face token, Jira composite credentials — first batch of follow-up connector child issues once v1 lands.
- Microsoft 365 OAuth. Same
oauth_pkcemachinery; newConnectorSpec+ provider class. Separate issue. - MCP per-tool grants. v1 ships single-toggle “use this server”; per-tool granularity is forward-compatible via the existing scope-list grant key but waits for v2 UX.
- Local-extension auto-detection. The
installed_checkcallable design is fine but per-platform implementations (macOS bundle id, Chrome extension probe) are non-trivial. v1 ships the type behind a manual “I installed it” toggle once it returns. - Cross-process refresh-token rotation race. Known #915 limitation; same applies to API-token rotation. Documented in
docs/security/connectors.mdx; out of scope for v1. - Custom user-supplied catalog entries. v1 catalog is frozen at module import — no runtime extensibility API. A future “register custom connector” surface would need a separate threat model.
- Settings sidebar nav (Claude-desktop-style General / Connectors / Privacy / Account in left rail). v1 keeps stacked sections; sidebar is a follow-up.
Acceptance criteria
The full AC list (≈ 50 items across Unit & Integration, API endpoints, End-to-end, UI structure / navigation, Migration / refactor of #915, Security & code review) lives on the tracking issue and is the source of truth for what “done” means. Highlights:- Original 157 #915 OAuth tests pass under the new module name
gaia.connectors. - Every entry in legacy
mcp.py:_CATALOGhas a matchingConnectorSpecwithtype="mcp_server". Everymcp_serverid ends with-mcp. McpServerHandler.configure(config)for a connector with secret fields produces amcp_servers.jsonwhose env block contains{"$keyring": "..."}references but no plaintext secret value.MCPClientfails closed (raisesConnectorsError) when a$keyringreference can’t be resolved at spawn time — asserted by a unit test that pre-seedsmcp_servers.jsonwith a dangling reference.MCPClient.reload()afterconfigure()makes the new server’s tools visible to agents without a GAIA restart.- Every mutating endpoint requires the
X-Gaia-UI: 1CSRF header; backfilled on legacy/api/mcp/serversroutes too. - Settings is a top-level page (replacing
SettingsModal), with back/close affordance. No modal-on-modal anywhere. - Adding a new connector requires only a
ConnectorSpecrow — no new React component. Asserted by review checklist + a doc note indocs/sdk/infrastructure/connectors.mdx. - No secret value (refresh token, MCP env api_key, etc.) appears in any log record, file under
~/.gaia/connectors/, traceback, Pydantic dump, OpenAPI schema, or SSE event payload.
Implementation playbook
The detailed task list (T-0 through T-9, TDD-paired), the file-level rename map, and the full 6-agent adversarial review (with 11 Critical findings auto-amended into the plan) live at~/.claude/plans/floating-discovering-gray.md on the implementation worktree. That file is execution-time material; this document is the public spec.
A condensed sequence:
Risks & open coordination
- Connector Hub track overlap. Issues #735 / #736 / #737 / #738 / #740 cover the same destination with a different storage architecture (
vault://references via #545 vs. OS keyring +$keyringreferences already shipped in #915). Decision pending @kovtcharov-amd: close #735–#740 as superseded (re-target #737 as a v2 child of #927), OR demote #927 to a child of #735 as “Phase 0”. Either is fine; running both parents in parallel is not. See #927’s Coordination block for the row-by-row mapping. - PR shape (single vs. split). Currently #926 carries the #915 baseline; the framework refactor lands on top. With ≈ 50 AC items, splitting into PR-A (rename + framework scaffolding) + PR-B (handlers + UI + MCP unification) probably wins on reviewability — execution-time call.
- MCP
$keyringreference resolution at spawn. New surface area; the fail-closed test is the contract guard. - Cross-process refresh-token rotation race. Documented as v1 limitation in
docs/security/connectors.mdx(when that file lands). Same class of issue extends to future API-token rotation. - Local-extension installed-detection. Punted to v2 — v1 ships the type behind a manual toggle when it returns.