Integrations
The desktop app's Integrations tab - the same backend-owned connections the web app makes, connected in-app with any provider sign-in opening in the system browser.
The Integrations tab is the desktop screen onto the same backend-owned catalog the web app uses - one catalog, one set of connections, one place they are stored. A user who connects a service in their browser finds it already connected in the app, and the reverse. AI integrations documents the system; this page is what the app adds.
| Declared in | packages/config/src/ai/integrations.ts - see Catalog entries |
| Where users connect | Settings > Integrations |
| What it offers | every catalog entry whose surfaces include desktop |
| Stored | encrypted in your backend database, per user - nothing on the device |
| Shown when | config.ai.enabled, inside the desktop agents envelope |
The screen is gated on the AI service, never on how full the catalog is: every input to the settings rail is a build fact, so no entry waits on a network read. An empty catalog renders the empty state rather than hiding the entry - it is where a user connects their first integration, so hiding it until they had would be a door that only opens once you are through it.
What the app adds
- Sign-in opens the system browser. An
oauthentry sends the user to the provider in their real browser, so their existing session and password manager work, and the app never handles the credential. - No local credential store. There is no keychain entry and no on-device dialog: the app calls the same
/ai/integrationsroutes the web app does, over its device-granted bearer token. - Connections follow the workspace, not the device. Each is placed at a scope - a project, an organization, or the connector alone - from the same picker the web app shows, and a second machine inherits whatever that scope makes visible at sign-in. One connection is a tool on every surface at once: web chat, automated runs, and this app's own agents.
How a sign-in reaches the browser
The app authenticates with a bearer token, and a freshly opened system browser carries no session cookie for your API - so it cannot simply open the session-guarded /start URL. The app asks the backend for a start handoff instead: a short-lived, single-use token minted over the channel that is authenticated, then spent by the browser. Connecting never needs a web sign-in.
127.0.0.1, on a port the OS picks, behind a fresh nonce (RFC 8252).POST /api/ai/integrations/oauth/{provider}/handoff, authenticated, with the placement it wants and its listener: { "scope": "project" | "organization" | "user", "projectId": "...", "loopback": { "port": 49152, "nonce": "..." } } (all optional; omitted scope means the connector alone).{ "startUrl": "...", "expiresInSeconds": 60, "pollId": "..." }.startUrl in the system browser, verbatim. The URL is built server-side, so the app never assembles a path or a parameter name.POST /api/ai/integrations/oauth/{provider}/handoff/poll with { "pollId": "..." } every 2 seconds (for up to 10 minutes, stopping when the dialog closes) and gets { "status": "pending" } or { "status": "ready", "code": "..." }.POST /api/ai/integrations/oauth/{provider}/handoff/complete with { "code": "..." }, over its own bearer token. Only the session that minted the handoff can claim; the backend re-checks the placement, stores the connection and answers { "outcome": "connected" | "error" | "limit" }.How the finished sign-in gets back
The callback lands in the system browser, which has no session for the app and no reliable way into it: a browser does not always follow a redirect into a custom scheme. So the callback answers a small page that sends the browser straight on to the app's loopback listener.
| What the user sees | A moment of Back to {app}, then a page saying the tab can be closed |
| Where it goes | http://127.0.0.1:<port>/done?complete=<code>&connect=<entry>&redirect=<path>&nonce=<nonce> |
| Fallback | An Open {app} button: <protocol>://settings/integrations?complete=..., also tried on load when the app opened no listener |
| What it carries | The completion code and the URL's own fields - no session, no token, no account detail |
The listener answers one GET /done with its own Host and nonce, 404s everything else, and closes after that hit, after 10 minutes, on a newer sign-in or on quit. It routes the query exactly as a deep link: the Integrations tab claims the code, announces the outcome, reopens the entry after a failure, and continues to redirect when the sign-in worked - see sending a user to connect one thing. A code delivered twice (loopback plus poll or deep link) is claimed once.
A dev launch on macOS (pnpm dev) does not claim the scheme: it runs the stock Electron app, whose bundle id other apps that ship Electron share, so the link could open one of them instead. The loopback does not depend on the scheme, so dev sign-ins finish the same way.
| Property | Value |
|---|---|
| Lifetime | 60 seconds |
| Uses | one - a replayed link starts nothing |
| Bound to | the minting user, that one provider, and the placement resolved at mint time |
| Can it be traded for a session? | No. It begins one authorization and does nothing else. |
| Completion code | one use, ten minutes; reaches the poll only from a browser signed in as the minter; claimable only by the minting session |
| Loopback | 127.0.0.1 only, one hit, port 1024-65535, a url-safe nonce of 16-128 characters |
| Poll budget | cacheConfig.oauthHandoffPollRateLimit - 60 a minute per user, apart from the AI window |
The token stands in for a session in a URL handed to the OS, so it will be written to browser history. That is why it is single-use and expires in a minute. A forwarded link cannot connect someone else's account: a browser signed in as another user is refused, and a sessionless one's sign-in never reaches the sender's poll - its hand-back goes to 127.0.0.1 on the recipient's own machine, where the sender's listener is not. An attacker's own completion code delivered to someone else's app is refused at the claim.
Local MCP servers are a different feature
An mcp integration is a streamable-HTTP server your backend dials. A stdio server runs on the user's own machine, where no backend can reach it: users add those to their own coding CLI, the app leaves them alone, and they never appear on this tab.
Shared automations
A definition one member writes and every member can choose to run on their own machine - what it stores, how turning it on works, and why consent is the only gate on it.
Agent runtime
The agent runtime a desktop app with AI forks from its own binary - consent, lifecycle, and what runs on-device.