docs/adr/0232-hosted-connections-use-the-body-broker.md

ADR 0232: Hosted Connections use the body broker

Status: proposed (2026-10-05). Implements the code portion of VUH-1383, updating ADR 0196's initial account API proposal. Provider registrations and production authorization remain James's developer-account actions.

Decision

GitHub and Linear are connections to Clankie, following ADR 0181. The app's Connections settings and the signed-in account page use the same body-owned /v1/accounts API. They show verified identity and actual granted scopes, start authorization, and disconnect. The body stores provider credentials exclusively in its credential broker. This follows the VUH-1369 model-key boundary: fleet storage, gateway logs and body telemetry contain no customer provider tokens.

The hosted portal obtains an ephemeral paired Take Control device through the signed, sealed pairing flow in ADR 0173. It verifies body identity and carries device authorization inside the existing authenticated encryption envelope. Browser keys and the device bearer remain in memory. Explicit sign-out revokes that device; closing the tab forgets its local state. No server-side connection table mirrors the body's accounts.

GitHub

The body starts the device flow. The customer opens GitHub's verification page and enters the user code; the device code stays on the body. The body respects polling intervals and stores the returned access token under github, alongside the returned scopes and verified login. OAuth scope repo supports private-repo issue read/write. A future GitHub App can reduce permission breadth; that change is outside this delivery.

Hosted bodies never receive Clankie's shared GitHub developer secret: a customer with Take Control can read that body's broker. Hosted disconnect deletes only its local credential, reports revoked: false, and returns the GitHub permission-management page. The UI explains the remaining action. Removing the whole GitHub app authorization there is a customer action that can affect their other connected bodies; Clankie never does it automatically.

An owner-run self-hosted body may hold its own OAuth app secret under github-oauth-app. It revokes only its stored token with DELETE /applications/{client_id}/token and the token body, preserving other tokens for the same GitHub user and app. Only HTTP 204 is reported as revoked. The whole-app /grant endpoint is never used. If the owner's secret is absent or token revocation fails, the body deletes its local credential and returns the provider permission-management page. The shared developer secret is never provisioned to hosted bodies, bootstrap, fleet storage or customer portals.

Provider references: GitHub device authorization, token revocation.

Linear

Use a registered OAuth app with S256 PKCE and actor=app, requesting read,write for tracker operations. App installation requires the customer's workspace administrator consent. No admin, mention or assignment scope is requested. The body creates the state and verifier, validates the callback's single-use state and exchanges the code itself. Granted scopes come from the token response. Refresh and revocation remain broker operations.

The registered return is the gateway origin's /account/connections/callback. The callback page clears its URL before loading more UI, has no analytics, and uses no-store and no-referrer. Access logs omit callback query parameters. For the account page, a same-origin BroadcastChannel keyed by the unpredictable flow state returns the code to the initiating tab, including when provider COOP severs the popup's opener. For the native app, an explicit Open Clankie link returns clankie://accounts/linear/callback. Both consumers verify pending state, expiry and the current body connection, consume once, and seal completion to that body. A callback authorization code is not an access token; the body alone holds the verifier needed to exchange it. No OAuth verifier or token reaches the browser, app, gateway or fleet database.

Registered API OAuth uses broker entry linear-api and the GraphQL API. Existing linear credentials belong to the separately configured MCP/app connection; an API token must never be sent to the MCP audience. The body's in-process API tracker implements the canonical surface from ADR 0226, retaining repository, account, write and fleet grant fences. API outage refuses rather than selecting another backend. Disconnect clears both Linear credential lanes and pending flows so a hidden legacy connection cannot reappear after the customer disconnects. Provider-wide compare-and-set of issue descriptions is unavailable; unsafe description edits must refuse rather than risk overwriting uploaded evidence.

Provider references: Linear OAuth and PKCE, app actors, GraphQL API.

Configuration and verification

Public client IDs and the exact HTTPS callback can be provisioned to hosted bodies through strict bootstrap accounts configuration, mapped to body oauthApps. Without configured apps, connections show unavailable. Broker secrets and customer tokens cannot be accepted in bootstrap or fleet settings. Locally, clankie accounts apps configures public values; a developer GitHub revocation secret enters only an owner-run self-hosted body's broker through explicit stdin setup for its own OAuth app; hosted bodies refuse that setup. /connections and clankie accounts expose the same lifecycle.

Tests use local provider fixtures, real encrypted transport and isolated broker and fleet persistence. They exercise connection, tracker writes, disconnect, grant changes and refusal paths, and inspect logs/telemetry/persistence for credential markers. Native UI execution and real provider authorization require registered apps and remain separate from these code checks. No OAuth app, customer sign-in, token, live deployment or service restart is created here.