Adapters¶
An adapter is what runs an agent's "thinking" step. This page covers the adapter types, when to pick each, and the operator‑side configuration that goes with them.
For the conceptual background, see ../concepts/the-mental-model.md and ../board/configuring-an-agent.md (the board user's view of changing adapters). For the risk profile of each adapter (what it can read, where it runs, network reach), read adapter-isolation.md alongside this page.
How an adapter is selected¶
For each heartbeat run, the engine picks the adapter in this order
(engine.ExecuteHeartbeatRun):
- The
defaultsentinel resolves first. Ifadapter_typeis the literaldefault, the engine swaps it for the company'sagent_default_adapter_type(and adopts the company'sagent_default_adapter_configwhen the agent's own config is empty) before any of the steps below. This happens server‑side, so aremoteworker never seesdefault. See thedefaultsentinel below. - Provider binding wins. If the agent has a
provider_idset, the engine dispatches through that company provider (enforcing the enabled flag and a cross‑company guard) — this is thellmpath. - Else the
adapter_typeif it is one of the seven registry types below. - Else the default
llmadapter. An emptyadapter_typeresolves tollm. If no provider is configured at all, the run fails.
Adapter types at a glance¶
The factory registry holds seven types; llm is the implicit default
when adapter_type is empty.
| Adapter | Runs | When to pick it |
|---|---|---|
llm (default) |
An HTTP call to a provider (Anthropic, OpenAI, Perplexity, OpenAI‑compatible, Bedrock). The engine renders the full heartbeat prompt (issue + timeline + roster + manager) before the call. | Default. Simplest, most agents. |
claude_local |
The Claude Code CLI (claude) as a host subprocess. Session ID persisted across heartbeats. |
Filesystem/shell/coding agents on a host with ~/.claude auth. Trusted single‑tenant only. |
codex_local |
The OpenAI Codex CLI (codex exec) as a host subprocess. Distinct JSONL stream; same session model. |
Same as claude_local with GPT‑style models; needs ~/.codex auth. |
claude_sandbox |
The same claude CLI inside a hardened Docker container on the egress‑allowlist network. |
Production‑safe local‑CLI execution; preferred over claude_local for multi‑tenant. |
codex_sandbox |
The same codex CLI inside a hardened Docker container. |
Production‑safe Codex execution; preferred over codex_local. |
docker |
A container per heartbeat with the prompt + context piped in. | Custom runtime images, per‑agent isolation, third‑party tools. |
lambda |
An HTTP POST to AWS Lambda or any HTTP endpoint that speaks the payload contract. | Agent logic hosted elsewhere — Function URLs, Cloud Run, self‑hosted inference. |
remote |
Enqueues a task and polls until a registered trimus-worker (or a custom worker per REMOTE_WORKERS.md) claims, runs, and posts the result back. |
Run execution on a different host — e.g. server in Docker but local‑CLI agents need a host's ~/.claude / ~/.codex. |
default (sentinel) |
Not a runtime — resolved at dispatch to the company's agent_default_adapter_type. |
Portable agents and exported templates that should follow the receiving instance's posture instead of hardcoding one adapter. |
You can mix adapters within a company — an llm triage agent routing
work to a claude_sandbox engineer, or a remote agent dispatching to
a worker host that runs Claude locally there.
The default sentinel¶
default is not a real adapter — it is a dispatch‑time pointer to
whatever the company is configured to use. Setting an agent's
adapter_type to default means "run me with this company's
agent_default_adapter_type," resolved fresh on every heartbeat:
- The concrete type is read from the company default; if the agent's
own
adapter_configis empty ({}), the company'sagent_default_adapter_configis adopted too (a non‑empty agent config is never overwritten). - Resolution is server‑side and one‑shot: the concrete adapter is
used for both local factory dispatch and any
remotepayload, so a worker never receives the literaldefault. - A company default of
defaultis rejected (it would resolve to itself); an empty or somehow‑still‑defaultvalue degrades tollm.
This is what makes agents and exported template/portability bundles
portable: a bundle authored on a hosted instance (remote) imports
cleanly into a local instance and follows its default instead of
forcing the original adapter. The built‑in onboarding templates ship
their agents as default for exactly this reason.
llm adapter¶
The standard path. Reads the agent's adapter_config JSONB:
{
"provider": "anthropic",
"model": "claude-opus-4-6",
"api_key": "(populated from the provider, NOT pasted directly here)",
"base_url": "(optional override)",
"max_tokens": 4096,
"temperature": 0.7
}
In practice, you don't fill this in by hand — the Configuration → Provider form on the agent page builds it for you. Read this only if you're curious or scripting agent setup via the JSON API.
Operator-side concerns:
- Add the matching provider first (providers.md).
- Heartbeats hit the vendor on every run. Costs scale linearly with run count.
claude_local adapter¶
Spawns the Claude Code CLI (claude by default) on the same host
that runs trimus-server. Each heartbeat is a separate CLI
invocation, with session-id persistence across runs so the
conversation continues.
Required on the host¶
- The
claudebinary on PATH (or specify a full path in the agent's adapter config). - Filesystem permissions for the OS user running
trimus-serverto read and write the agent's working directory.
Working directory resolution¶
Per heartbeat, the CLI is spawned with a specific CWD. Order:
project.working_dir— if the issue is attached to a project with a working_dir set, that's the CWD. Operator-managed.<agent-home>/work/— fallback if no project working_dir. Auto-created.
This is what lets you point a project at /repos/myproject and
have every claude_local agent working on issues in that project run
inside the repo.
Session persistence¶
The Claude session ID is saved in postgres after each run. The next run resumes the session, so the conversation has continuity. Side effects:
- A model upgrade requires a session reset. The CLI rejects a resume if the model changed. Click Clear Session in the agent header.
- A stale session can produce confused output. Same fix.
Agent configuration knobs¶
On the agent's Configuration tab → Heartbeat Config (Claude):
| Field | Notes |
|---|---|
| Claude binary path | claude if on PATH, otherwise /full/path/to/claude. |
| Claude flags | Extra CLI flags. Common: --allowed-tools to restrict tool set, --add-dir to surface extra directories. |
| System prompt | The agent's role prompt. |
| Heartbeat interval | Seconds between ticks. |
Operator gotchas¶
- Multi-host installs: each Trimus instance must have its own
claudebinary. If you're running trimus-server on multiple boxes with the same database, the box that picks up a run is the one that needs the CLI. For deterministic placement, use remote workers (remote-workers.md) with tags. - Auth: Claude Code uses its own auth (API key or subscription).
Not configured by Trimus. Whoever runs the CLI must be logged in.
Most teams set
ANTHROPIC_API_KEYin the trimus-server's environment, which the CLI picks up. - Concurrency: each heartbeat spawns a CLI process. Multiple in-flight claude_local agents = multiple subprocesses. Plan host CPU/RAM accordingly.
- Output capture: the CLI's stdout is streamed into the run's event log and rendered as the issue's Live Transcript. If your CLI version produces unexpected output formats, you'll see them raw — file an issue.
docker adapter¶
Runs each heartbeat inside a container. The container gets the agent's prompt + issue context on stdin and produces a reply on stdout.
Required on the host¶
- Docker daemon reachable from
trimus-server. - The image you reference is pullable (or pre-pulled).
Agent configuration¶
On the agent's Configuration → Heartbeat Config (Docker):
| Field | Notes |
|---|---|
| Image | myorg/my-agent:latest or whatever. |
| Pull policy | always / if-not-present / never. if-not-present is sane. |
| Mount points | Host:container path pairs. Common: mount the agent's home so the container can read skills. |
| Heartbeat interval | Seconds. |
Use cases¶
- Pre-built agent images that bundle their own LLM client + tools.
- Tight isolation: the container can't read other agents' files.
- Testing custom agent logic without affecting the main process.
Gotchas¶
- Image bloat. Each agent's image is pulled independently. Share base layers ruthlessly.
- Networking. The container needs egress to its LLM provider.
Default
DockerConfignetwork isnone; pointNetworkat thetrimus-egress-allowlistnetwork for SSRF‑safe egress.
Hardening¶
The docker adapter applies these by default (see
adapter-isolation.md for the full contract):
--read-only root + writable /tmp tmpfs, --cap-drop ALL,
--security-opt no-new-privileges, --pids-limit (default 100, via
TRIMUS_DOCKER_PIDS_LIMIT or per‑agent pids_limit), and
--memory-swap pinned to --memory. Ship a seccomp/AppArmor profile
with TRIMUS_DOCKER_SECURITY_OPTS=seccomp=/etc/trimus/seccomp.json.
claude_sandbox and codex_sandbox adapters¶
These run the same claude / codex CLIs as the *_local
adapters, but inside a hardened Docker container on the
trimus-egress-allowlist network instead of as a bare host
subprocess. They are the production‑safe way to use the local CLIs:
they keep session persistence and CWD‑aware resume, but the agent can
no longer read the host filesystem or inherit DATABASE_URL /
TRIMUS_ENCRYPTION_KEY from the host environment.
Required on the host¶
- A Docker daemon reachable from
trimus-server. - The
trimus-egress-allowlistnetwork (created automatically at boot). - A container image that carries the relevant CLI.
Hardening¶
Container root is read‑only with a writable /tmp, host env is not
inherited (only the curated provider/context vars), --pids-limit is
governed by TRIMUS_SANDBOX_PIDS_LIMIT (default 100), and all the
Track‑B flags (cap‑drop, no‑new‑privileges, memory‑swap parity) apply.
With the egress DNS resolver enabled (TRIMUS_EGRESS_DNS_ADDR), the
container resolves only allow‑listed hostnames; everything else is
NXDOMAIN and recorded for review. See
adapter-isolation.md.
Prefer the sandbox adapters in production
For any multi‑tenant or internet‑exposed deployment, choose
claude_sandbox / codex_sandbox over claude_local /
codex_local. The behaviour is the same; the blast radius of a
prompt‑injection or CLI bug is far smaller.
lambda adapter¶
POSTs the heartbeat payload to an HTTP endpoint and parses the response. The endpoint can be AWS Lambda, Cloud Run, a custom internal service — anything that speaks the protocol.
Agent configuration¶
On the agent's Configuration → Heartbeat Config (Lambda):
| Field | Notes |
|---|---|
| Endpoint URL | https://lambda.example.com/trimus-agent. Where heartbeats POST. |
| Auth header | Typically Authorization: Bearer <token> so the endpoint can verify the calling Trimus instance. |
Protocol¶
POST body: JSON with agent_id, issue (title/body/status/etc.),
context (recent comments, activity), and prompt (the assembled
system prompt).
Response body: JSON with reply_text (becomes the comment / parsed
for actions). See internal/engine/adapters/lambda.go for the exact
schema.
Use cases¶
- Bedrock / Vertex agents that ride on those platforms' SDKs.
- Custom agents written in non-Go languages.
- Federated setups: agents running in a different security domain than Trimus itself.
codex_local adapter¶
Added in v2.7.0. Sibling of claude_local for the OpenAI Codex CLI.
Spawns codex exec as a subprocess and parses its JSONL output stream.
Session IDs are persisted across heartbeats the same way Claude's are.
{
"adapter_type": "codex_local",
"adapter_config": {
"model": "gpt-5-codex",
"sandbox": "workspace-write",
"dangerously_bypass_approvals_and_sandbox": true,
"timeout_sec": 600,
"max_turns_per_run": 10
}
}
| Field | Default | Description |
|---|---|---|
model |
(CLI default) | Codex model slug (e.g. gpt-5-codex). |
sandbox |
workspace-write |
One of read-only, workspace-write, danger-full-access. |
dangerously_bypass_approvals_and_sandbox |
true |
Skip per-command approval prompts (parity with claude_local's dangerously_skip_permissions). |
timeout_sec |
300 |
Hard wall-clock cap per heartbeat. |
max_turns_per_run |
10 |
Advisory — Codex itself doesn't expose a native max-turns flag, but Trimus records it for parity. |
Same operator setup story as claude_local: the user that runs the
Trimus service needs a working codex CLI on PATH and a valid
~/.codex/ auth directory.
remote adapter¶
Dispatches the heartbeat to an external worker process that connects
back to Trimus over the documented worker protocol. The server queues
a row in remote_tasks; the worker polls
POST /api/workers/tasks/claim, runs the agent locally with whatever
local adapter it's configured for, and POSTs the result back.
{
"adapter_type": "remote",
"adapter_config": {
"local_adapter_type": "claude_local",
"tags": ["darwin", "claude_local"],
"timeout_seconds": 600
}
}
| Field | Default | Description |
|---|---|---|
local_adapter_type |
llm |
What adapter the worker runs internally on the task. |
tags |
[] |
Worker must possess ALL these tags to claim the task. Empty means any worker. |
timeout_seconds |
600 |
How long the server waits before giving up. |
The full wire protocol is at
REMOTE_WORKERS.md. The canonical
worker contract is served at GET /api/workers/operating-prompt
and rendered on the /admin/workers UI. An end-to-end smoke-test
script with curl + jq lives at
remote-worker-smoke-test.md.
Choosing per-agent¶
Mix freely. Typical company shape:
- Triage agent →
llm(cheap, fast, no filesystem needed). - Engineer agents →
claude_local(filesystem + shell). - Researcher agents →
llmwith a Perplexity provider. - Designer agents →
llm, expensive model. - CEO agent →
llm, expensive model, low heartbeat frequency.
The adapter is a per-agent property, not company-wide.
Switching an agent's adapter¶
Agent detail page → Properties → Adapter type. Pick the new
type, save, then fill out the new adapter's config on the
Configuration tab. Switching clears parts of the existing
config that don't apply (e.g., switching llm → claude_local
nulls out the provider/model fields).
The agent's run history is preserved — heartbeat_runs records
the adapter that ran each turn, so swapping doesn't make old
runs ambiguous.
Where to next¶
- providers.md — for
llm-typed agents, configure their upstream vendor here. - remote-workers.md — when you want
per-adapter workers on different hosts (e.g., GPU box for
docker-running agents). - ../board/configuring-an-agent.md — the board-side per-agent configuration UI.