Provider Recipes
Use this page while creating a provider for a known upstream. First select the enforcement boundary, then apply one recipe, bind the resource, wire one language-native transport, and validate the whole path. Field schemas stay canonical in Define Resources and Providers and the Admin package reference.
Prerequisites
Section titled “Prerequisites”- The resource, scopes, Gateway application, and active policy already exist.
- The upstream’s official token or authentication documentation is available.
- Production secrets are ready to enter through Console or a trusted Admin API process, never application source.
Provider recipes live in the documentation, not the web console. The web console stays focused on the fields a given setup needs; this page is the reference you keep open beside it.
Choose the enforcement boundary first
Section titled “Choose the enforcement boundary first”The most common mistake is treating these four boundaries as interchangeable. They are not - they differ in where enforcement happens, who writes the action-result audit, and who holds the upstream credential.
| Boundary | Enforcement point | Credential holder | Action-result audit | Use when |
|---|---|---|---|---|
| Gateway-mediated | Caracal Gateway | Gateway (sealed provider secret) | Gateway | The upstream is HTTP-routable and you want Caracal to broker the credential and enforce every call. |
| Adapter-verified | Your service process | Your service | Your service after adapter verification | You own the service and must enforce mandates in process. |
| Application-managed call | Your application code | Your application | Your application | You call the provider directly and only attach Caracal context for attribution. Not Caracal-enforced. |
| Runtime token injection | Your runtime, eligibility gated by Caracal | Your runtime process | Your application | An existing client or CLI must receive a brokered token at runtime, and the provider sets allow_runtime_injection=true. |
Prefer Gateway-mediated unless you have a specific reason to enforce elsewhere. The recipes below note which boundary they use.
OpenAI API key (Gateway-mediated)
Section titled “OpenAI API key (Gateway-mediated)”Broker an OpenAI key so the agent never holds it.
| Field | Value |
|---|---|
| Provider kind | api_key |
header_name | Authorization |
auth_scheme | Bearer |
api_key | Your OpenAI secret key (sealed at creation). |
| Resource identifier | resource://openai |
| Resource upstream URL | https://api.openai.com |
| Resource scopes | openai:chat, openai:embeddings |
The Gateway forwards Authorization: Bearer <sealed key> upstream and strips caller auth. The agent calls the Caracal resource, not OpenAI directly.
Google Workspace (OAuth authorization code)
Section titled “Google Workspace (OAuth authorization code)”Delegated user consent for a Google API, refreshed inside STS.
| Field | Value |
|---|---|
| Provider kind | oauth2_authorization_code |
authorization_endpoint | https://accounts.google.com/o/oauth2/v2/auth |
token_endpoint | https://oauth2.googleapis.com/token |
redirect_uri | Your exact Caracal callback, e.g. https://api.pipernet.example/v1/zones/<zone>/provider-connections/oauth/callback |
client_id / client_secret | From the Google Cloud OAuth client. |
scopes | e.g. https://www.googleapis.com/auth/drive.readonly |
allowed_token_hosts | oauth2.googleapis.com |
authorization_params (Advanced) | access_type=offline, prompt=consent for refresh tokens. |
| Resource upstream URL | https://www.googleapis.com |
Caracal owns client_id, redirect_uri, state, and PKCE. Use the provider connect action to mint the consent URL for the shared upstream account, or bind it to a specific subject for per-customer isolation.
Google Cloud service account (jwt_bearer)
Section titled “Google Cloud service account (jwt_bearer)”Machine access to Google APIs - Vertex AI, Cloud Storage, and every other googleapis.com surface - signed with a service-account key instead of user consent.
| Field | Value |
|---|---|
| Provider kind | oauth2_client_credentials |
grant_type | jwt_bearer |
token_endpoint | https://oauth2.googleapis.com/token |
client_id | The service account email, e.g. agent@project.iam.gserviceaccount.com |
private_key | The service account’s PEM private key (sealed). |
scopes | e.g. https://www.googleapis.com/auth/cloud-platform |
assertion_subject (Advanced) | A user email, only for domain-wide delegation. |
| Resource upstream URL | e.g. https://aiplatform.googleapis.com |
STS signs the RFC 7523 assertion with the sealed key, carries the scopes inside the assertion’s scope claim as Google expects, and caches the returned access token. The same shape covers Salesforce’s JWT bearer flow: set client_id to the consumer key, assertion_subject to the Salesforce username, and assertion_audience to https://login.salesforce.com.
Atlassian Jira (HTTP Basic)
Section titled “Atlassian Jira (HTTP Basic)”Jira Cloud REST authenticates with an email and API token as a Basic pair.
| Field | Value |
|---|---|
| Provider kind | http_basic |
username | The Atlassian account email. |
password | The Atlassian API token (sealed). |
| Resource identifier | resource://jira |
| Resource upstream URL | https://your-site.atlassian.net |
| Resource scopes | jira:issues-read, jira:issues-write |
Gateway composes Authorization: Basic ... at forward time; callers never see the pair. The same recipe covers Twilio (Account SID as username, auth token as password), Elasticsearch, Zendesk, and other Basic-authenticated REST APIs.
GitHub (bearer token, with OAuth note)
Section titled “GitHub (bearer token, with OAuth note)”For automation, the simplest path is a pre-issued token.
| Field | Value |
|---|---|
| Provider kind | bearer_token |
bearer_token | A GitHub fine-grained personal access token (sealed). |
allowed_token_hosts | api.github.com |
| Resource identifier | resource://github |
| Resource upstream URL | https://api.github.com |
| Resource scopes | github:repo-read, github:issues-write |
For a delegated, consented account instead of a shared token, use oauth2_authorization_code with authorization_endpoint=https://github.com/login/oauth/authorize and token_endpoint=https://github.com/login/oauth/access_token.
Slack (OAuth client credentials or bearer)
Section titled “Slack (OAuth client credentials or bearer)”For a Slack bot token that does not expire, use a bearer provider.
| Field | Value |
|---|---|
| Provider kind | bearer_token |
bearer_token | The Slack bot/user OAuth token (sealed). |
allowed_token_hosts | slack.com |
| Resource identifier | resource://slack |
| Resource upstream URL | https://slack.com/api |
| Resource scopes | slack:chat-write, slack:channels-read |
For rotating machine tokens, use oauth2_client_credentials with Slack’s token endpoint and scopes.
Internal API (Caracal mandate or none)
Section titled “Internal API (Caracal mandate or none)”For services you own, you usually do not need an upstream credential.
| Setup | Provider kind | Use when |
|---|---|---|
| Mandate-aware | caracal_mandate | The internal service verifies Caracal tokens directly (issuer, audience, scopes, expiry, revocation). The Gateway forwards the resource mandate as Authorization: Bearer .... |
| No upstream credential | none | The Gateway is the enforcement point and the upstream expects no credential. |
| Field | Value |
|---|---|
| Resource identifier | resource://pipernet |
| Resource upstream URL | https://api.pipernet.example |
| Resource scopes | pipernet:read, pipernet:submit |
Runtime token injection
Section titled “Runtime token injection”When an existing CLI or client cannot route through the Gateway but still needs a brokered credential, set allow_runtime_injection=true on the provider. This is a distinct security boundary: the token is injected into your runtime, and enforcement of the actual call is your application’s responsibility, not the Gateway’s. Use it deliberately and confirm eligibility with the preflight below.
Wire the transport into provider clients
Section titled “Wire the transport into provider clients”Once a provider and resource exist, the client side of a Gateway-mediated setup is two lines: a resource binding and the SDK transport. The binding maps the provider’s natural upstream prefix to the resource, so agent code keeps calling the provider’s real URL - the transport rewrites matching requests through the Gateway, attaches the caller’s mandate, and the Gateway injects the sealed provider credential upstream.
export CARACAL_RESOURCES="resource://openai=https://api.openai.com"Bindings can also live in the runtime profile as [[credentials]] entries; see Configure Workloads. Provider clients still require an API key argument at construction - pass a placeholder such as caracal-gateway. The transport replaces the Authorization header on Gateway-routed calls, so the placeholder never reaches the provider.
Each recipe below assumes a constructed SDK client (const caracal = new Caracal(), caracal = Caracal(), client, _ := caracal.New()) and runs inside a governed Session context - transport() borrows the ambient Session’s authority, unlike applicationTransport(), which provisions its own (see Call Protected Resources).
OpenAI
Section titled “OpenAI”import OpenAI from 'openai'
const openai = new OpenAI({ apiKey: 'caracal-gateway', fetch: caracal.transport({ scopes: ['inference:invoke'], propagation: 'gateway-only' }),})from openai import AsyncOpenAI
openai = AsyncOpenAI( api_key="caracal-gateway", http_client=caracal.transport(scopes=["inference:invoke"], propagation="gateway-only"),)import ( "github.com/openai/openai-go" "github.com/openai/openai-go/option")
openaiClient := openai.NewClient( option.WithAPIKey("caracal-gateway"), option.WithHTTPClient(client.Transport(nil, caracal.CallOptions{ Scopes: []string{"inference:invoke"}, Propagation: caracal.PropagationGatewayOnly, })),)Anthropic
Section titled “Anthropic”Binding: resource://anthropic=https://api.anthropic.com.
import Anthropic from '@anthropic-ai/sdk'
const anthropic = new Anthropic({ apiKey: 'caracal-gateway', fetch: caracal.transport({ scopes: ['inference:invoke'], propagation: 'gateway-only' }),})from anthropic import AsyncAnthropic
anthropic = AsyncAnthropic( api_key="caracal-gateway", http_client=caracal.transport(scopes=["inference:invoke"], propagation="gateway-only"),)import ( "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/option")
anthropicClient := anthropic.NewClient( option.WithAPIKey("caracal-gateway"), option.WithHTTPClient(client.Transport(nil, caracal.CallOptions{ Scopes: []string{"inference:invoke"}, Propagation: caracal.PropagationGatewayOnly, })),)Google Gemini
Section titled “Google Gemini”Binding: resource://gemini=https://generativelanguage.googleapis.com.
from google import genaifrom google.genai import types
gemini = genai.Client( api_key="caracal-gateway", http_options=types.HttpOptions( httpx_async_client=caracal.transport( scopes=["inference:invoke"], propagation="gateway-only" ) ),)import "google.golang.org/genai"
gemini, err := genai.NewClient(ctx, &genai.ClientConfig{ APIKey: "caracal-gateway", HTTPClient: client.Transport(nil, caracal.CallOptions{ Scopes: []string{"inference:invoke"}, Propagation: caracal.PropagationGatewayOnly, }),})The @google/genai JavaScript SDK does not accept a custom fetch. In TypeScript, call Gemini through its OpenAI-compatible surface with the OpenAI wiring above and baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/"; the binding stays the same.
LiteLLM
Section titled “LiteLLM”The LiteLLM Python library sends traffic through module-level httpx sessions:
import litellm
litellm.aclient_session = caracal.transport( scopes=["inference:invoke"], propagation="gateway-only")litellm.client_session = caracal.sync_transport( scopes=["inference:invoke"], propagation="gateway-only")Bind one resource per upstream LiteLLM reaches, or govern a self-hosted LiteLLM proxy as a single resource (for example resource://litellm=https://litellm.internal.example). The proxy speaks the OpenAI protocol, so TypeScript and Go clients use the OpenAI wiring above with baseURL pointed at the proxy.
Ollama
Section titled “Ollama”Binding: resource://ollama=http://ollama.internal.example:11434.
import { Ollama } from 'ollama'
const ollama = new Ollama({ host: 'http://ollama.internal.example:11434', fetch: caracal.transport({ scopes: ['inference:invoke'], propagation: 'gateway-only' }),})import "github.com/ollama/ollama/api"
base, _ := url.Parse("http://ollama.internal.example:11434")ollamaClient := api.NewClient(base, client.Transport(nil, caracal.CallOptions{ Scopes: []string{"inference:invoke"}, Propagation: caracal.PropagationGatewayOnly,}))The Ollama Python client constructs its own httpx client internally. In Python, call Ollama through its OpenAI-compatible /v1 endpoint with the OpenAI wiring above and base_url="http://ollama.internal.example:11434/v1".
Self-hosted upstreams like LiteLLM and Ollama usually need no upstream credential: use provider kind none and let the Gateway be the enforcement point.
Validate before the first call
Section titled “Validate before the first call”After you create the provider, bind it to the resource, and activate a policy, run Check Provider Readiness. It validates control-plane and Gateway readiness, the resource-to-provider binding, application validity, provider configuration completeness, scope coverage, token endpoint and upstream reachability, runtime-injection eligibility, and that the active policy set actually returns allow for your application, resource, and scopes - with a concrete remediation for every failure.
Then make one allowed call and one scope-denied call. The allowed call must appear as an STS decision followed by a Gateway result under one request ID; the denied call must not reach the upstream.
Related
Section titled “Related”- Define Resources and Providers
- Govern Agent Frameworks
- Author Policy Data
- Debug Authorization Decisions
- Protect a Gateway-Routed HTTP API
Next Step
Section titled “Next Step”Integrate the caller with the TypeScript, Python, or Go workflow.

