Routing and credentials
The code declares what a task is and its default budget. The TOML file decides
where it runs: coding agent, model, effort, credential, failover chain, and
budgets that outrank the declared ones. The demo’s, whose agent stub is the demo’s
own Backend that examples/demo/run.py registers (contraagent check ... --stub
checks a config that names it):
# Operations, not definitions: which coding agent and model each kind runs on, and# where it goes on a 429. Budgets declared in tasks.py are defaults this file outranks.# `stub` is the demo's own coding agent (`DemoAgent` in run.py, which registers it);# `contraagent check tasks.py:docs --config pipeline.toml --stub` checks this file.
[defaults]agent = "stub"effort = "medium"
[tasks.summarize]model = "primary-model"
[[tasks.summarize.fallbacks]] # block order is hop orderagent = "stub"model = "fallback-model"Precedence
Section titled “Precedence”Highest first: overrides (Pipeline(overrides=...), e.g. from command-line
flags), the kind’s [tasks.<kind>] table, agent_task(...), [defaults], the
built-in default. A credential has one more place, the task call (below): it sits
under the overrides and over the kind’s table.
[defaults] takes the routing keys (agent, model, effort, credential,
model_failover, fallbacks), the run’s own settings (max_passes, preflight,
claim_capacity) and the default of every kind that gives none (context_warn_pct),
nothing else. A budget (max_turns, timeout_s, max_budget_usd,
max_corrections, …) is a kind’s: it goes in [tasks.<kind>] or agent_task(...),
and [defaults] refuses it.
Refused before anything launches: a kind with no turn or time budget anywhere,
an effort the coding agent’s CLI does not accept, an unknown agent in a chain,
and any credential problem (below). A [tasks.<name>] table that names no task
of the flow is reported, not fatal: one config file often serves several flows,
but it may be a typo.
A pipeline keeps its own settings in the same file, each in a pipeline table
[pipeline.<name>]. Flow code and Python tasks read one of the run’s config (or of
the config a check is given) with
contraagent.pipeline_table(name) (tasks), other code with
PipelineConfig.table(name); either is an empty table when the file has none.
contraagent never checks the keys inside; a pipeline.<name> that is not a table is
refused. Any other top-level table is refused before
anything launches.
Budgets
Section titled “Budgets”max_turns and timeout_s are required for every kind. max_budget_usd caps
a launch’s cost as the coding agent reports it: Claude Code enforces it itself;
on omp, opencode and pi contraagent sums the cost the agent reports for each API
call and ends the launch (budget) once it passes the cap; Codex reports no
cost, so the cap does not hold there. On Codex max_turns counts tool calls, and
omp and pi do not enforce it. Coding agents has each agent’s budgets
in one table. Every key: the reference at the end.
context_budget caps a launch’s peak context: the whole input (fresh input,
cache read and cache write) of its largest API call. Claude Code and opencode
launches end context_budget once one call passes it; omp, pi and Codex report
each call’s tokens but are not stopped. context_warn_pct (1 to 99, in a task
table, agent_task(...) or [defaults]) is a percent of context_budget and
stops nothing: an attempt whose peak context passed it is recorded
(context_warned in the journal, beside context_budget and
usage.peak_context) and logged ([context] <key>: peak context ...). The
journal’s usage.context_window is the window of the model the peak context was
on, as the coding agent reports it: in its stream (Claude Code, Codex, omp RPC) or,
when the stream gives none, in its model catalogue for the model the launch ran on
(omp -p, pi, opencode; Backend.context_window). launches.jsonl holds it and
context_pct, the peak context as a percent of it, both null when the coding agent
reports no window. A kind with
a context_warn_pct but no context_budget is refused before anything launches;
[defaults].context_warn_pct applies to every kind with a context_budget and
none of its own.
max_corrections (0 to 20, default 3, in a task table or agent_task(...)) caps the correction rounds of a kind: how many times an output that
fails its checks goes back to the same session (contracts).
A value out of that range is refused before anything launches.
keep_failed_changes = true (in a task table, which outranks agent_task(...)) leaves
the files a failed attempt created under the kind’s changes patterns in place, for
the next attempt to start from
(Files a task changes). A kind with no changes
patterns cannot keep them, and contraagent check reports a table that sets it.
max_concurrent (in a task table or agent_task(...)) is how many tasks of a kind
run at once. The run’s own limits sit beside it, and
Concurrency says how they work
together: the run’s slots, a kind’s limit and the claims, and how a claim keeps long
Python tasks from taking the agent tasks’ slots.
disallowed_tools (in a task table or agent_task(...)) lists tools a kind’s
launches may not use, in Claude Code’s names: a whole tool (WebFetch) or a
tool with a pattern of what it is given (Bash(rm:*)). It narrows what the
role’s tools: allow. Claude Code denies both (--disallowedTools); opencode
whole tools and a pattern of a command (Bash(git push:*)), a path (Read(secrets/**),
Edit(...)), a domain (WebFetch(domain:example.com)) or a skill (Skill(deploy)),
each after the rule the tool had, so the rest of the tool stays allowed; a pattern of
another tool there is refused; omp, in an extension of contraagent’s own that sees
each call before it runs, whole tools and a pattern of a command, a path read or
written (Read, Write, Edit, MultiEdit; an Edit pattern also denies omp’s
ast_edit whole), a domain or a skill, so a pattern of another tool there is refused; pi whole tools (--exclude-tools), so a pattern there is refused;
Codex has no way to deny a tool. A route that cannot deny what the task names is
refused before anything launches (Backend.tool_deny_problem), failover hops
included (Coding agents).
Failover
Section titled “Failover”Each kind gets its own failover breaker: on a 429 the task walks its chain of
(agent, model, credential) hops, and a half-open probe returns to the primary
once the provider’s stated cool-off has passed. Until then every task of that
kind starts on the hop, not only the one that was rate limited (the log says so:
[route] <key>: starting on hop 2/2 ...).
Any other failure of a launch (an error, a refusal, a timeout, an output still
off-contract after its correction rounds, …) moves only that task to the next hop; the
next task still starts on the primary. Two failures do not move it: an input
that is off-contract (nothing was launched) and a launch that ran as another
identity than its credential (credential_mismatch). A launch ended by a signal
(the run is stopping) does not either.
An override of the agent or the model (contraagent run --agent omp) runs every
kind there and nowhere else: no kind has failover hops. An override of the
credential (Overrides(credential=...)) is every hop’s credential as well.
Preflight
Section titled “Preflight”The preflight probes every distinct route of the run’s reachable kinds once,
before the first task: a model its coding agent does not list is dropped
without a launch, and every other route gets one minimal launch (max_turns=1);
a route whose probe does not complete (a 429, an error, a timeout) is dropped.
Past the first 8 routes the rest are kept unprobed. Each failover chain is rebuilt on the routes left. The preflight policy says what
happens to a kind with no route left:
| Policy | A kind with no route left |
|---|---|
substitute |
runs on the first route that answered, when that route runs as the kind’s own credential; else it runs as configured |
stop |
the run raises PreflightStopped, naming each such kind and why each of its routes was dropped; nothing launches |
off |
nothing is probed (the default) |
Pipeline(preflight="stop"), contraagent run --preflight stop, or
preflight = "stop" in [defaults]; the argument and the flag outrank the
config. run.json records what the preflight did (preflight: the policy, the
routes dropped and the kinds that held them, each kind moved from one route to
another, each kind left as configured and why, and the kinds that stopped the
run), and the monitor returns it with the run’s plan (Plan.preflight).
Credentials
Section titled “Credentials”Three questions, answered in three places:
| Question | Answered by |
|---|---|
| What may this task’s data be sent under? | the task, in code: the traits it requires (e.g. zdr) |
| Which identities exist, and what are they? | the config: [credentials.<name>], each with a type and its traits |
| Which identity does each task use? | the config: credential in [defaults], [tasks.<kind>] and each failover hop; for one task, its call |
With no credential configured, every task runs as ambient: the host’s own
setup, untouched. ambient carries no traits, so a task that requires any never
runs on it. Traits are free-form labels; what they promise (zero data
retention, company billing) is for whoever writes the config to know. Secrets
are references (env:NAME, file:/path) read in the launching process and
never written anywhere.
Each coding agent says which credential types it can use:
| type | claude | omp | opencode | pi | codex |
|---|---|---|---|---|---|
subscription |
the /login in a CLAUDE_CONFIG_DIR |
the login in an omp profile; a ChatGPT login (provider = "openai") runs openai-codex/<model>; an OpenCode one is OPENCODE_API_KEY, which omp also reads from its .env files |
the provider’s stored login (opencode providers login) |
the /login in a pi agent directory (config_dir, by default ~/.pi/agent); for opencode and opencode-go with no such login there, opencode’s stored login |
the ChatGPT login in a CODEX_HOME (provider = "openai") |
api_key |
Anthropic | any provider (<PROVIDER>_API_KEY) |
any provider | any provider (<PROVIDER>_API_KEY) |
OpenAI (CODEX_API_KEY) |
oauth_token |
CLAUDE_CODE_OAUTH_TOKEN |
Anthropic | — | Anthropic | — |
gateway |
an Anthropic-API endpoint (bearer or API key, custom headers) | a provider endpoint (litellm by default) |
a provider endpoint: a provider opencode knows (anthropic, openai, …) keeps its own API, another name is OpenAI-compatible |
an OpenAI-compatible endpoint (litellm by default, custom headers) |
a Responses-API endpoint (bearer or API key, custom headers) |
workload_identity, bedrock, vertex |
yes | — | — | — | — |
pool |
several of the above used as one (in_order, round_robin) |
the same | the same | the same | the same |
A gateway is your own endpoint: LiteLLM, a company proxy, a self-hosted
API. Claude Code speaks the Anthropic Messages API to it; omp, opencode and pi
address it as a provider, so its models are <provider>/<model>
(litellm/<model>; on opencode, anthropic/<model> with provider = "anthropic" and a base_url ending in /v1). pi speaks the OpenAI
chat-completions API to base_url, and its token stays in the environment, not
in the models.json that names the provider. Codex speaks the OpenAI Responses
API to it (<base_url>/responses, so base_url ends in /v1), as a model
provider named after provider; its models are the gateway’s own names.
A pool uses several credentials as one. in_order uses the first member
until it answers 429, then the next; round_robin turns to the next member on
every launch. A member that answers 429 rests on that model for the provider’s
stated retry-after, and the task goes to the next member at once; with every
member resting, the route’s own failover takes over. A pool carries only the
traits every member carries.
A task call’s credential
Section titled “A task call’s credential”A kind runs as one credential, but the data of one unit may have to go under another
account. The task call names it, as the name of a [credentials.<name>]:
#: The documents whose data may go only under the company's account.RESTRICTED = {"contract"}
def summarize_all(documents: list[str]): """Summarize every document at once.""" for document in documents: summarize(document, credential="company" if document in RESTRICTED else None)#:schema ../../schemas/pipeline.schema.json# One kind, two accounts. The kind runs as `team`; a task call that names# `company` (flow.py) runs as `company` on the primary and on the failover hop.
[tasks.summarize]agent = "claude"model = "claude-opus-5-5"credential = "team"max_turns = 20timeout_s = 600
# A 429 moves the task to this hop, as the credential its call named, else as `team`.[[tasks.summarize.fallbacks]]agent = "claude"model = "claude-sonnet-5-5"
[credentials.team]type = "api_key"provider = "anthropic"key = "env:TEAM_ANTHROPIC_API_KEY"
[credentials.company]type = "api_key"provider = "anthropic"key = "env:COMPANY_ANTHROPIC_API_KEY"The rule is that the narrower choice outranks the wider one, and an override
outranks them all. The config outranks the declaration because a declaration is the
default of a whole kind. The config has no table for one unit, so the task call is
the only place that can name a credential for it, and it outranks the kind’s
[tasks.<kind>] table, agent_task(...) and [defaults]. An override
(Overrides(credential=...)) is the choice of the person who runs the pipeline and
outranks the call too: the task runs as the override’s credential, and the log says so.
The call’s credential is the credential of every route of the kind: the primary and each failover hop, whatever credential the hop names. A 429 or a refusal moves the task to the next hop as the same credential, never to another identity. Two hops that become the same route are one hop. The tasks of one kind and one call credential share a failover breaker of their own: a 429 as that credential does not move the kind’s other tasks, and a 429 of theirs does not move these.
A credential that does not exist, that does not carry a trait the task requires, or
that the coding agent of one of the routes cannot use with its model, is refused before
the task launches: by contraagent check for the tasks the flow creates before its
first wait, by Pipeline.run before anything launches for those same tasks, and at the
task call for a task a generator flow creates later (a ValueError in that flow call).
The preflight does not probe a route as a call’s credential.
A call’s credential that rests on its quota is passed over like any resting credential: every hop is on it, so the task is deferred, not failed.
The [route] line of a kind names the kind’s credential. contraagent check adds the
tasks whose calls name another one to their kind’s line (credential=company for summarize-beta, by their task calls), and a run’s [start] line names the call’s
credential ([start] summarize-beta credential=company). With an override, neither
does, since the override’s credential is in effect.
A credential is who the work is billed to, not what the work is: it is not part of the
fingerprint, and a resumed run that names another credential for a call skips the task
it already finished. plan.jsonl records the call’s credential (credential) when it is
in effect, and the journal records the credential each launch ran as, as for every task.
Quota caps
Section titled “Quota caps”A subscription’s quota (its 5-hour and weekly usage) belongs to the account,
so its caps are the credential’s: max_five_hour_pct and max_seven_day_pct
in its [credentials.<name>] table (on a pool, the cap of each member that
sets none). Pipeline(quota_rule=fn) adds a test of your own: fn(credential, reading) returns why the credential rests now, or None, from its latest reading
(QuotaReading); or the reason and when the rest ends, (reason, datetime) (a time
with no zone is UTC).
A credential past a cap or the rule rests until the window its reading
names resets. Nothing launches as it meanwhile: a pool passes over the member,
and a task skips the hop without launching it and takes the next hop of its
failover chain (a gateway, an API key), the reason in its journal row
(personal rests: 5-hour usage 93%, max 90%; ran on hop 2/2 ...) and the hop
it passed over in skipped_hops. A task with no hop left is deferred at once:
it spent nothing and did not fail (RunReport.deferred, not failed), a task
that reads its output is deferred with it, and deferred.jsonl says why, as does the
monitor (“not run” and the cause). A task of finishing_kinds still launches
on a resting credential for grace_s after the rest began, as it does past a
deadline.
A rest by the rule lasts until the end the rule gave with its reason, and the rule
is not asked again before it: a daily budget rests to the next day’s start, a weekly
one to the weekly window’s reset. A rule that gives a reason alone rests the
credential while the reading it judged stands: until the earliest reset among that
reading’s windows (a window that names no reset stands for its length after the
reading arrived). Nothing launches on a resting credential, so no
newer reading comes; after that time the stale reading no longer counts, the
credential does not rest, and the next launch brings a reading for the rule to
judge again. The rest records the reading it judged (reading: when it arrived,
each window’s usage and reset) and its end (until).
run.json records the run’s caps (quota_caps) and the rule’s name
(quota_rule), and rests.jsonl each rest as it begins and ends: the window,
the reading, the cap (or what the rule said and the reading it judged) and when it
ends. The
monitor’s overview shows them beside each account’s usage: the limit on each
window, the identity paused until when, and its rests over the run.
The readings are those the coding agents report: Claude Code when a session
starts or resumes, omp at each launch, Codex with each API call of a ChatGPT
login. opencode and pi report none, so for an opencode-go model contraagent reads
the OpenCode Go subscription’s usage itself at each launch, from the endpoint OpenCode’s
console serves (<base>/v1/usage with the key; omp reads the same): the 5-hour and
weekly windows, and the monthly one by name in windows. The run starts
from the newest of recent runs’ journals and takes each launch’s as it ends, so
tasks that start together all start before the first of them reports the cap is
reached. A credential with no readings (a gateway, an API key, an OpenCode Zen model)
never rests on a quota. A quota opencode is refused on ends the launch
rate_limited at once: opencode itself would wait as long as the provider asks.
A reading names the account that reported it, so one credential’s accounts
are kept apart: ambient is each coding agent’s own login (two accounts when
Claude Code and omp are logged in as different users), and a credential’s token
can be replaced between runs. A hop is judged by the readings of the account a
launch on it bills (Backend.account): an earlier token’s 98% does not rest the
new one. The account is a digest (SHA-256 over a fixed prefix,
the source and the value, cut to 12 hex digits), never the value: Claude Code’s
is its login’s oauthAccount.accountUuid (in the config directory’s
.claude.json; for an ambient launch, a key or token in its environment first)
or the token or key the credential holds; omp’s is the account its usage report
names. An OpenCode key is its own account on every agent: the digest of the key
opencode, pi or omp would send (on omp, OPENCODE_API_KEY of the launch’s
environment, else of omp’s .env files), so one login used on all three is one
account, and a rest on one of them holds on the others. A UUID and a token of one account give two digests, so a reading counts
for the source it came from. A reading with no account (an older journal) counts
for every account of its credential.
A reading keeps what the coding agent reported beside the two windows: every
window by its name (windows, a model’s own such as seven_day_opus too), the
window the status is about (status_window), the threshold past which it warns
(threshold_pct), whether usage past the limit is allowed (overage_status,
overage_disabled_reason, using_overage), and any other field as extra.
What is guaranteed:
- Before launch, every route of every kind (failover hops and pool members included) is checked: the credential exists, carries the traits the task requires, and the coding agent can use it with that model. A 429 can never move a ZDR task onto a subscription.
- Nothing else in the environment. For any credential but
ambient, every credential-selecting variable, and every variable that looks like a secret (*_API_KEY,*_AUTH_TOKEN, …), is removed from the child before the chosen one is set. A tool that needs a key of its own gets it back throughPipeline(env=...). - Nothing the agent loads by itself. Claude Code runs with empty config
directories, so a key that does not work fails instead of falling back to a
stored login. omp runs in a profile with no stored logins; omp keeps some
logins outside its profiles, so for providers other than
anthropic,openai-codexandlitellma task that requires traits is refused on omp. omp also reads.envfiles at start (its working directory’s, its agent directory’s, its config root’s and~/.env), and fills from them every variable the launch’s environment leaves unset: when one of them sets the key or endpoint variable of the launch’s provider and the credential does not, the binding says which file, and a task that requires traits is refused on omp. opencode gets no stored login but the chosen one, and the chosen key in its launch config, over any key in the user’s own opencode config; an OAuth login (which opencode refreshes and writes back over every stored login) is not narrowed, so a task that requires traits is refused on it. Codex runs with an emptyCODEX_HOMEand without the user’sconfig.toml. - What the agent says it used. A launch whose reported credential source
does not match its credential ends
credential_mismatchand is not ok.
The journal records each launch’s credential name, never a secret.
Every table and key
Section titled “Every table and key”The JSON Schema of the file is schemas/pipeline.schema.json beside this page
(Compatibility); the reference file’s first line gives it to an
editor that reads #:schema.
#:schema ../../schemas/pipeline.schema.json# A reference pipeline config: every table and key, annotated.# The package's tests load it, build a routing plan from it and check that it writes# every key of the JSON Schema, so it stays valid and complete.## The config is operations, not definitions: which coding agent, model and# credential each kind of task runs on, where it fails over to, and budgets that# outrank the ones its agent_task(...) declares.
# --- run-wide defaults --------------------------------------------------------
[defaults]_comment = "a note for the reader; every table takes one, and contraagent ignores it"agent = "claude" # a registered coding agent: claude, omp, omp-rpc, opencode, pi, codexeffort = "medium" # checked against the agent's CLI (Backend.effort_levels)credential = "personal" # without it: "ambient", the host's own setup# model = "claude-sonnet-5-5" # without it: the agent's default model# model_failover = false # hand same-agent fallback models to the CLI's own flag# max_passes = 10 # passes each `while` loop of flow code may make (Pipeline(max_passes=)); a function's @max_passes(n) outranks it# context_warn_pct = 80 # context_warn_pct of every kind with a context_budget and none of its own# preflight = "stop" # probe every route first; a kind with no route left: "substitute" or "stop"; "off" (default)
# How many running tasks may hold each claimed name (agent_task(claims=...)) at once, by the# name or an fnmatch pattern that gives each name it matches a capacity of its own (a limit# shared by many tasks needs one name they all claim). A number, or what one running task# needs of the host (PerTask): memory_mb and/or cores, as many at once as the free memory# (MemAvailable) and the usable cores hold, one while none runs.# Pipeline(claim_capacity=) outranks it key by key; a name that no entry gives: one at a time.[defaults.claim_capacity]"heavy:*" = 2 # claims=["heavy:{kind}"]: two at a time of each kindgpu = 4 # claims=["gpu"]: four at a time across the runindex = { memory_mb = 1500, cores = 1 } # claims=["index"]: a local index server of about 1.5 GB each
# The failover chain every kind without its own shares; block order is hop order.[[defaults.fallbacks]]agent = "omp"model = "opencode-go/glm-5.3" # OpenCode Go: its 5-hour, weekly and monthly usage is read at each launchcredential = "ambient" # the OPENCODE_API_KEY omp finds: the environment, else its .env files (~/.env)
# --- one table per kind of task ---------------------------------------------
[tasks.summarize] # agent_task("summarize", ...)# Routing: each key outranks [defaults] for this kind.agent = "claude"model = "claude-sonnet-5-5"effort = "high"# credential = "personal"# model_failover = false# Budgets. max_turns and timeout_s are required for every kind (here or in agent_task).max_turns = 20 # turns per launch; outranks agent_task(..., max_turns=10)timeout_s = 600 # seconds per launchmax_budget_usd = 2.0 # cost per launch, as the coding agent reports itstall_s = 300 # stop a launch whose transcript is silent this longcontext_budget = 150000 # stop a launch once one call's input passes this many tokenscontext_warn_pct = 80 # warn (journal context_warned, a log line) once a launch's peak context passes 80% of context_budget; it still runsmax_denials = 5 # stop a launch after this many permission denials# What a launch stopped for its time wrote is a partial result when it passes its checks.partial_on = ["timeout", "stalled"] # stops after which the output is kept; outranks agent_task(..., partial_on=)max_corrections = 2 # correction rounds after the first, 0 to 20 (default 3): an output that fails its checks goes back to the session with the errors# Scheduling and retries.max_concurrent = 2 # tasks of this kind at once (the run's own cap still applies)retries = 2 # fresh-session attempts after the first, each after one that made no progressretry_backoff_base = 2.0 # seconds, doubled per attempt# keep_failed_changes = false # leave the files a failed attempt created under its changes= patterns in place
[tasks.analyze] # agent_task("analyze", ..., requires=["zdr"])credential = "zdr" # must carry every trait the task requiresmax_turns = 30timeout_s = 1200# Tools its launches may not use, in Claude Code's names and patterns. Claude Code and# opencode deny a pattern too (opencode a Bash command pattern, any other tool whole), pi# whole tools only; a route on omp or codex is refused, so this kind's chain stays on claude.disallowed_tools = ["WebFetch", "Bash(git push:*)"]
# A kind's own failover chain; block order is hop order. A hop that names no# credential keeps the kind's own, and every hop is checked like the primary.[[tasks.analyze.fallbacks]]agent = "claude"model = "claude-opus-5-5"
[tasks.research] # agent_task("research", ...)credential = "subs" # a pool: two OAuth tokens taking turnsmodel = "claude-sonnet-5-5"max_turns = 15timeout_s = 600
[[tasks.research.fallbacks]] # another agent, another identity: omp through the gatewayagent = "omp"model = "litellm/claude-sonnet-5-5"credential = "company-gateway"
# A base kind's table: every implementation kind of it (review-python, review-go) starts from it.[tasks.review] # agent_task("review", implementations={"python": ..., "go": ...})model = "claude-sonnet-5-5"max_turns = 20timeout_s = 600
# An implementation kind's own table: each key it gives replaces the base kind's (fallbacks too).[tasks.review-go]model = "claude-opus-5-5"timeout_s = 900
# --- credentials --------------------------------------------------------------# Secrets are references, read in the launching process: env:NAME or file:/path.# `traits` are labels a task's `requires` is matched against. Every type takes# `isolate`: true (the default but for ambient) keeps the coding agent from using# anything it would load by itself (stored logins, keys in its own config) instead# of this credential; and the quota caps max_five_hour_pct and max_seven_day_pct.
[credentials.host] # the host's own setup, as the built-in "ambient" is, with caps of its owntype = "ambient"isolate = false # the default for ambient: the agent loads what it would load by itselfmax_five_hour_pct = 95
[credentials.personal] # a /login (Claude Code), a profile login (omp) or a stored login (opencode)type = "subscription"provider = "anthropic" # the default; omp, pi and opencode name the provider whose login it isisolate = true # the default: only this login, never a key the agent finds by itselfmax_five_hour_pct = 90 # quota caps: past them the credential rests until the window resets,max_seven_day_pct = 95 # and its tasks take the next hop (or are deferred with none left)# config_dir = "/home/me/.claude-work" # Claude Code: the CLAUDE_CONFIG_DIR holding the login# profile = "work" # omp: the profile holding it
[credentials.zdr] # a provider's own API keytype = "api_key"provider = "anthropic"key = "env:ZDR_ANTHROPIC_API_KEY"traits = ["zdr"]isolate = true
[credentials.company-gateway] # your own endpoint (LiteLLM, a proxy, a self-hosted API)type = "gateway"base_url = "env:LITELLM_BASE_URL" # or a literal URLtoken = "env:LITELLM_API_TOKEN"isolate = true# auth = "bearer" # or "api_key" (sent as x-api-key; Claude Code only)# provider = "litellm" # how omp and opencode address it: models are litellm/<model># headers = { X-Team = "research" } # Anthropic-API endpoints only
[credentials.token-a] # long-lived OAuth tokens (claude setup-token)type = "oauth_token"provider = "anthropic" # the defaulttoken = "env:CLAUDE_TOKEN_A"isolate = true
[credentials.token-b]type = "oauth_token"token = "env:CLAUDE_TOKEN_B"
[credentials.subs] # several credentials used as onetype = "pool"members = ["token-a", "token-b"]strategy = "round_robin" # or "in_order": the next member only after a 429cooldown_s = 900 # rest after a 429 when the provider states no retry-afterisolate = truemax_five_hour_pct = 90 # each member's cap when it sets none of its ownmax_seven_day_pct = 95
# Claude Code only, from here on.[credentials.federated] # Anthropic workload identity federationtype = "workload_identity"provider = "anthropic" # the defaultfederation_rule_id = "fdrl_0123"organization_id = "00000000-0000-0000-0000-000000000000"identity_token_file = "/var/run/secrets/tokens/anthropic" # the identity token, exchanged for API accessservice_account_id = "svac_0123" # optionalworkspace_id = "wrkspc_0123" # optionalisolate = true
[credentials.aws] # Amazon Bedrocktype = "bedrock"region = "us-east-1"aws_profile = "bedrock" # an AWS profile; without it the default chain# bearer = "env:AWS_BEARER_TOKEN_BEDROCK" # or a Bedrock API key insteadisolate = true
[credentials.gcp] # Google Vertex AItype = "vertex"project = "my-project"region = "us-east5"credentials_file = "/etc/gcp/vertex.json" # a service-account key or a federation config; without it the defaultisolate = true
# --- the pipeline's own tables ------------------------------------------------
[pipeline.report] # a pipeline table: contraagent.pipeline_table("report") in a runtitle = "Weekly summary" # any keys: contraagent never checks themsections = ["changes", "risks"]