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: Integration

An Integration has one of two types:

  • type: llm points the AI coding tools on each workstation, Claude Code and Codex, at an LLM gateway. This page describes it.
  • type: httpProxy adds 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: token

You 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:

vendorEndpointNotes
openrouterSupplied by Ringleaderurl must not be set. Configures Claude Code and every other tool that speaks Anthropic’s protocol, and Codex.
litellmYours (url required)A self-hosted LiteLLM proxy. Both protocols are served under one address, so both Claude Code and Codex are configured.
bifrostYours (url required)A self-hosted Bifrost gateway. Both protocols are served under one address, so both Claude Code and Codex are configured.
customYours (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: token

model 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

FieldTypeDescription
typestringRequired. llm or httpProxy. Each type takes its own block and refuses the other’s.
selector.matchLabelsobjectWhich workstations the Integration reaches. An empty selector does not mean “every workstation”. See Who it reaches.
selector.ownerScopebooltrue limits the Integration to workstations whose owner is its author. Default false.
llm.endpoint.sourcestringRequired for type: llm. external: a gateway someone else runs.
llm.endpoint.vendorstringRequired. openrouter, litellm, bifrost or custom.
llm.endpoint.urlstringThe endpoint’s address. Required for litellm, bifrost and custom. Refused for openrouter, which supplies its own.
llm.endpoint.modelstringThe model Codex uses, in the gateway’s own naming. Optional. Refused for custom.
llm.endpoint.credentialRefobject{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.
httpProxyobjectRequired for type: httpProxy, unless an injected mcp server supplies the only rule. The hosts to add a key to. See Credential injection.
mcpobjectAn 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

FieldTypeDescription
generatedConfigstringThe name of the WorkstationConfig this Integration generates. Empty when nothing was generated.
reachstringWhich workstations this Integration reaches now: owner (only its author’s own workstations) or admin (every workstation its labels select).
messagestringWhy nothing was generated, when nothing was. Empty otherwise.
injectionobjectFor 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 yaml

The 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 matchLabels at 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 no matchLabels reaches nothing, as an empty WorkstationConfig selector does. With ownerScope: true added, 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"
  ]
}
KeyMeaning
schemaVersionThe file’s format. 1 today. A program that reads it should accept a value it does not know.
name, namespaceThe Integration this file describes.
typeThe Integration’s type, llm or httpProxy.
vendorFor llm, the gateway you named (openrouter, litellm, bifrost or custom). Empty for httpProxy.
urlFor 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.
environmentFor llm, the names, sorted, of the environment variables this endpoint sets on the workstation.
mcpPresent 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"
done

If 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 be type: 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.
  • local puts 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 as Bearer inside 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: token

The Credential injection page has an injected example.

FieldDescription
mcp.nameRequired. The server’s name in Claude Code and Codex.
mcp.urlRequired. 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.modeinjected (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.credentialRefThe 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 claude command, and on a workstation without it that step fails. Declare the claude-code devtool 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.com

To 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 acme

Only 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