# Gateway configuration reference

The gateway-config/v1 document, the named refusals with their statuses, the builder operations, and the pinning vocabulary.

Source: https://tokentriage.com/docs/reference/gateway-configuration

---
import Details from '../../../components/Details.astro';
import NextSteps from '../../../components/NextSteps.astro';

A managed gateway's configuration is one JSON document per gateway and environment, plus
a closed list of names the control plane answers with when it declines to store or
compile one. This page describes both, and the operations that read and write them.

It mirrors the shipped contract: the document port
(`internal/control/foundation/gatewayconfig`), the lane's problem map, and the operation
table the SDK and the CLI pin. For the task flow that produces a document, read
[Configure, test and promote a gateway](/docs/build/gateway-builder).

## The gateway-config/v1 document

One document describes one gateway in one environment. The same gateway has a different
document in QA than in Production, and the environment is part of the document rather
than only part of its address.

The document carries references and dials, not content: inventory target ids, pinned
`(policy_id, policy_revision)` pairs, guardrail profile revisions, and slot bindings. The
policy content itself lives in the policy ledger, which is why two gateways can pin the
same revision without copying it.

| Field | Type | What it holds |
|---|---|---|
| `schema_version` | string | `gateway-config/v1`, the one accepted value |
| `gateway_id` | string | the gateway this document configures |
| `environment_id` | string | the environment this document configures |
| `model_choices` | array | the models this gateway may call, with their dials |
| `routes` | array | ordered first-match branches, default route last |
| `input_protection` | object | the guardrail attachment that sees the request |
| `output_protection` | object | the guardrail attachment that sees the answer |
| `connection_bindings` | array | one provider connection per upstream slot |

### Model choices

A model choice names one entry of the operator inventory the workspace may point at. A
workspace can name a target; it does not create one.

| Field | Type | What it holds |
|---|---|---|
| `id` | string | `mc_` plus twelve base58 characters, gateway-private |
| `label` | string, optional | the name a colleague reads |
| `target_id` | string | an inventory target id, checked for eligibility by the service |
| `settings.temperature` | number, optional | absent and zero are different documents |
| `settings.max_output_tokens` | integer, optional | above zero when present |
| `settings.timeout_ms` | integer, optional | above zero when present |

### Routes

Routes evaluate in slice order and the first match wins, so the order of the array is the
routing semantics rather than a rendering detail.

| Field | Type | What it holds |
|---|---|---|
| `id` | string | `rt_` plus twelve base58 characters, gateway-private |
| `label` | string, optional | the name the route list shows |
| `predicate` | object or `null` | `null` marks the default route |
| `policy_id` | string | a policy in the caller's own workspace |
| `policy_revision` | integer | a revision above zero, not a head |
| `model_choice_id` | string | a `model_choices[].id` in this document |
| `fallback_model_choice_id` | string, optional | a second choice, different from the primary |

A predicate is one registered fact, one operator and one value from that fact's closed
set. v1 registers one fact.

| `fact` | `op` | `value` |
|---|---|---|
| `request.stream` | `is` | `true` or `false` |

A fact outside that table is declined by name instead of becoming a branch that matches
nothing.

### Protection attachments

`input_protection` and `output_protection` carry the same three fields, and both pin a
revision.

| Field | Type | What it holds |
|---|---|---|
| `profile_id` | string | the gateway-local guardrail profile |
| `profile_revision` | integer | a revision above zero, not a head |
| `disabled_rule_ids` | array of string, optional | inherited rules turned off here; duplicates are declined |

A required company or workspace rule is not removable through that list. The composer
puts mandatory bindings first in each phase, and the more restrictive action wins, ordered
`block`, `modify`, `monitor`, `simulate`.

### Connection bindings

A slot id is the inventory upstream's name, which is why a slot maps to a connection and
not to a model: several models of one upstream share one credential.

| Field | Type | What it holds |
|---|---|---|
| `slot_id` | string | the upstream name of the targets bound here |
| `connection_id` | string | a saved provider connection this environment may bind |
| `purpose` | string | `application`, `playground`, `comparison`, `judge` or `connection_validation` |

A release binds the `application` purpose. The other four name the same closed set the
connection grants use.

### Constraints

The document port validates offline: the same bytes are accepted or declined on any
machine, with no store, clock or network involved.

1. `schema_version` equals `gateway-config/v1`, and `gateway_id` and `environment_id` are
   both set.
2. `model_choices` is non-empty, its ids are unique, and every `target_id` is set.
3. `routes` is non-empty, its ids are unique, and no two predicates are identical.
4. One route has `predicate: null`, and it is the last element.
5. Every `model_choice_id` and `fallback_model_choice_id` resolves inside this document,
   and a fallback differs from its own primary.
6. Every pinned revision is above zero.
7. Each `slot_id` appears in at most one binding, and each `purpose` is in the closed set.

The first problem is returned, naming the field and the array index that produced it.

### Identifier grammar

Model-choice and route ids are minted inside one draft and mean nothing outside it. Both
follow the platform's id discipline: a prefix plus twelve characters from the base58
alphabet `123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz`, which omits `0`,
`O`, `I` and `l`.

| Prefix | Names | Scope |
|---|---|---|
| `mc_` | a model choice | one gateway draft |
| `rt_` | a route | one gateway draft |

Ids the document points at are minted elsewhere and carry their own prefixes: `gw_` a
gateway, `env_` an environment, `pc_` a provider connection, `rtp_` a routing policy.

### Canonical bytes

The canonical encoding sorts object members by key byte order, keeps arrays in place, and
emits no insignificant whitespace. It validates first, so a document the port declines has
no canonical form and cannot reach a digest through the door a valid one uses. Reordering
the struct that describes the document cannot move a digest.

## Named refusals and their statuses

Each named answer below is an RFC 7807 problem body whose `type` is the slug and whose
`status` is the one in this table. The pairing is closed: a handler producing a slug whose
status disagrees with the table serves a `500` instead, so the two cannot drift apart.

| Slug | Status | What it reports |
|---|---|---|
| `draft_revision_stale` | 412 | somebody saved first; the body carries the winner's revision and document |
| `head_revision_stale` | 412 | the deployment head or the policy head moved since the form read it |
| `draft_conflict` | 409 | two writers changed one draft in a way a single save cannot represent |
| `model_choice_referenced` | 409 | a deleted model choice is still selected by routes, which are listed |
| `environment_not_active` | 409 | the environment is retiring or retired, so drafts, releases and deploys are closed |
| `environment_has_references` | 409 | retirement is blocked; the body carries the reference report |
| `provider_test_required` | 409 | the destination requires provider evidence and none is recorded |
| `provider_test_stale` | 409 | a test is recorded against a different effective digest |
| `rollback_incompatible` | 409 | the prior release no longer validates under today's controls and grants |
| `connection_context_changed` | 409 | a bound connection's provider, endpoint or account label moved |
| `mandatory_generation_stale` | 409 | the required controls changed between the review and the apply |
| `validation_failed` | 422 | the compiler's diagnostics, each at its own JSON pointer |
| `policy_document_not_composable` | 422 | a shared policy's document is not a single tier leaf or one cascade |
| `inventory_target_ineligible` | 422 | a model choice points at a target this environment may not use |
| `connection_slot_unbound` | 422 | a slot the compiled artifact dispatches to has no connection |
| `default_route_missing` | 422 | the compiled artifact has no unconditional route |
| `connection_not_eligible` | 403 | the caller or the environment may not bind that connection |
| `environment_protected_deploy_denied` | 403 | the caller holds `deploy` but not `deploy.protected` here |
| `policy_revision_not_found` | 404 | a revision nobody published and one in another company read alike |
| `release_digest_mismatch` | 503 | the two compile paths produced different digests, so nothing is served |

<Details summary="Which diagnostics stop a save and which are warnings">

`default_route_missing`, `default_route_not_last`, `duplicate_route_condition`,
`policy_revision_not_found`, `policy_reference_cycle` and `inventory_target_ineligible`
are validation errors wherever they appear. `unreachable_route_condition` and
`model_choice_unreferenced` are warnings when you inspect a draft and errors when you
create a release from it, so an experiment may carry an unused model and a release may
not.

</Details>

## Operations

Every path below is relative to the lane base `/portal/api/v1/customer/v1/`, and
`{gw-path}` stands for
`organizations/{org}/workspaces/{ws}/applications/{app}/gateways/{gw}`. The action column
names the grant the operation requires.

### Drafts and the two catalogues

| Operation | Method | Path | Action |
|---|---|---|---|
| `customer.gateways.draft.get` | GET | `{gw-path}/environments/{env}/draft` | `gateway.read` |
| `customer.gateways.draft.put` | PUT | `{gw-path}/environments/{env}/draft` | `config.edit` |
| `customer.gateways.draft.history` | GET | `{gw-path}/environments/{env}/draft/history` | `gateway.read` |
| `customer.gateways.components` | GET | `{gw-path}/components` | `gateway.read` |
| `customer.gateways.targets` | GET | `{gw-path}/environments/{env}/targets` | `gateway.read` |

`draft.get` seeds a gateway nobody has edited yet, so it is a read that can write, and it
seeds only for a caller who also holds `config.edit`. The palette names no environment: a
card's availability follows the gateway profile and the caller's grants.

### Explanations

| Operation | Method | Path | Action |
|---|---|---|---|
| `customer.gateways.inspect` | POST | `{gw-path}/environments/{env}/inspect` | `gateway.read` |
| `customer.gateways.diff` | POST | `{gw-path}/environments/{env}/diff` | `gateway.read` |

Both are POSTs because a subject and a request description do not belong in a query
string. Neither contacts a provider.

### Shared policies

| Operation | Method | Path | Action |
|---|---|---|---|
| `customer.policies.list` | GET | `organizations/{org}/workspaces/{ws}/policies` | `gateway.read` |
| `customer.policies.get` | GET | `organizations/{org}/workspaces/{ws}/policies/{p}` | `gateway.read` |
| `customer.policies.publish` | POST | `organizations/{org}/workspaces/{ws}/policies/{p}/revisions` | `config.edit` |
| `customer.policies.copy` | POST | `organizations/{org}/workspaces/{ws}/policies/{p}/copy` | `config.edit` |
| `customer.policies.impact` | GET | `organizations/{org}/workspaces/{ws}/policies/{p}/revisions/{rev}/impact` | `gateway.read` |
| `customer.policies.adopt` | POST | `{gw-path}/environments/{env}/draft/adopt` | `config.edit` |

Publishing appends a revision and moves nobody: each consumer stays pinned until its own
draft adopts the new one. The impact report is filtered to the scopes the caller may read,
and consumers outside them appear as counts.

### Environments

| Operation | Method | Path | Action |
|---|---|---|---|
| `customer.environments.list` | GET | `organizations/{org}/workspaces/{ws}/environments` | `workspace.read` |
| `customer.environments.create` | POST | `organizations/{org}/workspaces/{ws}/environments` | `workspace.manage` |
| `customer.environments.get` | GET | `organizations/{org}/environments/{env}` | `workspace.read` |
| `customer.environments.update` | PUT | `organizations/{org}/environments/{env}` | `workspace.manage` |
| `customer.environments.references` | GET | `organizations/{org}/environments/{env}/references` | `workspace.manage` |

The update body carries `display_name`, `state`, `require_fresh_provider_test` and
`expected_revision`. It has no field for `selector` or `protected`: both are fixed at
creation, and the store's update statement cannot reach either column.

### Releases and deployments

| Operation | Method | Path | Action |
|---|---|---|---|
| `customer.releases.create` | POST | `{gw-path}/environments/{env}/releases` | `config.edit` |
| `customer.releases.list` | GET | `{gw-path}/environments/{env}/releases` | `gateway.read` |
| `customer.releases.get` | GET | `{gw-path}/releases/{rel}` | `gateway.read` |
| `customer.deployments.deploy` | POST | `{gw-path}/environments/{env}/deployments` | `deploy` |
| `customer.deployments.rollback` | POST | `{gw-path}/environments/{env}/deployments/rollback` | `deploy` |
| `customer.deployments.head` | GET | `{gw-path}/environments/{env}/deployments/head` | `gateway.read` |
| `customer.gateways.promote.review` | POST | `{gw-path}/environments/{env}/promotion/review` | `config.edit` |
| `customer.gateways.promote.apply` | POST | `{gw-path}/environments/{env}/promotion/apply` | `deploy` |

Creating a release is authoring a pinning under `config.edit`; deploying one spends money
under `deploy`, and a protected environment adds `deploy.protected`. A repeated create
mints a second release rather than collapsing into the first, so a client that retried a
timed-out create lists and compares instead of assuming absorption.

`promote.apply` states three expectations about the destination, all required, and each
one loses with the slug that names what moved. The apply recomputes the plan rather than
trusting the caller's: the caller states expectations and the service states facts.

| Member | What it echoes | Slug when it is stale |
|---|---|---|
| `expected_mandatory_generation` | the destination's required-controls generation at review time | `mandatory_generation_stale` |
| `expected_draft_revision` | the destination's current draft revision at review time | `draft_revision_stale` |
| `expected_head_revision` | the destination's deployment head revision, `0` for a target nobody has deployed | `head_revision_stale` |

They are checked in that order, and the draft revision is settled before anything is
derived or deployed. A concurrent edit of the destination therefore loses as
`draft_revision_stale` and never reaches the head.

A release read publishes the release's identity rather than its document: the revisions it
pins, its effective digest, its mandatory generation and its resolved connection bindings.
It carries no `routes` and no `model_choices`, which is why a client comparing a draft
against what is applied calls `customer.gateways.diff` rather than rendering a release.

**Availability.** These eight rows are declared in the lane's registry, and the promotion,
release and deployment handlers behind them are landing across this private test.
Promotion review and apply, the deployment head read and the diff are the four the shipped
portal calls today. A deployment that composes no release ledger answers `503
dependency_unavailable` with "deployments not configured" rather than `404`, so a wired
path and a misspelled one stay distinguishable.

## Words a release pins

Seven words carry the lifecycle, and readers get the last three wrong most often.

| Term | What it means |
|---|---|
| draft | the editable `gateway-config/v1` document of one gateway in one environment; saving one reroutes no traffic |
| draft revision | the append-only counter of that document's saves, echoed back as `expected_revision` |
| release | one immutable pinning derived from a draft: the settings revision, the derived policy and profile revisions, the mandatory generation, every slot's connection, and the effective digest |
| deployment head | the one mutable pointer per gateway and environment: desired release, applied release, derived state, and the revision a change must echo |
| effective digest | the compiler's identity for what will execute, compared between the control plane and the serving cell |
| mandatory generation | the version of the required controls a release was compiled under, detectable later without recompiling |
| slot | an inventory upstream's name, and the unit a connection binds |

Creating a release changes nothing about what a gateway serves; only the deployment head
does that. A target nobody has deployed has no head, which is a different fact from a head
in `draft` state.

The digest is why a release cannot drift: the control plane derives and compiles it, the
serving cell resolves the same pinned revisions under the same compiler id, and a mismatch
is `release_digest_mismatch` rather than a request served from a guess. The mandatory
generation is what makes `mandatory_generation_stale` and `rollback_incompatible`
answerable. A slot maps to a connection because several models of one upstream share one
credential, and an unknown upstream stops the request before dispatch.

## What this page does not cover

- The policy language inside a pinned revision: [Routing policy
  language](/docs/build/policy-language).
- The environment lifecycle and promotion review as a task: [Move a gateway between
  environments](/docs/build/environments-and-promotion).
- The grants named in the action column: [Who can do what in your
  company](/docs/build/organizations-and-access).

<NextSteps items={[
  { title: 'Configure, test and promote a gateway', href: '/docs/build/gateway-builder', description: 'The task flow that writes one of these documents.' },
  { title: 'Move a gateway between environments', href: '/docs/build/environments-and-promotion', description: 'Custom environments, the deploy gate, retirement and rollback.' },
  { title: 'Errors & problem+json', href: '/docs/reference/errors', description: 'The problem body every slug above is carried in.' }
]} />
