$npx -y skills add superdesigndev/tools-registry --skill add-oauth-providerAdd a provider to treg's OAuth registry (the ones treg holds its own approved app for). Use when asked to "add YouTube/Notion/Meta OAuth", "support connecting X", "add a new OAuth provider", or when a connect flow, capability picker, channel/account picker, or provider health pro
| 1 | # Adding an OAuth provider |
| 2 | |
| 3 | Two connect modes exist. **BYO** (`POST /oauth/start` with a caller-supplied |
| 4 | client_id/secret) already works for any OAuth2 provider and needs no code. This |
| 5 | skill is the **registry** path: treg owns the registered app, so the user picks a |
| 6 | provider and supplies nothing. Only add a provider here if treg holds — or is |
| 7 | willing to go get — the platform approval behind it. |
| 8 | |
| 9 | Read `src/treg/oauth_providers.py`'s module docstring first. The rule it states is |
| 10 | the one most easily broken: **scopes are per capability, never per provider.** |
| 11 | |
| 12 | ## The code changes |
| 13 | |
| 14 | All of these, in order. Skipping any one produces a provider that looks fine in |
| 15 | `/oauth/providers` and fails somewhere the tests don't reach. |
| 16 | |
| 17 | ### 1. Credentials → `src/treg/config.py` |
| 18 | |
| 19 | Add `<name>_client_id` / `<name>_client_secret` to `Settings`, loaded from |
| 20 | `TREG_<NAME>_CLIENT_ID` / `_SECRET`. **Reuse an existing pair when the platform |
| 21 | uses one app for several products** — all four Google providers plus YouTube |
| 22 | share `google_client_id`, because Google verification and API audits are scoped to |
| 23 | the *Cloud project*, not to the OAuth client. A second client in the same project |
| 24 | isolates nothing. |
| 25 | |
| 26 | ### 2. The provider → `src/treg/oauth_providers.py` |
| 27 | |
| 28 | Add the `OAuthProvider` and **register it in `REGISTRY`** (easy to forget; the |
| 29 | provider silently doesn't exist until you do). |
| 30 | |
| 31 | Capabilities are **cumulative supersets**, never swaps: |
| 32 | |
| 33 | ```python |
| 34 | scopes={ |
| 35 | "read": [READ_SCOPES], |
| 36 | "post": [*READ_SCOPES, UPLOAD], # post CONTAINS read |
| 37 | "manage": [*READ_SCOPES, UPLOAD, WRITE], # manage CONTAINS post |
| 38 | } |
| 39 | ``` |
| 40 | |
| 41 | `satisfied_capabilities()` is set-containment, so a non-cumulative `write` yields a |
| 42 | connection that can write but reports "no read". `default_capability` is the |
| 43 | broadest (most scopes) — deliberately, see its docstring. |
| 44 | |
| 45 | Split a capability whenever the platform splits the scope. YouTube needs |
| 46 | `read`/`post`/`manage` because uploading a video and being able to edit or delete |
| 47 | one are different Google scopes; collapsing them means a connection that can post |
| 48 | and then never fix a typo. |
| 49 | |
| 50 | Per-provider quirks worth knowing: `auth_params={}` for providers that reject |
| 51 | Google's `access_type`/`prompt` (LinkedIn, Slack, X), `pkce=True` + |
| 52 | `token_endpoint_auth_method="client_secret_basic"` for X, `extra_credential_*` when |
| 53 | a second credential rides along (Google Ads' developer token). |
| 54 | |
| 55 | ### 3. Discovery — "which account does this connection act on?" |
| 56 | |
| 57 | Set `discover_path` / `discover_key` / `discover_id_field` / `discover_label_field`. |
| 58 | |
| 59 | - Label and id fields are **dotted paths** (`_dig`), so nested values work: |
| 60 | `discover_label_field="snippet.title"`. |
| 61 | - `discover_base_url` when listing lives on a different host than the data API |
| 62 | (GA4 reports come from analyticsdata, properties from analyticsadmin). |
| 63 | - `discover_nested_key` when the rows are nested one level down. |
| 64 | - `enrich_*` when the listing returns bare ids and a second call is needed for a |
| 65 | human name (Google Ads). |
| 66 | - Query strings in `discover_path` are fine — discovery does not pass `params`. |
| 67 | |
| 68 | Leave it unset if the credential acts on the whole account; the UI then says |
| 69 | "whole account" rather than showing an empty picker. |
| 70 | |
| 71 | ### 4. `probe_path` — health + the Tools "Use" prefill |
| 72 | |
| 73 | A cheap authenticated GET **on `base_url`** (not `discover_base_url` — the probe |
| 74 | runs against the provisioned tool's own host). Without it the tool reads |
| 75 | "unchecked" forever and the Try panel opens blank. |
| 76 | |
| 77 | Prefer the path that returns a *human-recognisable* field, and keep it identical to |
| 78 | the sample path in step 5 — `openUse` prefers `health_check.path` over the sample |
| 79 | map, so if they differ the prefill silently changes after a reconnect. |
| 80 | |
| 81 | ### 5. Dashboard → `src/treg/web/index.html` |
| 82 | |
| 83 | Provider rows, the Connect button and the pickers are all data-driven from |
| 84 | `listing()`. **No new provider needs a UI change** — except: |
| 85 | |
| 86 | - **A new capability name** must be added to `capLabel` / `capHelp` (~line 2762). |
| 87 | An unknown name renders as the bare key plus "Requests the *x* scopes." |
| 88 | - **`samplePath`** (~line 3024) is a hardcoded host→path map used when a tool has |
| 89 | no `health_check` yet. Add the host so the Try panel prefills for tools that were |
| 90 | provisioned before the probe existed. |
| 91 | |
| 92 | ### 6. Tests |
| 93 | |
| 94 | `tests/test_oauth_providers_m3.py::test_every_provider_is_registered` asserts an |
| 95 | **exact set** of service names. Add yours or the suite fails. |
| 96 | |
| 97 | Then: `uv run pytest -q` (full suite — the registry touches health, tools and |
| 98 | connections). |
| 99 | |
| 100 | ## The platform side (the actual long pole) |
| 101 | |
| 102 | Th |