---
title: jr2.config.ts
description: "Every field of jr2.config.ts: its type, whether it is required, its default, and what it does."
---

# jr2.config.ts

An Instance's config is the default export of its `jr2.config.ts`: a `JR2Config`, written with `defineConfig`. Each field below has its path, as in `git.credentials[].match`, where `[]` is each entry of a list and `<name>` is each key of a map.

The shape of `jr2.config.ts`: the deployment facts of an Instance, which a Machine cannot carry.
It holds the Instance's name, its secrets, how the cluster authenticates to Repos, what the
Harness can reach, where Sandboxes run, and where images go. It names no Repo, no model and no
Sandbox Image: a Machine names those itself.

Every field is optional, and an Instance can run with no `jr2.config.ts`. Write the file as
`export default defineConfig({ … })`. When the config loads, a file with no default export is
refused: `` <file> has no default export (use `export default defineConfig({…})`) ``. The load also
checks the shape of `git.credentials`, `secrets`, `harness.catalog` and `priorityClasses`. It
checks that `sandbox` is an object, and checks its `resources`. Each refusal names the field.
`jr2 up` checks the rest.

**Example**

```ts
import { defineConfig, type JR2Config } from "@jr2/orchestrator";

const harness: JR2Config["harness"] = {
  provider: {
    id: "vllm",
    api: "openai-completions",
    baseUrl: process.env.VLLM_URL ?? "",
    models: { "Qwen/Qwen3.6-35B-A3B": { contextWindow: 131072, maxTokens: 16384 } },
  },
};

export default defineConfig({ name: "my-instance", harness });
```

**See also**

- [Connect a model](../get-started/connect-a-model.md)
- [Secrets and trust](../concepts/secrets-and-trust.md)
- [ADR-0050: What code names, config declares; what the CLI names, discovery finds](decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md)

Fields: [`name`](#name), [`secrets`](#secrets), [`git`](#git), [`harness`](#harness), [`sandbox`](#sandbox), [`priorityClasses`](#priorityclasses), [`registry`](#registry), [`kitRegistry`](#kitregistry), [`platforms`](#platforms), [`operator`](#operator).

<a id="name"></a>

## `name`

- Type: `string`
- Required: no
- Default: the name of the Instance folder

The Instance's name. Its Kubernetes namespace is this name (`-n` overrides it), and
`jr2 up` labels every object that it owns with it.

<a id="secrets"></a>

## `secrets`

- Type: `readonly SecretEntry[]`
- Required: no

Every secret of the Instance, in one list. A secret goes where something names it. A Held
secret goes where a `workspace()` lists it under `secrets` or an Agent's provider reads it. An
Effect secret goes where an Effect names it with `secret("name")`. Its `hosts` is a Held
secret's binding, never its placement: an entry that nothing names goes nowhere. A model API
key goes here, never in `harness.env`. See `SecretEntry`. Each entry is checked when the
config loads: `<file> at secrets[<i>]<path>: <problem>`, followed by the shape of an entry.

<a id="secrets-name"></a>

### `secrets[].name`

- Type: `string`
- Required: yes

As a Held secret, the name of the env var that the Harness container sees, which holds the
Stand-in. As an Effect secret, the name that `secret("<name>")` names, and the name of the
Effector's file. For a Held secret, it must match `[A-Z_][A-Z0-9_]*`:
`secrets[<i>].name "<name>" is not an env var name ([A-Z_][A-Z0-9_]*)`. It must not start with
`JR2_` and must not be a proxy or trust variable of the kit:
`secrets[<i>].name <NAME> is the kit's — JR2_ names and the proxy and trust variables belong to the mechanism`.
One name, one entry: `secrets[<i>].name <NAME> is held twice`.

<a id="secrets-value"></a>

### `secrets[].value`

- Type: `string`
- Required: no

The value, usually read from `process.env` (`.env`). `jr2 up` writes it only into the Secrets
of the containers that hold it. Write exactly one of `value` and `valueFrom`:
`secrets[<i>] needs exactly one of value or valueFrom`. A `.env` variable that is not set reads
as no value, and an empty one is refused: `secrets[<i>].value is empty — set <NAME> in .env`.

<a id="secrets-valuefrom"></a>

### `secrets[].valueFrom`

- Type: `object`
- Required: no

Or a key of a Secret that you manage (Sealed Secrets, External Secrets):
`{ secretKeyRef: { name, key } }`. `jr2 up` checks that the Secret and the key exist. Each
container that holds the secret mounts that one key as a file, and no other container does.

<a id="secrets-valuefrom-secretkeyref"></a>

### `secrets[].valueFrom.secretKeyRef`

- Type: `object`
- Required: yes

The Secret and the key in it that hold the value.

<a id="secrets-valuefrom-secretkeyref-name"></a>

### `secrets[].valueFrom.secretKeyRef.name`

- Type: `string`
- Required: yes

The name of the Secret.

<a id="secrets-valuefrom-secretkeyref-key"></a>

### `secrets[].valueFrom.secretKeyRef.key`

- Type: `string`
- Required: yes

The key in the Secret that holds the value.

<a id="secrets-hosts"></a>

### `secrets[].hosts`

- Type: `readonly string[]`
- Required: no

Where the value may go as a Held secret. Each entry is `host`, `host:port`, or an `https://`
URL, of which only the host and port count. Write exact DNS names or IP addresses, with no
wildcards. The port default is 443. A known model key, such as `ANTHROPIC_API_KEY`, may leave
it out: it then goes to the one host that its provider calls. Any other Held secret must name
its hosts: `secrets[<i>].hosts is absent, and <NAME> has no single default host`.

<a id="secrets-headers"></a>

### `secrets[].headers`

- Type: `readonly string[]`
- Required: no
- Default: `["authorization", "x-api-key", "x-goog-api-key", "api-key"]`

The request headers in which the Custodian puts the value in place of the Stand-in,
lowercase. A header
that frames or routes the request is refused:
`secrets[<i>].headers[<j>] "<header>" cannot carry a held secret`.

<a id="secrets-paths"></a>

### `secrets[].paths`

- Type: `readonly string[]`
- Required: no
- Default: every path

The path prefixes to which the value may be sent, such as `/v1/messages`. Each starts with
`/`. Narrow it for a gateway key that can reach admin routes, such as a
LiteLLM key that can make new keys. The Agent could take away a key made that way.

<a id="git"></a>

## `git`

- Type: `GitConfig`
- Required: no

How the cluster authenticates to Repos, and the fence that a per-run url must pass. It is not
a list of Repos: a Machine names its Repos in the Repo Slots of its `workspace()`. See
`GitConfig`.

<a id="git-credentials"></a>

### `git.credentials`

- Type: `readonly GitCredential[]`
- Required: no

The credential entries, matched by prefix on the Repo identity. The longest `match` wins; on a
tie, the first entry in the list wins.

The list is also a fence. A per-run url, which comes from the run input, must match an entry,
or the Sandbox refuses it when it attaches the Repo:
`Sandbox "<name>" refuses the per-run repo <url> for slot "<slot>": no git.credentials entry matches "<identity>"`.
Thus a run cannot spend this cluster's credentials against any host. A url that a Machine
binds needs no entry. When no entry matches it, it clones with no credential.

Each entry is checked when the config loads:
`<file> at git.credentials[<i>]<path>: <problem>`, followed by the shape of an entry.

<a id="git-credentials-match"></a>

### `git.credentials[].match`

- Type: `string`
- Required: yes

The Repos this entry is for: a prefix of a Repo's identity, such as `github.com/ourorg/`, or
`*` for every Repo. The identity of a Repo is its host and path with no scheme, user or `.git`.
When more than one entry matches, the longest `match` wins; on a tie, the first entry in the
list wins.

<a id="git-credentials-token"></a>

### `git.credentials[].token`

- Type: `string`
- Required: no

For an `https://` url: the name of the env var that holds a token, read from the shell or
`.env` when `jr2 up` runs. `jr2 up` writes the value into a Secret (`jr2-git-<hash>`) that only
the Repo cache agent reads. The Agent never sees it.

<a id="git-credentials-sshkey"></a>

### `git.credentials[].sshKey`

- Type: `string`
- Required: no

For an ssh url: the name of a Secret that you create. It holds a deploy key under the Flux
key names `identity`, `identity.pub` and `known_hosts`.

<a id="git-credentials-push"></a>

### `git.credentials[].push`

- Type: `object`
- Required: no
- Default: the clone credential pushes

The credential that a `push()` Effect uses, when it is not the clone credential. Write
`{ sshKey }` or `{ token }`. An entry with no credential
admits a clone and never a push. When the push finds no usable credential, it is refused, for
example: `no git.credentials entry matches "<identity>" — a push spends the entry its url
matches`, or `git.credentials entry "<match>" holds no credential a push over <scheme> can
spend`.

It is one of these objects:

- Form 1: `sshKey`
  - `sshKey`, `string`: For an ssh url: the name of a Secret that you create, with the same keys as the entry's `sshKey`.
- Form 2: `token`
  - `token`, `string`: For an `https://` url: the name of the env var that holds the push token, read as for the entry's `token`.

<a id="git-credentials-sign"></a>

### `git.credentials[].sign`

- Type: `object`
- Required: no

The key that a `push({ sign: true })` signs with, and the committer it signs as:
`{ sshKey, name, email }` or `{ gpgKey, name, email }`. `sshKey` names a Secret with
`identity`. `gpgKey` names a Secret with `private.asc` and no passphrase. jr2 never signs with
the clone or push key, so name the signing key here. The email must belong to the forge account
that holds the key, or the forge shows the signature as unverified. A signed push without it is
refused: `signed, and git.credentials entry "<match>" holds no sign field`.

It is one of these objects:

- Form 1: `sshKey`, `name`, `email`
  - `sshKey`, `string`: The name of a Secret that you create, which holds the ssh signing key under `identity`.
  - `name`, `string`: The committer's name on the signed commit.
  - `email`, `string`: The committer's email. It must belong to the forge account that holds the key.
- Form 2: `gpgKey`, `name`, `email`
  - `gpgKey`, `string`: The name of a Secret that you create, which holds the GPG signing key under `private.asc`, with no passphrase.
  - `name`, `string`: The committer's name on the signed commit.
  - `email`, `string`: The committer's email. It must belong to the forge account that holds the key.

<a id="git-credentials-committer"></a>

### `git.credentials[].committer`

- Type: `object`
- Required: no

Who commits an unsigned `push({ squash: true })`: `{ name, email }`. The squashed commit is
new, so it needs a committer. Its date comes from the Effector's clock, and its author stays
the Agent. A signed push is committed by the signer, so it does not read this field. A squashed
push with neither field is refused: `squashed without sign, and git.credentials entry "<match>"
holds neither sign nor committer`.

<a id="git-credentials-committer-name"></a>

### `git.credentials[].committer.name`

- Type: `string`
- Required: yes

The committer's name on the squashed commit.

<a id="git-credentials-committer-email"></a>

### `git.credentials[].committer.email`

- Type: `string`
- Required: yes

The committer's email on the squashed commit.

<a id="harness"></a>

## `harness`

- Type: `HarnessConfig`
- Required: no

What the Harness can reach: a custom model provider, gateways for catalog providers, env
vars, and CAs. See `HarnessConfig`.

<a id="harness-provider"></a>

### `harness.provider`

- Type: `HarnessProvider`
- Required: no
- Default: none; the Agents use catalog providers only

A custom model provider, such as a vLLM or Ollama endpoint. See `HarnessProvider`.

<a id="harness-provider-id"></a>

### `harness.provider.id`

- Type: `string`
- Required: yes

The provider id that model names start with: `vllm` in `vllm/Qwen/Qwen3-32B`. It must not
also be a key of `harness.catalog`:
`harness.catalog.<id>: "<id>" is also harness.provider.id — a custom provider owns its own baseUrl`.

<a id="harness-provider-api"></a>

### `harness.provider.api`

- Type: `string`
- Required: yes

The wire protocol. The only value is `"openai-completions"`, which most OpenAI-compatible
endpoints speak, such as vLLM and Ollama. `jr2 up` refuses any other value before it builds or
applies anything:
`refusing: harness.provider.api is "<api>" — the Harness speaks: openai-completions (ADR-0018)`.

<a id="harness-provider-baseurl"></a>

### `harness.provider.baseUrl`

- Type: `string`
- Required: yes

The endpoint's URL. The pods must reach it: `localhost` never works, and a LAN address works
on kind. It changes between deployments, so read it from `process.env` (`.env`). `jr2 up`
checks from inside the cluster that the endpoint answers.

<a id="harness-provider-apikey"></a>

### `harness.provider.apiKey`

- Type: `string`
- Required: no
- Default: none, for an endpoint that needs no key

The API key, when the endpoint needs one. Read it from `process.env`. It is a Held secret,
`JR2_PROVIDER_API_KEY`: `jr2 up` gives the value to the Custodian only, bound to the host of
`baseUrl`, and the Harness container holds only its Stand-in. Thus `baseUrl` must be
`https://` when you set it:
`harness.provider.apiKey is set, and harness.provider.baseUrl is not https://`.

<a id="harness-provider-contextwindow"></a>

### `harness.provider.contextWindow`

- Type: `number`
- Required: no

The context window, in tokens, of each model of this provider that `models` does not set.
Set it here or in `models`: a limit that nothing sets is 0, and then Compaction has no context
budget.

<a id="harness-provider-maxtokens"></a>

### `harness.provider.maxTokens`

- Type: `number`
- Required: no
- Default: no limit is sent

The most output tokens in one completion, for each model of this provider that `models` does
not set.

<a id="harness-provider-models"></a>

### `harness.provider.models`

- Type: `Record<string, HarnessProviderModel>`
- Required: no

Token limits for each model, keyed by the model id after the provider prefix: the key for
`vllm/Qwen/Qwen3-32B` is `"Qwen/Qwen3-32B"`. A limit set here wins over the provider's.

<a id="harness-provider-models-name-contextwindow"></a>

### `harness.provider.models.<name>.contextWindow`

- Type: `number`
- Required: no
- Default: the provider's `contextWindow`

The model's context window, in tokens (vLLM: `max_model_len`).

<a id="harness-provider-models-name-maxtokens"></a>

### `harness.provider.models.<name>.maxTokens`

- Type: `number`
- Required: no
- Default: the provider's `maxTokens`

The most output tokens in one completion.

<a id="harness-catalog"></a>

### `harness.catalog`

- Type: `Readonly<Record<string, CatalogReach>>`
- Required: no
- Default: each provider's own endpoint

Catalog providers whose calls go to another endpoint, such as a gateway, keyed by the catalog
provider id: `anthropic`, `openai`, and so on. See `CatalogReach`. A key that is not a
catalog provider with a known model key is refused:
`harness.catalog.<id>: "<id>" is not a pi catalog provider jr2 knows a key for`.

<a id="harness-catalog-name-baseurl"></a>

### `harness.catalog.<name>.baseUrl`

- Type: `string`
- Required: yes

The whole base URL that the provider's API adds its paths to: `https://litellm.corp.example`
for `anthropic`, which adds `/v1/messages`; `https://litellm.corp.example/v1` for `openai`. It
must be `https://`: `harness.catalog.<id>.baseUrl must be an https:// URL`. It changes between
deployments, so read it from `process.env` (`.env`).

<a id="harness-env"></a>

### `harness.env`

- Type: `readonly HarnessEnvVar[]`
- Required: no

Env vars for the Harness container. The Agent can read every one of them, so do not put a
credential here. A known model key is refused here. Put it in the top-level `secrets` list.
See `HarnessEnvVar`.

<a id="harness-env-name"></a>

### `harness.env[].name`

- Type: `string`
- Required: yes

The env var's name. A known model key name, such as `ANTHROPIC_API_KEY`, is refused:
`harness.env[<i>] sets <NAME>, a model key the Agent could read and take out of the pod — hold it instead`.

<a id="harness-env-value"></a>

### `harness.env[].value`

- Type: `string`
- Required: no

The value, usually read from `process.env`. `jr2 up` writes it into a Secret of the Instance.
Write `value` or `valueFrom`.

<a id="harness-env-valuefrom"></a>

### `harness.env[].valueFrom`

- Type: `Record<string, unknown>`
- Required: no

Or a Kubernetes `valueFrom` reference, such as `{ secretKeyRef: { name, key } }`, given to the
pod as it is. A reference to a Secret of the kit is refused:
`harness.env[<i>] (<NAME>) reads Secret "<name>", which is the kit's and never the Harness's`.

<a id="harness-envfrom"></a>

### `harness.envFrom`

- Type: `readonly HarnessEnvFromSource[]`
- Required: no

Secrets and ConfigMaps that you manage, whose keys all become env vars of the Harness
container. See `HarnessEnvFromSource`.

<a id="harness-envfrom-prefix"></a>

### `harness.envFrom[].prefix`

- Type: `string`
- Required: no
- Default: none

Text put before each key's name, as Kubernetes does.

<a id="harness-envfrom-secretref"></a>

### `harness.envFrom[].secretRef`

- Type: `object`
- Required: no

A Secret that you manage, by name: `{ name }`. `jr2 up` checks that it exists.

<a id="harness-envfrom-secretref-name"></a>

### `harness.envFrom[].secretRef.name`

- Type: `string`
- Required: yes

The name of the Secret.

<a id="harness-envfrom-configmapref"></a>

### `harness.envFrom[].configMapRef`

- Type: `object`
- Required: no

A ConfigMap that you manage, by name: `{ name }`.

<a id="harness-envfrom-configmapref-name"></a>

### `harness.envFrom[].configMapRef.name`

- Type: `string`
- Required: yes

The name of the ConfigMap.

<a id="harness-cabundle"></a>

### `harness.caBundle`

- Type: `string`
- Required: no

A path to a PEM CA bundle, relative to the Instance folder. An example is the CA of an
internal endpoint in front of a LAN vLLM. Commit the file: CA certificates are public. `jr2 up`
puts it in the `jr2-ca` ConfigMap and uses it when it checks the provider. The Harness
container trusts it, and so does the Custodian when it verifies a host that a Held secret is
bound to.

<a id="sandbox"></a>

## `sandbox`

- Type: `SandboxConfig`
- Required: no
- Default: a Sandbox runs wherever an ordinary pod runs, at the kit's Size

Which nodes are Sandbox nodes, and the Size of a Workspace that states none. See `SandboxConfig`.

<a id="sandbox-nodeselector"></a>

### `sandbox.nodeSelector`

- Type: `Readonly<Record<string, string>>`
- Required: no
- Default: none

The pod's `nodeSelector`: a Sandbox node must have every one of these labels.

<a id="sandbox-tolerations"></a>

### `sandbox.tolerations`

- Type: `readonly Toleration[]`
- Required: no
- Default: none

The pod's `tolerations`, as you write them. jr2 adds no `tolerationSeconds`.
Thus a tolerated `NoExecute` taint keeps a Sandbox on its node while its Lease is renewed.

<a id="sandbox-tolerations-key"></a>

### `sandbox.tolerations[].key`

- Type: `string`
- Required: no

The taint key. Leave it out, with `operator: "Exists"`, to tolerate every taint.

<a id="sandbox-tolerations-operator"></a>

### `sandbox.tolerations[].operator`

- Type: `"Exists" | "Equal"`
- Required: no
- Default: `"Equal"`

`"Equal"` matches a taint whose value is `value`; `"Exists"` matches any value.

<a id="sandbox-tolerations-value"></a>

### `sandbox.tolerations[].value`

- Type: `string`
- Required: no

The taint value, for `operator: "Equal"`.

<a id="sandbox-tolerations-effect"></a>

### `sandbox.tolerations[].effect`

- Type: `"NoSchedule" | "PreferNoSchedule" | "NoExecute"`
- Required: no
- Default: every effect

The taint effect to tolerate: `"NoSchedule"`, `"PreferNoSchedule"` or `"NoExecute"`.

<a id="sandbox-tolerations-tolerationseconds"></a>

### `sandbox.tolerations[].tolerationSeconds`

- Type: `number`
- Required: no
- Default: the pod stays

For `"NoExecute"`: how many seconds the pod stays on the node after the taint is added.

<a id="sandbox-resources"></a>

### `sandbox.resources`

- Type: [`Size`](authoring-surface/type-size.md)
- Required: no
- Default: the kit default, memory `2Gi` and cpu `1`

The Size of a Workspace that states no Size at all: `{ limits: { memory, cpu } }` and nothing
else. The kit default fills a field that it leaves out. It never changes a Size that a Machine
states: to change that, use `customize(machine, { resources })`. jr2 reserves the whole Size and enforces the cpu limit. See
[`Size`](authoring-surface/type-size.md) for the refusals, such as
`<file> at sandbox.resources.requests is not accepted`.

<a id="priorityclasses"></a>

## `priorityClasses`

- Type: `PriorityClasses`
- Required: no

Existing PriorityClasses to use in place of the two that `jr2 up` creates. See
`PriorityClasses`.

<a id="priorityclasses-control"></a>

### `priorityClasses.control`

- Type: `string`
- Required: no
- Default: `jr2-control`, which `jr2 up` creates

An existing PriorityClass for the Orchestrator, the operator and the Repo cache agent.

<a id="priorityclasses-sandbox"></a>

### `priorityClasses.sandbox`

- Type: `string`
- Required: no
- Default: `jr2-sandbox`, which `jr2 up` creates

An existing PriorityClass for the Sandboxes and the Instance Harness.

<a id="registry"></a>

## `registry`

- Type: `string`
- Required: no
- Default: none; `jr2 up` loads the images into the kind cluster

The image registry prefix to which `jr2 up` pushes the images that it builds, such as
`registry.example.com/team`. It changes between deployments, so read it from `process.env`
(`.env`). On a cluster that is
not kind, `jr2 up` then refuses:
`` context <context> is not a kind cluster and no `registry` is configured ``.

<a id="kitregistry"></a>

## `kitRegistry`

- Type: `string`
- Required: no
- Default: none; the cluster pulls from the canonical home, `ghcr.io/snapwich/jr2-harness:<version>` and the others

The registry from which this cluster pulls the published Kit images: a mirror for a cluster
that is self-hosted, air-gapped, or mirror-only. Set, the cluster pulls
the same tags from `<kitRegistry>/jr2-harness:<version>`. Fill the mirror with
`jr2 kit push <registry>`; `jr2 up` never pushes to it. Read it from `process.env` (`.env`).

<a id="platforms"></a>

## `platforms`

- Type: `readonly string[]`
- Required: no
- Default: the platforms of the cluster's schedulable nodes that the kit releases images for

The platforms for which `jr2 up` builds its images, as docker platform strings, such as
`["linux/arm64"]`. `jr2 up` reports a node platform that it skips.

Set it when the nodes cannot tell. One case is a node group that scales up from zero. Another
is a set of nodes that need no image, such as an amd64 GPU group beside arm64 nodes. When you
set it, it replaces the default. It adds to nothing and removes from nothing. The kit releases
images for `linux/amd64` and `linux/arm64` only, so `jr2 up` refuses any other:
`` `platforms` names <platforms> — the kit releases images for linux/amd64, linux/arm64 and nothing else ``.

<a id="operator"></a>

## `operator`

- Type: `object`
- Required: no

Settings of the operator layer, for kit development.

<a id="operator-manage"></a>

### `operator.manage`

- Type: `boolean`
- Required: no
- Default: `true`

`false`: `jr2 up` does not converge the operator, and you run its controller yourself.
