Integration
Connect workstations to a shared external service, such as an LLM gateway or an API that needs a key, so the tools on them use it with no per-machine setup.
An Integration connects workstations to a shared external service. It holds the service’s address, a reference to the Secret with its key, and the settings the tools on each workstation need to use it.
Without one, you set up each tool on each machine by hand, and each tool does it differently. With an Integration you declare the service once, and every workstation it reaches is set up the same way. The key stays in one Secret instead of being pasted into machines.
apiVersion: workstations.ringleader.dev/v1
kind: IntegrationAn Integration has one of two types:
type: llmpoints the AI coding tools on each workstation, Claude Code and Codex, at an LLM gateway. This page describes it.type: httpProxyadds a key to requests for the hosts you name as they leave the workstation, so the key is never on it. Credential injection describes it.
An Integration of either type can also register an MCP server with Claude Code and Codex. See Registering an MCP server.
Connect workstations to an LLM gateway
This Integration points Claude Code and Codex on every workstation labeled
team: platform at OpenRouter:
apiVersion: workstations.ringleader.dev/v1
kind: Integration
metadata:
name: llm
namespace: acme
spec:
type: llm
selector:
matchLabels:
team: platform
llm:
endpoint:
source: external
vendor: openrouter # picks the URL and the protocol
credentialRef:
name: openrouter # a Secret in this namespace
key: tokenYou write no base URL, no placeholder key and no tool settings. vendor selects them.
Ringleader knows each supported vendor’s address, the protocols it speaks, and the
environment variables and config files each tool needs. So you never write
ANTHROPIC_BASE_URL or an apiKeyHelper, and you never have to know which of a tool’s two
authentication variables must be set to an empty string.
If that is all you need, you are done. The rest of this page covers the vendors, the fields, who an Integration reaches, what it writes on a workstation, MCP servers, and how to override it.
Vendors
Four vendors are supported:
vendor | Endpoint | Notes |
|---|---|---|
openrouter | Supplied by Ringleader | url must not be set. Configures Claude Code and every other tool that speaks Anthropic’s protocol, and Codex. |
litellm | Yours (url required) | A self-hosted LiteLLM proxy. Both protocols are served under one address, so both Claude Code and Codex are configured. |
bifrost | Yours (url required) | A self-hosted Bifrost gateway. Both protocols are served under one address, so both Claude Code and Codex are configured. |
custom | Yours (url required) | Any other gateway. Configures the tools that speak Anthropic’s protocol. Codex is left alone, because Ringleader cannot work out a custom endpoint’s OpenAI-compatible address from its Anthropic one. |
If your gateway is LiteLLM or Bifrost, name it instead of using custom. Ringleader then
works out the OpenAI-compatible address as well as the Anthropic one, and configures Codex
too.
Where Codex is configured, both the Codex CLI in a terminal and the Codex extension in the
browser IDE use it. They read the same ~/.codex/config.toml, so there is no second setting
to write for the editor.
To choose the model Codex uses, set model under endpoint. It takes the gateway’s own name
for the model, and Ringleader writes it into Codex’s settings unchanged:
llm:
endpoint:
source: external
vendor: openrouter
model: anthropic/claude-sonnet-4.5
credentialRef:
name: openrouter
key: tokenmodel is accepted for openrouter, litellm and bifrost, and refused for custom, which
configures no Codex. Without it, OpenRouter uses ~openai/gpt-latest. Your organization’s
policy can limit which models you can name, and a model
outside that list is refused when you apply.
A self-hosted endpoint’s url is checked when you apply. It must be an http:// or
https:// URL, it may use only a limited set of characters, and it may not carry a user name
or password.
Your operator may limit which hosts and path prefixes you can name. A refusal names the field
and the reason.
If a vendor’s settings do not suit you, set the variables yourself in a WorkstationConfig. See Overriding an Integration.
Fields
| Field | Type | Description |
|---|---|---|
type | string | Required. llm or httpProxy. Each type takes its own block and refuses the other’s. |
selector.matchLabels | object | Which workstations the Integration reaches. An empty selector does not mean “every workstation”. See Who it reaches. |
selector.ownerScope | bool | true limits the Integration to workstations whose owner is its author. Default false. |
llm.endpoint.source | string | Required for type: llm. external: a gateway someone else runs. |
llm.endpoint.vendor | string | Required. openrouter, litellm, bifrost or custom. |
llm.endpoint.url | string | The endpoint’s address. Required for litellm, bifrost and custom. Refused for openrouter, which supplies its own. |
llm.endpoint.model | string | The model Codex uses, in the gateway’s own naming. Optional. Refused for custom. |
llm.endpoint.credentialRef | object | {name, key}: the Secret in this namespace that holds the gateway’s key. Optional, for a gateway that lets workstations in by their network address. |
httpProxy | object | Required for type: httpProxy, unless an injected mcp server supplies the only rule. The hosts to add a key to. See Credential injection. |
mcp | object | An MCP server to register on each workstation. See Registering an MCP server. |
The key is always a reference to a Secret, never a value in the YAML. So it gets the Secret’s
encryption, its access permission, and its redaction in diagnostics. You must be able to
read the Secret to apply an Integration that names it.
Status
| Field | Type | Description |
|---|---|---|
generatedConfig | string | The name of the WorkstationConfig this Integration generates. Empty when nothing was generated. |
reach | string | Which workstations this Integration reaches now: owner (only its author’s own workstations) or admin (every workstation its labels select). |
message | string | Why nothing was generated, when nothing was. Empty otherwise. |
injection | object | For type: httpProxy: which hosts are having a key added, and why the others are not. See Check what is being injected. |
How it reaches a workstation
Ringleader turns each Integration into an ordinary
WorkstationConfig named
integration-<name>. That config carries the endpoint’s environment, the tool settings and
the endpoint file described below. From there it works like any other
config: its selector picks the workstations, it merges with their other layers, and its
${secret:…} references are filled in on each workstation.
To read exactly what your workstations will get:
rl integration get llm -n acme -o yaml # status.generatedConfig names the config
rl workstationconfig get integration-llm -n acme -o yamlThe generated config also carries a workstationconfig.ringleader.dev/generated-from
annotation naming the <namespace>/<name> of its Integration. So a config you find in
rl workstationconfig get can always be traced back to the Integration that produced it.
Ringleader owns the generated config. An edit to it is overwritten, and deleting the Integration deletes it. Change the Integration instead.
Who it reaches
Who an Integration reaches depends on who applied it, the same way as for a WorkstationConfig:
- An ordinary member’s Integration reaches only the workstations that member owns. Its labels narrow that further. They never widen it to a colleague’s workstation, and a member whose labels match none of their own workstations reaches none.
- A namespace administrator’s Integration reaches every workstation in the namespace that its
labels match. An administrator who wants to limit it to their own workstations sets
selector.ownerScope: true. - An Integration with no
matchLabelsat all is a personal Integration when a member applies it: it reaches every workstation that member owns. It also changes how a failure is reported, as If the endpoint file cannot be written explains. An administrator’s Integration with nomatchLabelsreaches nothing, as an empty WorkstationConfig selector does. WithownerScope: trueadded, it reaches every workstation that administrator owns, as a member’s does.
Every workstation owner must be able to read the Secret
When an llm Integration reaches workstations you do not own, the gateway’s key travels to
each one inside its configuration. So each workstation’s owner must be able to read the
Secret: by owning it, through a role with the access verb, or through a Grant.
An owner who cannot read it sees two things, and neither says why. Their workstation is set
up with no endpoint, and their own stop, start or edit of it can be refused. Give each owner
access with rl secret share.
rl apply -f prints this when it writes an Integration that reaches workstations you do not
own:
Note: this integration reaches workstations you do not own. In this release, its gateway
credential travels to each workstation inside its configuration. Each workstation owner
must be able to read Secret openrouter. Access can come from ownership,
a role granting the access verb, or a Grant.
An owner who cannot sees two things, neither of which reports itself: their workstation
configures cleanly with no endpoint, and their own stop/start/edit on it can be refused.
Grant that access, per owner, with:
rl secret share openrouter -n acme --with <subject>The endpoint files
A program on a workstation finds the endpoints attached to it by reading
/etc/ringleader/integrations/. There is one JSON file, named after the Integration, for each
attached llm Integration and for each Integration that registers an MCP server. Read every
file in the directory, because there is no combined file. An httpProxy Integration with no
MCP server writes no file, because nothing on the workstation is set up for it.
$ ls /etc/ringleader/integrations/
llm.json
$ cat /etc/ringleader/integrations/llm.json
{
"schemaVersion": 1,
"name": "llm",
"namespace": "acme",
"type": "llm",
"vendor": "openrouter",
"url": "https://openrouter.ai/api",
"environment": [
"ANTHROPIC_API_KEY",
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_BASE_URL",
"OPENROUTER_API_KEY"
]
}| Key | Meaning |
|---|---|
schemaVersion | The file’s format. 1 today. A program that reads it should accept a value it does not know. |
name, namespace | The Integration this file describes. |
type | The Integration’s type, llm or httpProxy. |
vendor | For llm, the gateway you named (openrouter, litellm, bifrost or custom). Empty for httpProxy. |
url | For llm, the gateway’s base address. A named vendor reports its real address. A tool adds its own protocol path, such as /v1/messages for Anthropic’s protocol. |
environment | For llm, the names, sorted, of the environment variables this endpoint sets on the workstation. |
mcp | Present when the Integration registers an MCP server: its name, url and mode. |
environment lists names, never values, so the file holds no key. To get a value, read the
variable. The list is every variable the endpoint sets, not only the ones that hold a key. It
includes ANTHROPIC_BASE_URL, and for OpenRouter an ANTHROPIC_AUTH_TOKEN that is set to an
empty string on purpose, because Claude Code ignores the gateway when it is unset. To find the
variable that holds the key, go by vendor, not by a variable’s position in the list.
An endpoint with no credentialRef lists no key variables at all. They are left out, not set
to an empty string, because a tool treats an empty value differently from a missing one.
The files are mode 0640, owned by root and by the login user’s primary group. The login
user and root can read them, and no other account on the workstation can.
When an Integration stops applying
An Integration stops applying to a workstation when it is deleted, when its selector changes,
or when the workstation’s labels change. The next configuration pass then removes everything
the Integration wrote: its environment variables, its endpoint file, and the tool settings it
produced. A ~/.codex/config.toml that Ringleader wrote in full is deleted. From a file that
Codex also writes, only Ringleader’s keys are removed.
A configuration pass that could not assemble every layer removes nothing, whatever the reason. So a leftover file on a workstation that otherwise reports healthy means the last pass was incomplete. The next complete pass removes it.
Two more things to plan for:
- A shell that was already open keeps the environment it started with. Check in a new shell.
- The endpoint file shows the configuration at the time it was written. It does not say whether the gateway is up or the key still works. To find out, call the endpoint.
# what the workstation says is attached
for f in /etc/ringleader/integrations/*.json; do
jq -r '.name + " " + (.environment // [] | join(" "))' "$f"
doneIf the endpoint file cannot be written
What happens depends on whether the Integration is personal.
A personal Integration has no matchLabels and reaches only its author’s own
workstations: a member’s Integration with no labels, or an administrator’s with
ownerScope: true and no labels. If its endpoint file cannot be written, the workstation
still reports Configured. The failure shows in the Optional configuration section of
rl workstation describe, and rl troubleshoot shows the failing step and its output.
For every other Integration, writing the endpoint file is an ordinary configuration step. That
includes the example at the top of this page, an administrator’s Integration, and a member’s
Integration with matchLabels, with or without ownerScope. If the file cannot be written,
that workstation reports Configured: False until the cause is fixed. Other workstations are
not affected, because each one applies its own configuration.
Registering an MCP server
An mcp block registers one MCP server with Claude Code and Codex on every workstation the
Integration reaches. Claude Code gets it at user scope, the same way
claude mcp add --scope user adds one. Codex gets an [mcp_servers.<name>] table in
~/.codex/config.toml. A server of the same name that you declare yourself, in a
tool configuration, is used instead of
this one.
The server’s key reaches it in one of two ways, chosen with mode:
injected, the default, keeps the key off the workstation. The key is added to each request as it leaves, the same way a credential injection rule adds it, so the Integration must betype: httpProxy. It works on cloud workstations, and on a local workstation the Integration’s author owns if its provider adds a key. A wsl2 workstation gets no key, whoever owns it. On any other local workstation, requests to the server go out without the key.localputs the key on the workstation, in each tool’s own settings. It works on every provider, with either type. The Secret’s whole value becomes the header, so store any prefix such asBearerinside the Secret.
Ringleader adds an Integration endpoint, an injected request host, and an MCP server’s host to
the attached workstation’s compiled egress policy. This keeps the service reachable without
asking every workstation author to repeat it. An inspect: true rule does not add reach: it
checks a host only when that workstation’s egress policy already permits it.
This local example adds a Linear MCP server beside an LLM gateway:
apiVersion: workstations.ringleader.dev/v1
kind: Integration
metadata:
name: llm
namespace: acme
spec:
type: llm
selector:
matchLabels:
team: platform
llm:
endpoint:
source: external
vendor: openrouter
credentialRef:
name: openrouter
key: token
mcp:
name: linear
url: https://mcp.linear.app/mcp
mode: local
header: Authorization
credentialRef:
name: linear
key: tokenThe Credential injection
page has an injected example.
| Field | Description |
|---|---|
mcp.name | Required. The server’s name in Claude Code and Codex. |
mcp.url | Required. The server’s http:// or https:// address, with no user name or password in it. Your operator may limit which hosts you can name. |
mcp.mode | injected (the default) or local. |
mcp.inject | {header, value}, as on a credential injection rule. Required in injected mode, and refused in local mode. |
mcp.header, mcp.credentialRef | The header to send, and the Secret {name, key} whose value it carries. Required in local mode, and refused in injected mode. |
Before you attach an MCP server, check two things about the workstations it reaches:
- Claude Code must be installed. Ringleader registers the server with the
claudecommand, and on a workstation without it that step fails. Declare theclaude-codedevtool in the same layers. - Removing the server takes it out of Codex. It stays in Claude Code when it was the last
server Ringleader registered there. To remove it from Claude Code, run
claude mcp remove --scope user <name>on the workstation.
In local mode, every owner of a workstation the Integration reaches must be able to read
the Secret, as for an llm key.
Overriding an Integration
The generated config has a negative priority, so it loses to every WorkstationConfig a person writes. It does not lose to the settings in your own workstation’s spec.
Ringleader starts from the workstation’s own spec, then applies each config over it in
ascending priority order. A config at any priority, a negative one included, therefore wins on
environment and toolconfigs. So setting ANTHROPIC_BASE_URL in your own workstation’s
spec does not override an administrator’s Integration:
# does NOT work: the Integration's config is applied over the workstation's own spec
kind: Workstation
spec:
environment:
ANTHROPIC_BASE_URL: https://my-own-gateway.example.comTo override it, write a WorkstationConfig of your own. Any priority of 0 or more puts it above the generated config:
# works: your own config outranks the generated one
apiVersion: workstations.ringleader.dev/v1
kind: WorkstationConfig
metadata:
name: my-llm-override
namespace: acme
spec:
selector:
ownerScope: true
environment:
ANTHROPIC_BASE_URL: https://my-own-gateway.example.com
ANTHROPIC_API_KEY: ${secret:my-gateway/token}Then check what the workstation actually gets:
rl workstation get-resolved-configuration my-box -n acmeOnly image, defaultLocalBinding and tailscale work the other way. Ringleader applies
the workstation’s own value for them after the configs, so the workstation’s value wins. For
every other field that takes a single value, the config wins. Placement is not merged at all.
requirements and provider are read from the workstation alone and ignored on a config, and
a WorkstationConfig that sets providerConfig is refused.
See also
- WorkstationConfig: the configs an Integration becomes, and how they merge.
- Credential injection:
type: httpProxy, adding a key to requests as they leave the workstation. - Workstation: the machine an Integration reaches, and its own
spec.environment. - Access control: ownership, the
accessverb on Secrets, and Grants. rl secret share: giving an owner access to the gateway’s key.- Claude Code tutorial: setting up the tool itself on a workstation.