The Agents tutorial shows the fast path: drop an
agent.toml with a provider and a prompt, and sling work to it. This guide is
the how-to reference for everything you can tune underneath that — the
five independent axes of an agent — with a deep dive on the upstream
axis (who serves the model), which is the newest knob.
For the exact field list and types, see the generated
Config Reference. This guide explains how the pieces fit;
the reference is the authoritative spec.
The five axes
An agent is the composition of five orthogonal choices. You set each one
independently — change the model without touching the harness, switch who serves
the model without changing the box, and so on.
A note on the word “provider.” It is overloaded by history. In an
[[agent]] / agent.toml block, provider selects the harness (the agent
CLI — claude, codex, …). In the city [session] block, provider selects
the runtime backend (where sessions run — tmux, k8s, …). They are
different axes; this guide always says “harness” or “runtime” to disambiguate.
The first three axes (harness, model, upstream) are per-agent. Transport is
per-agent. Runtime is city-/environment-wide (every session in a city runs on
the same backend).
Axis 1 — Harness (provider)
The harness is the agent CLI gc launches and drives. Gas City ships built-in
presets for the popular ones — claude, codex, gemini, grok, kimi,
cursor, copilot, amp, opencode, and more — so the minimal agent is just:
Each preset defines the command, default args, resume behavior, prompt
delivery, and the model + upstream contracts (below). Customize one with a
city-level [providers.<name>] block: a same-named block merges over the
built-in (you set only what you change), and a differently-named one can inherit
from any provider via base:
When command points to a launcher wrapper, also set resume_command to
invoke that wrapper with the harness’s resume arguments and {{.SessionKey}}.
The inherited resume command is a separate setting; changing command alone
can leave resumed sessions launching the original executable.
If a wrapper redirects the harness to a private configuration directory,
prepare required startup state in that directory too. Verify a fresh interactive
launch as well as resume: a non-interactive prompt can skip first-run screens
that would block an interactive agent.
Keep wrapper diagnostics out of the interactive terminal so they cannot overwrite
startup menu options. Match process_names to the executable actually running
inside the wrapper, and verify that Gas City still reports the agent alive after
startup and while it claims work.
For copy-paste setup of each built-in harness — the env vars it reads and the
direct / custom-endpoint / model permutations — see
Harness Recipes. See
Understanding Packs for shipping a harness preset
as a reusable pack.
Axis 2 — Model (option_defaults.model)
A harness exposes its configurable knobs through an options schema. The
model option is the common one; selecting a value injects the right CLI flags
for that harness — you pick an abstract label, the harness renders the flag.
option_defaults is a map of option key → choice value. The provider’s
options_schema declares the allowed choices and the flag_args each one emits
(see ProviderOption / OptionChoice in the reference).
This is why you write model = "sonnet" and not a raw --model flag: the schema
keeps the model selection portable and the flags server-side.
model is not an agent field of its own: set it with option_defaults (a
--model flag in args works too). A top-level model = "sonnet" in
agent.toml is ignored without a warning. In a legacy city’s city.toml
[[agent]] block it is an unknown field: gc config show warns about it and
gc start --foreground refuses to start.
Reasoning effort (option_defaults.effort)
effort is an options-schema key like model. Its tiers are
low · medium · high · xhigh · max, and the harness renders the matching flag
(for claude, --effort <tier>). The builtin claude harness defaults to
permission_mode = "unrestricted" (--dangerously-skip-permissions) and
effort = "max" (--effort max), so set effort explicitly when you want
cheaper, faster turns. Three places set it:
Agent keys win over provider keys, so the reviewer above runs at low while
other claude agents run at high, and a patch’s keys merge into the agent’s
own. A patch needs name and targets that one agent (rig = "*" reaches it
in every rig); there is no wildcard for all agents, so use
[providers.claude] for a city-wide default. Set effort = "" to launch
without an --effort flag at all, leaving the choice to Claude Code.
An agent’s args can select an option too: args = ["--effort", "high"]
launches at high, ahead of any option_defaults. An agent’s args replace
the provider’s args.
gc config explain --provider claude shows the provider-level resolved value
and which layer set it; it does not include agent-level option_defaults, so
an agent’s own effort is read from its agent.toml.
A single step can override these for the session launched to run it: opt_effort
or opt_model metadata on the step’s bead applies when that session launches (see
per-dispatch provider options). The options
are launch flags, so they stay with the session rather than the step: a warm
session that runs a later step, or a pool slot that claims a different ready bead
than the one it launched for, keeps the flags it launched with. Give steps that
need different options their own agent or pool, or set the same options on every
step that shares a pool or a gc.continuation_group.
Axis 3 — Upstream (who serves the model)
This is the newest axis. The model is what you ask for; the upstream is
who serves and resolves it — direct Anthropic, Bedrock, Vertex, a self-run
proxy, an OpenAI-compatible gateway. Switching upstream changes the base URL and
credentials the harness talks to, without changing the model, the harness, or
the box — and (post-un-weld) without re-provisioning: it relaunches the agent
in the warm box.
Declare an upstream, select it
Upstreams are named presets at the city level; an agent selects one by name.
Resolution order for an agent’s upstream: agent upstream → agent_defaults.upstream
→ unset (no upstream env injected; the harness uses whatever is ambient).
Abstract vs. raw — and why abstract is portable
An upstream can be written two ways, which compose:
Abstract (portable). base_url, api_key, and auth_token are
harness-agnostic. The resolver renders them onto that harness’s env-var
names, declared by the harness as its upstream_env binding. So one upstream
works on any harness:
- on a
claude agent → ANTHROPIC_BASE_URL + ANTHROPIC_API_KEY
- on a
codex agent → OPENAI_BASE_URL + OPENAI_API_KEY
The built-in harnesses ship their bindings out of the box (claude → ANTHROPIC_*,
codex → OPENAI_*, gemini → GOOGLE_GEMINI_BASE_URL/GEMINI_API_KEY, and so
on). A custom harness declares its own:
An abstract field with no matching harness binding (and no override, below)
is a hard error, never a silent no-op — you find out at resolution time,
not when the agent quietly talks to the wrong endpoint.
Raw (escape hatch). When the abstract trio doesn’t cover what a harness
needs, set raw env keys with env. They merge after the abstract render, so
they win:
Gateway harnesses — the per-field override
Some harnesses are gateways: one CLI (e.g. opencode) fronts many upstreams
whose credential env var is upstream-dependent (GROQ_API_KEY for Groq,
CEREBRAS_API_KEY for Cerebras, …). Such a harness has no single binding to
declare. The upstream names its own target with the per-field *_env overrides:
Per-field precedence is: upstream *_env override → harness binding → hard
error. Native single-upstream harnesses stay fully abstract via their binding;
gateways supply the name themselves.
Secrets and fingerprints
- Secrets are never inlined. Abstract and raw values may reference controller
env vars with
$VAR / ${VAR}, expanded at resolution — so api_key = "$ANTHROPIC_API_KEY" keeps the secret out of city.toml.
- Switching the upstream name relaunches the agent in the warm box (it is a
launch-half fingerprint change). Rotating the key within the same upstream
moves no fingerprint — the resolved serving env is excluded from the hash, so a
credential rotation never churns live sessions.
Axis 4 — Transport (session)
The transport is how gc drives the harness. The default is tmux (gc sends
keystrokes and captures the pane). Set session = "acp" to drive the harness
over the Agent Client Protocol (JSON-RPC over
stdio) instead — the harness’s resolved provider must declare supports_acp = true.
Axis 5 — Runtime (where it runs)
The runtime is where the session’s box lives. It is selected city-wide via the
[session] block (or the GC_SESSION environment variable), not per-agent:
Built-in runtime backends: tmux (local, default), subprocess (local,
headless), k8s (pods), ssh:user@host (a remote box over SSH), and
exec:<script> (a pluggable exec session pack
— this is how sandbox / micro-VM runtime packs plug in). GC_SESSION=ssh:user@host
or GC_SESSION=exec:<runtime-pack> selects one without editing city.toml.
An agent that never needs interactive attach can set attach = false to let gc
pick a lighter runtime (subprocess) where the city allows it.
Putting it together
A fully-specified agent that runs Claude on Bedrock, in a Kubernetes pod, driven
over tmux:
Inspect the result without launching anything:
A missing upstream binding (an abstract field with no harness upstream_env
and no *_env override) is reported as a hard error when the agent’s session is
resolved — so a misconfiguration fails the launch loudly rather than silently
talking to the wrong endpoint.
Where to go next
- Spec / reference: the authoritative field list and types —
Config Reference. Wire contract for exec runtimes —
Exec Session Provider.
- Packs: ship harness presets — a provider and its
upstream_env
serving-env binding — as reusable, shareable bundles —
Understanding Packs,
Shareable Packs. (A pack may also declare named
[upstreams.<name>] endpoint presets as a base that the importing city
inherits — import the pack, get the upstream for free. A city’s own
[upstreams.<name>] in city.toml or a city fragment overrides a
pack-provided one of the same name. A pack-carried [upstreams.<name>]
(its base_url, credential env-refs, and raw [env] block) is TRUSTED
configuration — the same trust level as a pack’s provider.command — so
only import packs you trust.)
- Tutorial: the guided walkthrough that introduces agents —
Tutorial 02 — Agents.
Last modified on October 23, 2026