| name | integration-adder |
| description | Add a new third-party service credential integration to the ToRivers platform. Use this skill whenever anyone says "add X integration", "add X credential", "implement X in ToRivers", "make X available as a credential", "integrate X service", or any similar phrasing. This skill knows the exact files to modify, the correct order across all eight layers (catalog, API, frontend, sandbox security, sandbox runtime, SDK, infra, public capabilities surface), and handles all four integration patterns (new OAuth provider, Google service extension, API key, token). It also keeps the public capabilities endpoint and the bundled torivers-automation-creator snapshot in sync when an integration becomes fully wired. Always invoke this skill instead of exploring the codebase from scratch — it encodes all the conventions. |
ToRivers Integration Adder
You are implementing a new third-party service credential integration in the ToRivers monorepo. This is a specific, well-understood process with a fixed set of files to touch in a specific order. The system has eight distinct layers — missing any of them will break either the UI, the runtime, the sandbox security model, the developer experience, or the public capabilities contract.
Overview of the eight layers
| Layer | Files |
|---|
| 1. Shared type catalog | packages/shared/src/credentials/catalog.ts |
| 2. API / OAuth logic | packages/api/src/routers/credentials.ts, packages/database/src/credentials.ts |
| 3. Frontend UI | apps/web/utils/credential-providers.ts, apps/web/components/credentials/service-icons.tsx |
| 4. Sandbox security model | apps/ai-engine-v2/sandbox/credential_proxy.py |
| 5. Sandbox runtime execution | apps/ai-engine-v2/sandbox/credential_proxy_runtime.py |
| 6. SDK developer surface | torivers-sdk/src/torivers_sdk/context/clients.py, torivers-sdk/src/torivers_sdk/context/mocks.py |
| 7. Infra / deployment / docs | .env.example, docker-compose files, MCP prompts, internal docs |
| 8. Capabilities surface (only when fully wired) | apps/web/app/api/public/sdk/capabilities/route.ts, skills/torivers-automation-creator/references/capabilities-snapshot.json |
Step 1: Classify the integration type
| Type | When to use | Examples |
|---|
| A: New Google service | A new Google Workspace service under the existing Google OAuth umbrella | YouTube, Google Docs |
| B: New OAuth provider | A service using OAuth that isn't Google | Slack, Notion, LinkedIn, HubSpot |
| C: API key / token | Static API key or personal access token — no OAuth flow | OpenAI, Stripe, GitHub PAT, SendGrid |
If unclear, do a quick web search or Context7 lookup to confirm the auth mechanism.
Ask the user if ambiguous:
- Should this be immediately available (
isAvailable: true) or a "Coming Soon" placeholder?
- Do you want an SDK client implemented (for automation authors)?
- For OAuth: is the OAuth app already set up in the provider's developer console?
Step 2: Research the integration
Use Context7 first:
mcp__context7__resolve-library-id → find official SDK/docs
mcp__context7__query-docs → get auth details, scopes, endpoints
Fall back to WebSearch if Context7 has nothing useful:
{service} OAuth 2.0 developer guide
{service} API authentication scopes
{service} OAuth token endpoint refresh
What you need from research:
- OAuth authorization URL (Type B)
- OAuth token URL (Type B)
- Required scopes for each sub-service (OAuth types)
- How to get the user's email / unique identifier after OAuth (for deduplication)
- Does the token expire? Is there a refresh_token?
- API key format and field names (Type C)
- What HTTP endpoints will automation authors actually call?
Step 3: Execute all file changes
Work through the sections below for your integration type. Read every file before editing it.
LAYER 1 — Shared Type Catalog
File: packages/shared/src/credentials/catalog.ts — Required for ALL types.
This file is the canonical source of truth used by both the frontend and backend to resolve service names.
For Type A (new Google service): Add to:
GOOGLE_SERVICE_ALIASES (if it has a short alias)
GOOGLE_SCOPE_BY_SERVICE (the OAuth scopes)
SERVICE_DISPLAY_NAME
SERVICE_ICON_KEY
For Type B (new OAuth provider): Add to:
CredentialProviderId type union
SERVICE_TO_PROVIDER map
SERVICE_DISPLAY_NAME
SERVICE_ICON_KEY
For Type C (API key): Add to:
SERVICE_TO_PROVIDER (use the service name as its own provider)
SERVICE_DISPLAY_NAME
SERVICE_ICON_KEY
LAYER 2 — API / OAuth Logic
File: packages/api/src/routers/credentials.ts
Type A — add to oauthProviders.google.scopeOptions (around line 315):
your_service: [
"https://www.googleapis.com/auth/your.scope",
],
Type B — three additions:
- Add entry to
oauthProviders object (around line 306):
your_provider: {
authUrl: "https://provider.com/oauth/authorize",
tokenUrl: "https://provider.com/oauth/token",
defaultScopes: ["identify", "email"],
scopeOptions: {
your_service: ["scope1", "scope2"],
},
},
- Add user info fetcher near the other
fetchXxxUserInfo functions (around line 575):
async function fetchYourProviderUserInfo(accessToken: string): Promise<{ email?: string; id?: string }> {
const response = await fetch("https://api.yourprovider.com/users/@me", {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!response.ok) throw new Error(`Failed to fetch user info: ${response.statusText}`);
const data = await response.json();
return { email: data.email, id: data.id };
}
- Add dispatch to
getOAuthAccountIdentifier() (around line 630):
case "your_provider": {
const userInfo = await fetchYourProviderUserInfo(tokens.access_token);
return userInfo.email || userInfo.id;
}
Type C — no changes needed to this file.
File: packages/database/src/credentials.ts
Type B only — two additions:
- Add a token refresh function (model it on
refreshGoogleOAuthTokens around line 1264):
export async function refreshYourProviderOAuthTokens(
refreshToken: string,
clientId: string,
clientSecret: string,
): Promise<{ access_token: string; expires_in: number }> {
const response = await fetch("https://provider.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: clientId,
client_secret: clientSecret,
}).toString(),
});
if (!response.ok) throw new Error(`Token refresh failed: ${response.statusText}`);
return response.json();
}
- Dispatch to it inside
refreshOAuthCredential() (around line 1393):
} else if (provider === "your_provider") {
newTokens = await refreshYourProviderOAuthTokens(
decryptedData.refresh_token, clientId, clientSecret,
);
If the provider doesn't use refresh tokens, skip this step and add a comment explaining why.
LAYER 3 — Frontend UI
File: apps/web/utils/credential-providers.ts
Type A — Add service to the Google provider's services array:
{
id: 'google_your_service',
name: 'Google Your Service',
icon: 'google_your_service',
description: 'Short description of what it does',
credentialType: 'oauth',
scopes: ['https://www.googleapis.com/auth/your.scope'],
isAvailable: true,
},
Type B — Add a new provider to the credentialProviders array. Place it in the appropriate category group, or create a new group:
{
id: 'your_provider',
name: 'Provider Name',
icon: 'your_service',
brandColor: '#HEXCOLOR',
supportsMultipleServices: false,
description: 'Short tagline',
services: [
{
id: 'your_service',
name: 'Service Name',
icon: 'your_service',
description: 'What users can do with it',
credentialType: 'oauth',
isAvailable: true,
},
],
},
Type C — Add to an existing category provider (e.g., 'development', 'commerce') or create one. Use credentialType: 'api_key' or 'token' and define fields:
{
id: 'your_service',
name: 'Service Name',
icon: 'your_service',
description: 'What it does',
credentialType: 'api_key',
fields: [
{
name: 'apiKey',
label: 'API Key',
type: 'password',
required: true,
placeholder: 'sk_...',
helpText: 'Find your key at dashboard.yourservice.com/api-keys',
},
],
isAvailable: true,
},
File: apps/web/components/credentials/service-icons.tsx
Check iconMap first — ~40 icons already exist. Add only if missing:
function YourServiceIcon({ className, ...props }: IconProps) {
return (
<svg viewBox="0 0 24 24" className={className} {...props}>
{/* Use the official brand SVG — find at brand kit, simpleicons.org, or Wikimedia */}
</svg>
);
}
your_service: YourServiceIcon,
LAYER 4 — Sandbox Security Model
File: apps/ai-engine-v2/sandbox/credential_proxy.py — Required for ALL types.
This file defines the allowlist of what each service can do inside sandboxed automations. If you skip this, the sandbox will reject any credential access with "invalid service" errors.
Step 1 — Add to ServiceType enum (around line 189):
class ServiceType(str, Enum):
GOOGLE_SHEETS = "google_sheets"
GMAIL = "gmail"
SLACK = "slack"
...
YOUR_SERVICE = "your_service"
Step 2 — Add to SERVICE_OPERATIONS dict (around line 390). Define every operation that automation authors can call on this service:
ServiceType.YOUR_SERVICE.value: [
ServiceOperation(
name="operation_name",
description="What this operation does",
required_params=["param1", "param2"],
optional_params=["optional_param"],
),
ServiceOperation(
name="another_operation",
description="Another operation",
required_params=["required_param"],
optional_params=[],
),
],
The operation name values here must exactly match the operation names used in credential_proxy_runtime.py.
LAYER 5 — Sandbox Runtime Execution
File: apps/ai-engine-v2/sandbox/credential_proxy_runtime.py — Required for ALL types.
This file contains the actual HTTP API calls made when an automation uses a credential. It has a class with service-specific async methods.
Step 1 — Add dispatch in the execute() method (around line 288, in the if service == chain):
if service == "your_service":
return await self._your_service(operation, credentials, params)
Step 2 — Add a service-specific method that implements each operation. Model it on the existing _slack() or _gmail() methods. Use execute_credential_operation() to make authenticated calls:
async def _your_service(
self, operation: str, credentials: dict, params: dict
) -> dict:
"""Execute Your Service operations."""
base_url = "https://api.yourservice.com/v1"
api_key = credentials.get("data", {}).get("apiKey") or credentials.get("data", {}).get("access_token")
if operation == "operation_name":
required = ["param1", "param2"]
missing = [p for p in required if p not in params]
if missing:
raise CredentialProxyError(f"Missing required params: {missing}")
result = await execute_credential_operation(
service="your_service",
operation="operation_name",
url=f"{base_url}/some/endpoint",
method="POST",
headers={"Authorization": f"Bearer {api_key}"},
json={"key": params["param1"]},
)
return {"result": result}
raise CredentialProxyError(f"Unknown operation: {operation}")
The credential dict structure depends on credential_type:
- OAuth:
credentials["data"]["access_token"], credentials["data"]["refresh_token"]
- API key:
credentials["data"]["apiKey"] or credentials["data"]["key"]
LAYER 6 — SDK Developer Surface
File: torivers-sdk/src/torivers_sdk/context/clients.py
Add a new client class that automation authors use. Model it on the existing GmailClient or SlackClient. Then register it in SERVICE_CLIENT_REGISTRY (around line 853):
class YourServiceClient(ServiceClient):
"""Client for Your Service API."""
@property
def service_name(self) -> str:
return "your_service"
async def some_operation(self, param1: str, param2: str) -> dict:
"""Do something with the service.
Args:
param1: Description
param2: Description
Returns:
Result dict
"""
return await execute_credential_operation(
service="your_service",
operation="operation_name",
url=f"{self._base_url}/endpoint",
method="POST",
headers=self._auth_headers(),
json={"param1": param1},
)
SERVICE_CLIENT_REGISTRY: dict[str, type[ServiceClient]] = {
"gmail": GmailClient,
"google_sheets": GoogleSheetsClient,
"slack": SlackClient,
"your_service": YourServiceClient,
}
File: torivers-sdk/src/torivers_sdk/context/mocks.py
Add a mock client for local testing. Model it on MockGmailClient or MockSlackClient:
class MockYourServiceClient(YourServiceClient):
"""Mock Your Service client for testing.
Example:
from torivers_sdk.context.mocks import MockYourServiceClient, MockCredentialProxy
client = MockYourServiceClient()
proxy = MockCredentialProxy()
proxy.register_mock_client("your_service", client)
"""
def __init__(self) -> None:
"""Initialize mock client with test data."""
super().__init__()
self._operations_performed: list[dict] = []
async def some_operation(self, param1: str, param2: str) -> dict:
"""Mock implementation — records the call and returns test data."""
call = {"operation": "some_operation", "param1": param1, "param2": param2}
self._operations_performed.append(call)
return {"result": "mock_result", "status": "success"}
def get_performed_operations(self) -> list[dict]:
"""Return list of operations called (for test assertions)."""
return self._operations_performed.copy()
LAYER 7 — Infrastructure, Deployment & Docs
Environment variable files (OAuth types only)
The env var pattern for provider "slack" is:
SLACK_OAUTH_CLIENT_ID / SLACK_CLIENT_ID
SLACK_OAUTH_CLIENT_SECRET / SLACK_CLIENT_SECRET
Update all of these files for a new OAuth provider:
.env.example (repo root) — add near the existing Google OAuth section:
YOUR_PROVIDER_OAUTH_CLIENT_ID=__CHANGE_ME__YOUR_PROVIDER_OAUTH_CLIENT_ID__
YOUR_PROVIDER_OAUTH_CLIENT_SECRET=__CHANGE_ME__YOUR_PROVIDER_OAUTH_CLIENT_SECRET__
Docker Compose files — add to the environment: section of the web service in each file:
docker-compose.yml
docker-compose.prod.yml
docker-compose.staging.yml
docker-compose.local.yml
YOUR_PROVIDER_OAUTH_CLIENT_ID: ${YOUR_PROVIDER_OAUTH_CLIENT_ID:-}
YOUR_PROVIDER_OAUTH_CLIENT_SECRET: ${YOUR_PROVIDER_OAUTH_CLIENT_SECRET:-}
YOUR_PROVIDER_CLIENT_ID: ${YOUR_PROVIDER_CLIENT_ID:-}
YOUR_PROVIDER_CLIENT_SECRET: ${YOUR_PROVIDER_CLIENT_SECRET:-}
The four docker-compose files have the same env var block — update all four. Look for the existing Google vars (lines ~139-154 in each) and add your new vars right after them.
MCP Server prompts & resources
File: packages/mcp-server/src/torivers_mcp/prompts.py
The create_automation() prompt (around line 38) lists example credential values. Add your new service name to the examples:
File: packages/mcp-server/src/torivers_mcp/resources.py
The MANIFEST_SCHEMA (around line 91) documents valid values for required_credentials. Add the new service name to the description or examples so AI coding assistants know it's valid.
Internal developer docs
These files contain example service names. Update them if they list specific integrations:
docs/developers/guides/using-credentials.md — the primary guide developers read. Add a section for the new service client showing how to call its methods with code examples.
docs/developers/reference/sdk-api.md — add the new client class to the API reference with method signatures.
docs/developers/reference/manifest-schema.md — shows credential name examples (gmail, slack). Add the new service name.
docs/developer-automation-ecosystem.md — high-level overview that lists supported integrations. Add your new service.
LAYER 8 — Capabilities Surface (Public Endpoint + Skill Snapshot)
This layer publishes the integration to the public SDK capabilities contract
that the torivers-automation-creator skill (and any third-party tooling) reads
when planning automations.
Apply this layer ONLY when the integration is fully usable end-to-end.
"Fully usable" means all three of these hold:
isAvailable: true in apps/web/utils/credential-providers.ts (Layer 3)
- The
Service has a client class in SERVICE_CLIENT_REGISTRY (Layer 6)
- The sandbox layers are wired (Layers 4 + 5)
If any of those is missing, the integration is "Coming Soon" and Layer 8 is
skipped. Note explicitly in your PR description that Layer 8 is deferred
until the gating layer lands.
File: apps/web/app/api/public/sdk/capabilities/route.ts
The endpoint already iterates credentialProviders (the Layer 3 file) and emits
only services that are isAvailable: true AND appear in the
SDK_CLIENT_BY_SERVICE map. So your service becomes visible automatically once
you add it to the map. Find the constant near the top of the file (around line
22) and add an entry:
const SDK_CLIENT_BY_SERVICE: Record<string, { client: string; mock: string }> = {
gmail: { client: 'GmailClient', mock: 'MockCredentialProxy.with_gmail()' },
google_sheets: { client: 'GoogleSheetsClient', mock: 'MockCredentialProxy.with_google_sheets()' },
your_service: { client: 'YourServiceClient', mock: 'MockCredentialProxy.with_your_service()' },
};
The client value matches the class name in SERVICE_CLIENT_REGISTRY (Layer 6).
The mock value matches the public mock factory exposed by the SDK.
File: skills/torivers-automation-creator/references/capabilities-snapshot.json
This bundled file is the offline fallback that the
torivers-automation-creator skill reads when the live endpoint is unreachable.
It is published publicly with the skill. It must stay in sync with the
endpoint's response shape and never lag the endpoint.
Add an entry to the credentials array:
{
"name": "your_service",
"title": "Your Service",
"provider": "your_provider",
"credential_type": "oauth",
"sdk_client": "YourServiceClient",
"mock_helper": "MockCredentialProxy.with_your_service()",
"oauth_scopes": [
"scope:one",
"scope:two"
],
"usable_in_automations": true
}
Field rules:
name — matches Service.id in credentialProviders and the SERVICE_CLIENT_REGISTRY key
title — human display name (matches Service.name)
provider — the parent Provider.id from credentialProviders
credential_type — oauth | api_key | token (matches Service.credentialType)
sdk_client — class name from SERVICE_CLIENT_REGISTRY
mock_helper — public mock factory (e.g. MockCredentialProxy.with_<name>())
oauth_scopes — full URL scopes for OAuth credentials; use [] for api_key/token
usable_in_automations — always true for entries in this array (the array's contract is "usable")
Public-safe content rules — applies to BOTH files in this layer
The endpoint response and the bundled snapshot are read by external developers
worldwide. Do not add any of these to either file:
- Internal monorepo file paths
- Internal architecture notes ("no sandbox runtime handler", "client not yet built")
- Product roadmap, coming-soon lists, or unreleased features
- LLM tier / cost classification
- Security blocklists (forbidden_modules, blocked_functions, etc.)
- Internal version pins or build identifiers
If you find yourself wanting to add explanatory context that points at internal
mechanics, put it in references/architecture.md (this skill's internal
reference) instead.
Validate Layer 8
cd apps/web && pnpm typecheck
python3 -m json.tool skills/torivers-automation-creator/references/capabilities-snapshot.json > /dev/null
Step 4: Type-check
Run in this exact order (only for packages you modified):
cd packages/shared && pnpm type-check
cd packages/api && pnpm type-check
cd apps/web && pnpm typecheck
For Python changes:
cd apps/ai-engine-v2 && source venv/bin/activate && flake8 sandbox/credential_proxy.py sandbox/credential_proxy_runtime.py --max-line-length=120
cd torivers-sdk && source .venv/bin/activate && flake8 src/torivers_sdk/context/ --max-line-length=120
Step 5: Report to the user
When done:
- Files changed — list each file with a one-line summary
- Environment variables — for OAuth: exact names to set (
SLACK_CLIENT_ID, etc.)
- OAuth app setup — exact redirect URI:
{NEXT_PUBLIC_BASE_URL}/dashboard/credentials/callback
- How to test — visit
/dashboard/credentials → "Add New Credential"
isAvailable status — whether it's live or "Coming Soon"
- SDK client — confirm whether a mock client was created for testing
- Capabilities surface — confirm whether Layer 8 was applied (endpoint map +
skill snapshot) or explicitly deferred (and which gating layer is missing)
Critical rules
- No database migrations —
user_credentials schema is generic, handles all types
- No new RLS policies — existing
auth.uid() = user_id policy covers all services
- No new tRPC procedures — existing
credentials.* procedures handle all types
- Skip sandbox layer only if coming-soon — if
isAvailable: false, you can defer credential_proxy.py and credential_proxy_runtime.py changes, but note this clearly
- Skip Layer 8 if any gating layer is missing — the public capabilities surface must only advertise services that are fully wired (isAvailable + SDK client + sandbox). If you ship Layer 8 prematurely, marketplace developers will declare credentials that fail at execution time
- The endpoint response and snapshot are public — never include internal file paths, architecture notes, roadmap, tiers, or blocklists in either Layer 8 file
- Check
iconMap before creating an SVG — ~40 icons already exist in service-icons.tsx
catalog.ts is always required — even for API key services
- All 4 docker-compose files need updating for OAuth (not just prod)
- Never commit secrets — only
.env.example with placeholder values; real values go in .env.local