Skip to main content

How to govern a coding-agent fleet with Talon

This guide shows how to govern an orchestrated coding-agent fleet — Claude Code subagents and Codex executors fanning out across two providers — through one Talon gateway, with per-session budgets, per-subagent attribution, and signed evidence. It is also the canonical reference for the neutral orchestration-metadata contract. Allow about 20 minutes.

For single-tool setup without orchestration, start with the OpenClaw guide — the gateway mechanics (vault, agent identity, policy) are identical.

The scenario

A platform team runs an orchestrator that plans a change, then fans out:

  • Claude Code subagents (planner, generator, reviewer) call Anthropic's Messages API.
  • Codex executors call OpenAI's Responses API.
  • One logical coding session spans both providers and a tree of subagents.

Terminology: the roles are labels, not Talon concepts

Every role name in this guide — orchestrator, planner, generator, reviewer, executor, judge — is an illustration, not a vocabulary Talon knows:

  • An orchestrator is simply whatever client code coordinates the work (a lead agent session, a script, a CI job) and passes one session id to everything it spawns. Talon never sees a role called "orchestrator" — it sees a Talon agent identity (which key authenticated), a session id, and per-request subagent labels.
  • X-Talon-Agent-ID / X-Talon-Parent-Agent-ID / X-Talon-Client values are free-form strings you choose. Talon does not validate them against any known set, assigns them no semantics, and never uses them in policy — it records them as attribution (provenance: client_asserted) and rolls costs up by them. Name your subagents whatever your fleet calls them.
  • The one enforced vocabulary is X-Talon-Stage: exactly generation (producing candidate work), judge (evaluating or selecting between candidates), or commit (finalizing the chosen result). Any other value is silently dropped at ingestion — the request proceeds, no stage is recorded.
  • Talon agent ≠ tenant ≠ subagent id. A Talon agent is a traffic identity defined by one agent.talon.yaml, authenticated by its vault-bound agent key (e.g. one agent per tool); a tenant is the owning account, derived from the agent (key → agent → tenant_id); a subagent id is the client-asserted runtime label above. Budgets and policy bind to Talon agents and tenants — never to subagent ids.
  • A wire family (api_family) is the provider's HTTP API shape — anthropic (Messages API) or openai (Chat Completions / Responses) — which determines how Talon parses, redacts, and prices traffic on that route.

In practice: with Claude Code and Codex CLI, the vendor adapters pick up whatever session/subagent headers the tools emit — nothing to invent. With a custom orchestrator, you choose the labels and set the generic headers yourself; if a tool version doesn't emit identity headers, set the generic ones from the process that launches it.

The platform team wants three things:

  1. Budgets — a runaway session stops burning money, no matter which provider route it uses.
  2. Attribution — every request traceable to a session, a subagent, and its parent.
  3. Signed evidence — one queryable, integrity-checkable record of what happened.

How Talon sees it

Talon sits between the fleet and both providers as a gateway. Every request carries orchestration metadata in HTTP headers; Talon validates it, records it in signed evidence, and rolls it up:

The fleet doesTalon records
Orchestrator picks a session id and passes it to every subagentOne session grouping all requests with that id, across both provider routes (TestGatewayOrchestration_CrossProviderAndIsolation)
Each subagent identifies itself and its parentagent_id / parent_agent_id per request, rolled up per subagent in the session summary
Spend accumulates on both wiresOne session cost total; max_session_cost denies on either route (TestSessionBudget_CrossProviderDeny)
A client asserts identityEvidence with provenance: "client_asserted" — attribution, not authentication (see Limitations)

Prerequisites

  • Talon installed (go install github.com/dativo-io/talon/cmd/talon@latest or curl -sL https://install.gettalon.dev | sh). macOS linker errors: CC=/usr/bin/clang go install ...@latest.
  • Claude Code, Codex CLI, or any orchestrator/client that can set HTTP headers on its LLM requests.
  • Real Anthropic and OpenAI API keys (for governed traffic; the demo needs none).

Steps

1. Generate the coding-agents pack

mkdir talon-coding && cd talon-coding
talon init --pack coding-agents --name coding-gateway

This creates agent.talon.yaml, agents/codex.talon.yaml, and talon.config.yaml pre-configured for coding traffic (source of truth: internal/pack/templates/coding-agents/). The defaults are deliberate:

  • Two agents, one per toolclaude-code (the primary agent.talon.yaml, Anthropic route) and codex (agents/codex.talon.yaml, OpenAI route), each its own AI use case with its own vault-bound agent key, metadata.team: coding, policies.allowed_providers, and its own budgets. Budgets and audit attribute per tool. To serve both from one talon serve, set agents_dir (#267, shipped) and give each agent its own agents/<name>/agent.talon.yaml — discovery matches that exact filename and fails closed on duplicate agent.name. Without agents_dir, talon serve runs the single default agent.talon.yaml.
  • Shadow mode — would-have-denied decisions are recorded in signed evidence while nothing blocks. Flip to mode: "enforce" once the dashboard looks right.
  • response_pii_action: allow (organization baseline) — the honest streaming default: any other value buffers the entire SSE stream before the first token reaches the developer (see LIMITATIONS.md §7). Input-side scanning (input_scan: true → warn) still applies, and a per-agent downgrade to allow is deliberately not expressible.
  • Long timeouts (request_timeout: 600s) — the 120s default hard-cuts long coding generations (#230).
  • Credential recognizers — high-precision patterns for PEM private keys, AWS AKIA... ids, GitHub tokens, and sk-ant-/sk-proj- LLM keys. Talon is not a secret scanner; run gitleaks/trufflehog in pre-commit for repository hygiene — these cover prompt/response traffic only.
  • session_limits.max_cost: 10.00 per agent — the per-session soft cap (see step 4).

Identity topology choice (this determines what "one session" means — see session semantics):

TopologyEffectWhen
One agent per tool (pack default)Per-tool budgets and attribution; a session id asserted under both agents is two sessionsIndependent tools, per-team chargeback
One agent for the whole orchestrator, allowed on both providers (policies.allowed_providers)One session spans both provider routes; subagent attribution via subagent idsOrchestrated fan-out with a single fleet budget (this is what the demo does)

2. Store both provider keys and start the gateway

export TALON_SECRETS_KEY=$(openssl rand -hex 32) # keep it; same value at serve time
talon secrets set anthropic-api-key "sk-ant-..."
talon secrets set openai-api-key "sk-..."

# Mint each tool's Talon agent key (bound via agent.key.secret_name)
talon secrets set claude-code-talon-key "$(openssl rand -hex 24)"
talon secrets set codex-talon-key "$(openssl rand -hex 24)"

talon serve --gateway

Each tool authenticates with its agent key; Talon resolves the key to the agent, derives the tenant, and injects the real provider key upstream. For the Anthropic provider family, vault-stored keys are the only upstream auth mode — upstream_auth_mode: client_bearer is rejected at config load. This also means subscription/OAuth billing cannot be governed: pointing only ANTHROPIC_BASE_URL at Talon sends Claude Code's OAuth token, which Talon rejects. See LIMITATIONS.md §7.

Point each tool's base URL at the gateway (http://localhost:8080/v1/proxy/anthropic for the Anthropic SDK; http://localhost:8080/v1/proxy/openai/v1 for OpenAI-SDK/Codex clients, which append their own path — the trailing /v1 matters, #235) with its agent key as the bearer token. Per-tool walk-throughs: Claude Code, Codex CLI.

3. Send orchestration metadata — the neutral contract

This section is the canonical reference for the contract (implementation: internal/gateway/orchmeta.go, #194/#192).

Generic headers (work on every provider route)

HeaderMeaning
X-Talon-Session-IDGroups requests into one coding session
X-Talon-Agent-IDThe subagent making this request
X-Talon-Parent-Agent-IDThe subagent that spawned it
X-Talon-ClientClient label recorded in evidence (e.g. my-orchestrator)

Any client that can set headers participates fully — no Talon change needed. Example fan-out (orchestrator sets the same session id on both wires):

# Claude Code subagent → Anthropic route ($CLAUDE_CODE_KEY = the value minted
# into claude-code-talon-key)
curl -s -X POST http://localhost:8080/v1/proxy/anthropic/v1/messages \
-H "Authorization: Bearer $CLAUDE_CODE_KEY" \
-H "X-Talon-Session-ID: sess-feature-4711" \
-H "X-Talon-Agent-ID: generator" \
-H "content-type: application/json" -d '{...}'

# Codex executor → OpenAI route, child of generator
curl -s -X POST http://localhost:8080/v1/proxy/openai/v1/responses \
-H "Authorization: Bearer $CODEX_KEY" \
-H "X-Talon-Session-ID: sess-feature-4711" \
-H "X-Talon-Agent-ID: executor" \
-H "X-Talon-Parent-Agent-ID: generator" \
-H "content-type: application/json" -d '{...}'

Vendor adapters are data, not code

Clients that already emit their own headers are mapped onto the same neutral contract by an adapter table entry (vendorAdapters in internal/gateway/orchmeta.go) — never a code branch in the request path. Two adapters ship today:

ClientSession headerAgent headerParent header
claude-codeX-Claude-Code-Session-IdX-Claude-Code-Agent-IdX-Claude-Code-Parent-Agent-Id
codexSession-IdX-Openai-Subagent— (none; use the generic header for parent attribution)

The first adapter with any populated header wins. Onboarding a new client is exactly one of: (a) the client sends the generic X-Talon-* headers — zero Talon changes — or (b) one new entry in the adapter table.

Precedence and provenance

  • Precedence per field: generic > vendor > synthetic. If both X-Talon-Session-ID and a vendor session header are present with different values, the generic value is recorded and the vendor value is ignored — per field, so a generic session id can coexist with a vendor-asserted agent id (TestResolveOrchestration_Precedence).
  • session_source records how the session id was obtained: client_asserted (generic header), vendor_asserted (adapter header), or synthetic (no client assertion; Talon derives sess_<correlation-id>).
  • provenance is always client_asserted. Orchestration identity distinguishes subagents within an already-authenticated agent; it does not authenticate them. It is exactly as trustworthy as the workload that presented the agent key.
  • Identity is never a policy input. Budgets bind to the Talon agent and the agent-scoped session tuple, never to subagent agent_id labels (TestPolicyInputParity_WithAssertedSession). Acting on client-asserted identity waits for workload attestation (#149).
  • Per-agent opt-out: agent.accept_client_metadata: false in the agent file ignores asserted subagent/parent/client identity and vendor session headers (the generic X-Talon-Session-ID still resolves the session id). Default is true; the flag gates recording only (TestGatewayOrchestration_FlagOff). Requests without any asserted session id get a synthetic sess_<correlation-id> in evidence either way — synthetic ids never create session-store state or budgets.
  • Optional X-Talon-Stage tags a request's pipeline stage; only generation, judge, commit are accepted — anything else is dropped at ingestion (TestNormalizeStage).

Header hygiene

Every orchestration value must be RFC 7230 token charset and at most 128 bytes. Violations are rejected with HTTP 400 — never truncated — so hostile client-asserted strings never reach signed evidence or operator dashboards (TestResolveOrchestration_Hygiene, TestGatewayOrchestration_HygieneRejectedAtGateway).

Session semantics: what "one session" means

One session id groups requests across providers, scoped per (tenant, agent). Two different agents asserting the same session id get separate sessions and separate session budgets — a session id is not a cross-tenant or cross-agent join key (TestSessionBudget_AgentAndTenantIsolation). Synthetic session ids are evidence-only and never create session state or budget rows (TestSessionBudget_SyntheticSessionsCreateNoRows).

4. Set budgets

Per agent, in its agent.talon.yaml — the agent's one override over the organization baseline:

policies:
session_limits:
max_cost: 10.00 # soft cap per coding session
cost_limits:
daily: 50.00
monthly: 500.00

How the session cap (session_limits.max_cost) behaves — precisely:

  • A new request is denied once accrued session spend + the pre-request estimate exceeds the limit. The denial is HTTP 403 with a provider-native error body (session_budget_exceeded: session spend 6.00 + estimate 1.00 exceeds limit 5.00), so clients surface it like any provider error. One documented exception: errors emitted before routing (e.g. a request for an unknown provider prefix) use the OpenAI error shape, since no provider — and therefore no wire family — was resolved yet (#195).
  • Spend accumulates per session, not per provider — the same session is denied on the other provider's route too (TestSessionBudget_CrossProviderDeny).
  • It is a soft cap: one in-flight request whose real cost exceeds the estimate can overshoot, and N concurrent first requests are bounded only by N × per-request cost. Atomic reservation is #144 (TestSessionBudget_SoftCapOvershoot, TestSessionBudget_ConcurrentBurstBound).
  • Session denies carry a structured evidence detail (limit, spent, estimate) — populated only for session-budget denies, not other reasons (TestSessionBudgetDetail_OnlyOnSessionDeny).
  • In shadow mode the would-have-denied request proceeds and the deny is recorded as a shadow violation in signed evidence (TestSessionBudget_ShadowMode).
  • If the session store fails, the budget check fails open and the gap is annotated in signed evidence (session_budget_unavailable, TestSessionBudget_FailOpenAnnotated).

5. Watch it: audit, costs, dashboard

All session surfaces read the same signed evidence through one aggregation (evidence.BuildSessionSummary) — the CLI, the export, and the dashboard cannot disagree.

# Per-session summary + the session's records (per-subagent rollup)
talon audit list --session sess-feature-4711

# Verify HMAC integrity of every record in the session
talon audit verify --session sess-feature-4711

# Export only this session's records (for hand-off). Cache-aware cost columns
# (cache_read_tokens, cache_write_tokens, pricing_basis) are included.
talon audit export --session sess-feature-4711

# Per-session cost rollup, machine-readable
talon costs --session sess-feature-4711 --json

Scoping: without --tenant/--agent these session commands are unscoped (they show the whole session, whichever tenant owns it — local CLI access implies DB access anyway); pass --tenant and/or --agent to filter. Note talon costs' calendar rollups (no --session) default to tenant default — the session forms do not.

The dashboard (talon serve) has a Coding Sessions (orchestration) panel showing active sessions with per-subagent attribution and denials_by_reason — budget trips show up as session_budget_exceeded counts.

6. Run the offline demo

The whole scenario — one session, two providers, subagent tree, PII warn, session budget trip, signed evidence — runs offline in about 30 seconds:

make coding-agents-demo # from the repo root
# or: cd examples/coding-agents-demo && docker compose up -d --build && ./demo.sh all

The mock provider speaks both the Anthropic Messages wire and the OpenAI Responses wire (including SSE with cache-token usage), so no real API key exists anywhere in the stack. The same sequence is smoke-run in CI: go test -tags=integration ./tests/integration -run TestCodingAgentsDemo_EndToEnd. See examples/coding-agents-demo/README.md.


Failure mode → defense mapping

Failure modeTalon defenseControl / backing
Runaway spend inside one coding sessionPer-session soft cap; new requests denied 403 with provider-native session_budget_exceededmax_session_cost; TestSessionBudget_SoftCapOvershoot
Session shifts spend to the other provider's routeSession spend accumulates cross-provider; denied on both routesTestSessionBudget_CrossProviderDeny
Two teams collide on the same session idSessions scoped per (tenant, agent) — separate sessions, separate budgetsTestSessionBudget_AgentAndTenantIsolation
Hostile header value aimed at evidence or dashboardsHygiene gate: token charset, ≤128 bytes, reject 400 (never truncate)TestGatewayOrchestration_HygieneRejectedAtGateway
Junk stage strings bloating session stateFixed stage set (generation/judge/commit); others dropped at ingestionTestNormalizeStage
Subagent forges its identityAttribution, not authentication: provenance: client_asserted; identity never a policy input; budgets bind to the Talon agentTestPolicyInputParity_WithAssertedSession; LIMITATIONS.md §7
Session store outageBudget check fails open; gap annotated in signed evidence (session_budget_unavailable)TestSessionBudget_FailOpenAnnotated
Budget denials disrupting rolloutShadow mode records would-have-denied as shadow violations while traffic flowsmode: "shadow"; TestSessionBudget_ShadowMode
Credentials pasted into promptsHigh-precision recognizers (PEM, AKIA..., GitHub tokens, sk-ant-/sk-proj-) evidenced via pii_action: warnpack agent.talon.yaml custom_recognizers
Evidence tamperingHMAC verification over every record in the sessiontalon audit verify --session <id>

What Talon does not see (limitations)

LIMITATIONS.md §7 is the honest boundary statement for this whole scenario. In one breath:

  • Local tool execution is invisible — file edits, shell commands, and local tool runs never transit the gateway; evidence shows the model's tool_use intentions, not local execution.
  • Client-asserted identity is attribution, not authentication — attestation is #149.
  • Subscription/OAuth billing cannot be governed — governed operation requires the agent-key + vault-injected-key model.
  • Session budgets are soft caps — atomic reservation is #144.
  • Response-PII actions other than allow buffer whole streams — which is why the pack's organization baseline sets response_pii_action: allow.
  • Cache pricing falls back to the input rate when a pricing entry lacks cache rates.

Summary

BeforeAfter
Orchestrator → two providers directlyOrchestrator → Talon → both providers
Spend per API key, unattributedPer-session soft caps + per-agent daily/monthly caps
No idea which subagent did whatPer-subagent attribution in one session view
Logs you hope are intactSigned evidence; talon audit verify --session

You're done

Your fleet's traffic now flows through one gateway: every request carries session and subagent attribution, session budgets deny runaway spend on either provider route, and one aggregation feeds the CLI, the export, and the dashboard.

Next steps:

I want to…Doc
See the whole scenario run offlineCoding-agents demo (make coding-agents-demo)
Cap daily spend per teamCost governance by agent
Hand evidence to an auditorCompliance export runbook
Copy-paste policy recipesPolicy cookbook
Know exactly where the boundary isLIMITATIONS.md §7