# Connections — credentials the agent can spend but never see A connection lets a Space's assistant call an outside service — GitHub, Google Calendar, an MCP server — with your user's credential, while no one can read that credential: not your page, not the room's members, not the model, not Witbitz. The credential lives in an attested enclave that can **use** it but never hands it out. > **Status: Production.** The SDK surface (`wb.connections`, `space.connections`) is part of the > [SDK](./sdk-accounts.md#account), in public beta. --- ## How it works Credentials live in the **standing Sentinel**: an always-on AWS Nitro enclave whose measurement is published. Each credential is sealed to a key only that measured image can open (AWS KMS, pinned to its PCR0), stored sealed, and spent only inside the enclave. 1. **Attaching checks the enclave first.** Before sealing anything, the SDK verifies a fresh Nitro attestation against the pinned measurement. A Sentinel running any other image is refused and nothing is sent. 2. **Your page keeps a handle, not the secret.** After attaching, the page holds only the connection's id, label and host, plus a call key. The secret never comes back. 3. **A call names the connection, not the credential.** The room says "call host X with connection Y". The enclave attaches the secret, makes the request, strips anything credential-like from the reply and returns the body. Every call carries a proof of spend — an HMAC with a per-connection key the enclave issued — so a captured request cannot be replayed onto another connection. The same path serves **confidential and standard rooms**: a confidential room's turn enclave reaches the Sentinel over an attested hop of its own. ## Who owns a connection A connection belongs to an **account**: the verified email on the user's sign-in token, never a field your page names. A sign-in made on your site is scoped to **your app** — connections attached there live under an account of your app's own, and your page cannot see or spend the user's connections from other apps. Your app needs no accounts of its own; the SDK signs users in with a one-time email code ([sign in](./sdk-people.md#sign-in)). --- ## Three kinds of connection | Kind | What it holds | How it is attached | |---|---|---| | **API key** | A bearer token for one host | `wb.connections.attach({ label, host, secret })` | | **OAuth** | A refresh token from **your** OAuth app; refreshed inside the enclave | `startConnect` → consent screen → `finishConnect` | | **MCP server** | A server's own sign-in, run inside the enclave, or a token you hold | `startMcp` → consent → `finishMcp`, or `attachMcp` | ### An API key ```js const gh = await wb.connections.attach({ label: 'GitHub', host: 'api.github.com', secret: token }) await wb.connections.list() // [{ id, label, host }] — never a secret, never a call key await wb.connections.detach(gh.id, { spaces: [space] }) // everywhere at once, and out of each Space you pass ``` ### OAuth, through your own OAuth app The OAuth app is **yours**, not ours. Your users consent to your app, on your consent screen. Witbitz never asks a provider for a scope on anyone's behalf, so Witbitz needs no provider verification — and neither do you, unless your scopes are sensitive (Gmail, for one, needs Google's assessment, for your app). ```js // your server, once (the tenant's secret key — never in a page) await admin.providers.attach({ kind: 'google', clientId, clientSecret, redirectUri, scopes }) // your page const { authUrl, state } = await wb.connections.startConnect({ providerId }) location.assign(authUrl) // the user consents // on your callback page: const conn = await wb.connections.finishConnect({ state, code }) ``` - **The enclave keeps the secrets.** It mints the PKCE verifier and holds your client secret, so the code your callback receives is useless to anyone else. Only the account that started a consent can finish it. - **The callback page is yours to host.** There is no Witbitz-hosted one. - **The consent URL and the hosts a provider may call are measured.** They are a table inside the enclave image, so no provider record can send a user to an unmeasured consent screen or open a host. Today it holds **Google**, whose connections may call `www.googleapis.com`. A new provider is a new enclave image. - **Refresh is safe under load.** One refresh per connection at a time, a rotated refresh token is always stored, and a refresh that fails to save leaves the previous record intact. Revoking the provider ends every connection under it. ### An MCP server The server's own sign-in runs inside the Sentinel, which registers with the server and keeps the PKCE verifier. Your page gets a URL and a state; the code it receives travels sealed back to the Sentinel. The server's token never reaches your page. ```js const st = await wb.connections.startMcp({ url: 'https://mcp.linear.app/mcp', label: 'Linear' }) let conn if (st.needsSignIn) { location.assign(st.authUrl) // your callback page (default /mcp-callback) receives ?code=&state=, then: conn = await wb.connections.finishMcp({ state, code, label: 'Linear', host: st.host }) } else { conn = await wb.connections.attachMcp({ url: 'https://mcp.deepwiki.com/mcp', label: 'DeepWiki' }) // or { token } } await wb.connections.mcpTools(conn.id) // { tools: [{ name, title, description, readOnly, … }], server } ``` Most MCP servers register a client during sign-in (Linear, Notion, Sentry, DeepWiki) and need nothing from you. Some need an OAuth app registered with the service — **GitHub, Asana, Box**, and Google's own MCP servers. For those the SDK signs in with Witbitz's registered app today. You can file your own now, with your page's `/mcp-callback` as its callback; the SDK will use it once it supports it: ```js await admin.providers.attachMcp({ issuer: 'https://github.com/login/oauth', clientId: 'Iv1.…', clientSecret: '…', // sealed to the attested Sentinel redirectUris: ['https://app.example.com/mcp-callback'], // one per origin your app runs on label: 'GitHub', }) ``` --- ## Granting a connection to a Space Nothing is granted by default. A grant writes the connection into the Space's sealed configuration, and only that Space's assistant may spend it. ```js await space.connections.grant(gh) // an API key or OAuth connection await space.connections.grant(conn) // an MCP server, with its recommended tools await space.connections.grant(conn, { tools: ['list_issues', 'create_issue'], auto: ['list_issues'] }) ``` **Only to a verified Space.** In an open Space the link is the whole key: whoever holds it could start a turn and spend the connection. So a grant needs an email-gated Space (otherwise `gate_required`). A private lane of a verified Space is gated to its member automatically; gate a lane of an open Space yourself before granting: `space.privateLane({ admission: { emails: [me] } })`. **Detach revokes everywhere.** `wb.connections.detach(id)` revokes the record in the enclave, which stops every room at once. ## What the assistant gets - **An API key or OAuth connection** becomes one tool, `connection_call`: method, path, headers and body against the connection's host. The assistant names the connection by its label or host. - **An MCP server** becomes its tools, each named `