Skip to main content
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