Add a new Rome-managed OAuth integration for a third-party service so a user can delegate access by clicking Connect, and Rome can act on the service with the delegated token (the GitHub/Slack model — brokered by Pantheon, NOT Composio). Use when asked to "add an integration / connector for <service>", "let users connect their <service>", or "delegate <service> access to Rome". NOT for Composio-managed toolkits (those are a declarative catalog entry only — see rome_apps/connector), and NOT fo...
Scanned 8/31/2026
Install to Claude Code
npx -y skills add rome-os/rome --skill add-oauth-integration --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Add Oauth Integration?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/rome-os-add-oauth-integration)More formats (shields.io, HTML) on the badges page.
---
name: add-oauth-integration
description: Add a new Rome-managed OAuth integration for a third-party service so a user can delegate access by clicking Connect, and Rome can act on the service with the delegated token (the GitHub/Slack model — brokered by Pantheon, NOT Composio). Use when asked to "add an integration / connector for <service>", "let users connect their <service>", or "delegate <service> access to Rome". NOT for Composio-managed toolkits (those are a declarative catalog entry only — see rome_apps/connector), and NOT for inbound real-time events (that needs a central webhook broker; this skill is OAuth/API-only). Worked example to diff against: the Slack connector (PR #1225) and the GitHub connector already in-tree.
---
# Add an OAuth integration
Goal end-state: a user opens **Settings → Connections**, clicks **Connect <Service>**, approves on the provider's consent screen, and Rome ends up holding a delegated token it can use to call the provider's API on the user's behalf. This mirrors the existing **GitHub** and **Slack** integrations — "Rome-managed" providers brokered by Rome's own Pantheon OAuth, with **zero Composio involvement**.
Work the five phases in order. Don't skip Phase 1 (scoping) — the answers decide how much of Phases 4–5 you build.
The two reference implementations to read and mirror throughout:
- **GitHub** — the original Rome-managed provider (single token, has a CLI consumer).
- **Slack** — the most recent, closest template for a fresh service (two tokens, pure API consumer). PR #1225.
---
## Phase 1 — Scope the integration (convey the goal, lock the decisions)
Two things are fixed for this skill and need no confirmation: the integration is **OAuth-only** (act on the service via its API — real-time inbound events need a central webhook broker and are out of scope) and **Rome-managed** (our own Pantheon OAuth, direct API, with **no Composio connection** — never mix the two). State the goal back to the user in one sentence, then resolve the two gates that actually change the build **with the human**:
1. **How many tokens?** Some providers return one access token; some return more, each for different methods. Find out — it drives token storage and the proxy's token selection.
2. **Token rotation?** Default **off**. Rotation is often irreversible once enabled and adds refresh machinery; only opt in deliberately.
Capture the answers — they are the spec for everything below.
---
## Phase 2 — Learn the provider's delegation mechanism (read the docs)
Read the provider's OAuth documentation and produce a short "build guide" (a scratch doc is fine). Extract exactly these, because the adapter in Phase 4 needs every one:
- **Authorization endpoint** + params: scope format (space vs comma), `state`, `redirect_uri`, and **whether it uses PKCE** (many confidential-client flows don't — confirm, don't assume).
- **Token exchange**: endpoint, request encoding, and the **response shape** — where the access token is, refresh token, expiry, and any *extra* tokens the response carries beyond the primary one.
- **Connecting account's identity** — resolve two things for the consenting human: an opaque, stable **`subject`** that keys and dedups the connection (the machine identity), and a **human-readable handle** so a person can recognize which account is wired up (it's the label in Settings → Connections). The handle can come from an **OIDC `email` / `email_verified` claim** or a **provider-specific profile API** — find whichever the provider exposes (Google has the OIDC claim; GitHub uses `/user/emails`; Slack uses `users.info`). **Rome broker invariant:** the callback (`packages/pantheon/src/app/oauth/[provider]/callback/route.ts`) currently requires that handle to be a **verified email** specifically and rejects the handoff without one, so the adapter must *produce* `{email, emailVerified}` by whatever means fits — real claim, provider API, or a justified synthesis (Slack treats any returned email as verified, since it only admits verified members). **If the provider exposes no email at all** (handle-only services), that's a genuine blocker to raise, not a detail — the fix is a broker change (key on `subject` alone), not something the adapter can paper over. **Trap:** if the grant mints a service/bot/app identity distinct from the human who clicked Connect, the "whoami" call on the primary token returns the *service* identity (no email) — resolve the human instead (read the installing user's id from the token response held in `bundle.raw`, then call the user-profile endpoint) and use the human for `subject` too.
- **Scopes** needed for the capabilities you want, and which are sensitive.
- **Quirks**: e.g. an API that signals errors in the response body rather than the HTTP status; rate-limit headers; a required `https` redirect.
---
## Phase 3 — Tell the human what to register (and where it plugs in)
You can't register the provider app for them (their account, their consent). Produce a precise checklist — and, if the provider's console supports app manifests (many do), a manifest file the human imports; otherwise step-by-step console instructions. The human needs:
- **Create the app** in the provider's developer console.
- **Redirect URL** — a single, central Pantheon callback: `https://<PANTHEON_DOMAIN>/oauth/<provider>/callback`. It is **not** per-tenant and has **no wildcard** — Pantheon brokers all tenants through one origin (`getPantheonOrigin` + `/oauth/<provider>/callback` in `packages/pantheon/src/app/start/route.ts`). Register the prod origin; for local testing add the tunnel origin too (Phase 5).
- **Scopes** from Phase 2.
- **Client ID + Secret** → set in Pantheon env as `<PROVIDER>_OAUTH_CLIENT_ID` / `<PROVIDER>_OAUTH_CLIENT_SECRET` (read automatically by `packages/pantheon/src/lib/oauth/providers.ts`).
- **Public distribution** toggle if users beyond the dev workspace will install it.
---
## Phase 4 — Build it
Two halves: (a) obtain consent + exchange the token (the broker, in Pantheon + core), and (b) consume the token for API calls (the connector). Mirror GitHub/Slack file-for-file.
### 4a. Obtain consent & exchange the token
- **Pantheon adapter** — `packages/pantheon/src/lib/oauth/<provider>.ts` implementing `OAuthProviderAdapter` (`createAuthorizationUrl`, `exchangeCode`, `fetchProfile`). Copy `oauth/slack.ts` (two-token, no PKCE) or `oauth/google.ts` (PKCE, refresh) as the closer match. Map the primary token to `bundle.accessToken`; stash the full provider response (any extra tokens, team/workspace id) in `bundle.raw` — it survives the broker→instance handoff. `fetchProfile` must return a verified email.
- **Register the provider**: add `"<provider>"` to `OAUTH_PROVIDERS` in `packages/pantheon/src/lib/oauth-providers.ts`, to `PROVIDER_ADAPTERS` in `packages/pantheon/src/lib/oauth/providers.ts`, and document the env vars in `packages/pantheon/.env.example`.
- **Core provider list**: add `"<provider>"` + a descriptor to `packages/core/src/lib/oauth-providers.ts`. The broker, token storage, `/api/integrations`, connect/disconnect, and the per-tenant handoff are all provider-agnostic and light up automatically.
- **Deliver the token to the runtime** (only if the connector/agent needs it): `packages/core/src/lib/<provider>-shell-integration.ts` writes the token(s) to `/run/rome/<provider>-oauth-token` on connect and clears on disconnect (single string like GitHub, or JSON for multiple tokens like Slack). Wire `sync…ForProvider` into `packages/core/src/api/routes/oauth.ts` (redeem) and `clear…ForProvider` into `packages/core/src/api/routes/integrations.ts` (disconnect). Reference `slack-shell-integration.ts` / `github-shell-integration.ts`.
### 4b. Consume the token for API calls
- **Mark it Rome-managed**: add `"<provider>"` to `ROME_MANAGED_TOOLKITS` in `rome_apps/connector/src/shared.ts`, set `romeManaged: true` in `rome_apps/connector/src/web/lib/connections.ts`, and ensure the toolkit exists in the SDK `SUPPORTED_CONNECTORS` (`packages/app-runtime-sdk`) with a `TOOLKIT_API_HOSTS` entry (`shared.ts`).
- **Direct proxy**: `rome_apps/connector/src/api/<provider>-proxy.ts` reads the token file and exposes a `<provider>ProxyCall` (auth header, default API host). Add a branch in `rome_apps/connector/src/actions/connector-proxy/index.ts` that, for this toolkit, reads the token and calls the proxy — bypassing Composio. For multi-token services, select the token by endpoint (Slack: `search.*` → user token). Reference `slack-proxy.ts` / `github-proxy.ts`.
- **Connect card**: `rome_apps/connector/src/web/<provider>-connect-card.tsx` (drives core's `/api/integrations` OAuth inline), side-effect-imported in `rome_apps/connector/src/web/App.tsx`, listed under `components:` in `rome_apps/connector/app.yaml`, and rendered from a branch in `rome_apps/connector/src/actions/connector-connect/index.ts`. The card is currently copy-pasted per provider — **if you're adding the 3rd one, generalize it** into one `<RomeManagedConnectCard provider>` instead.
- `connector_tool_execute` already short-circuits any Rome-managed toolkit to a "use connector_proxy" hint — no change needed.
### File checklist (mirror of the Slack change set)
```
packages/pantheon/src/lib/oauth/<provider>.ts NEW adapter
packages/pantheon/src/lib/oauth-providers.ts + OAUTH_PROVIDERS
packages/pantheon/src/lib/oauth/providers.ts + adapter map
packages/pantheon/.env.example + <PROVIDER>_OAUTH_CLIENT_ID/SECRET
packages/core/src/lib/oauth-providers.ts + provider + descriptor
packages/core/src/lib/<provider>-shell-integration.ts NEW token-file write/clear (if runtime needs the token)
packages/core/src/api/routes/oauth.ts + sync…ForProvider on redeem
packages/core/src/api/routes/integrations.ts + clear…ForProvider on disconnect
rome_apps/connector/src/shared.ts + ROME_MANAGED_TOOLKITS (+ TOOLKIT_API_HOSTS if missing)
rome_apps/connector/src/api/<provider>-proxy.ts NEW token read + proxy call
rome_apps/connector/src/actions/connector-proxy/index.ts + provider branch
rome_apps/connector/src/actions/connector-connect/index.ts + provider branch → connect card
rome_apps/connector/src/web/<provider>-connect-card.tsx NEW inline connect card
rome_apps/connector/src/web/App.tsx + import the card
rome_apps/connector/src/web/lib/connections.ts + romeManaged: true
rome_apps/connector/app.yaml + component + version bump
```
---
## Phase 5 — Validate end-to-end
Run the **validate-oauth-integration** skill — it owns the full recipe (static + unit → a token-only smoke that proves Rome can *use* a token → the real consent round-trip that proves Rome can *obtain* one) and the local-only blockers. Report honestly which layers actually ran; the real round-trip needs a registered app, creds, and a human at the consent screen, so it's often where the human takes over.
One build-coupled heads-up before you hand off: adding the provider trips drift guards — the enabled-provider lists in `packages/core/src/lib/oauth-providers.test.ts` and the Rome-managed lists in `rome_apps/connector/src/web/lib/connections.test.ts`. Update both, and switch any test that used the service as a stand-in *Composio* toolkit to a still-Composio one (e.g. `notion`).
---
## Gotchas (these generalize to every provider — platform invariants + universal OAuth facts)
Service-specific quirks are deliberately NOT listed here — Phase 2 tells you to hunt for them per provider, and Phase 4 says where they land. Two we hit with Slack are examples of that category, not standing gotchas: an API that signals errors in the body rather than the HTTP status (Slack's `ok:false` at 200), and a provider that returns more than one token (stash extras in `bundle.raw`). These below are the ones that bite on *every* integration:
- **The verified-email gate** in the Pantheon callback silently fails the handoff if `fetchProfile` returns no verified email. This is a Rome broker invariant, not an OAuth2/OIDC guarantee — most providers can satisfy it (OIDC claim, provider API, or justified synthesis), but a handle-only provider with no email can't without a broker change. Confirm the provider has a usable email *before* building (Phase 2).
- **Resolve the *human's* email, not the app's** — the single most likely bug (it broke Slack's whole round-trip): when the grant produces a service/bot identity alongside the human, calling the provider's "whoami" on the primary token returns the bot, whose empty email trips the gate on *every* connect. Read the consenting human's profile instead, and distrust comments that *say* installer while the code reads the app identity (contract > impl).
- **A scope is a multi-surface contract, not just an adapter constant** — adding a scope to the adapter's list grants nothing on its own; the *registered provider app* (and its manifest) must also offer it, or the consent screen never asks for it. Change all three in one diff: the adapter scope list, the manifest artifact, and the live app in the provider console (the human-only step). Auditing the granted token (`auth.test`-equivalent shows the scopes it actually has) is the only proof the contract closed.
- **One central redirect URL**, not per-tenant — Pantheon routes all tenants through `https://<PANTHEON_DOMAIN>/oauth/<provider>/callback`. (Rome architecture.)
- **Redirect-scheme strictness varies and breaks local testing** — providers differ on whether they allow `http`/`localhost` redirects. The strict ones require `https`, which is why local Layer-2 testing fronts Pantheon with a tunnel (`ROME_DEV_PANTHEON_PUBLIC_ORIGIN`). Check each provider's policy before assuming the dev `http` origin works.
- **Don't assume PKCE** — it varies per provider (Google uses `code_challenge`, others don't). Copy the closest adapter, but verify rather than inherit it.
- **Generalize at the 3rd provider** — the connect card and the per-provider proxy/connect branches are copy-paste today; fold them into one generic Rome-managed path instead of a fourth copy. (Codebase, not service.)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!