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
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
- Secrets and trust
- ADR-0050: What code names, config declares; what the CLI names, discovery finds
Fields: name, secrets, git, harness, sandbox, priorityClasses, registry, kitRegistry, platforms, operator.
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.
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.
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.
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.
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.
secrets[].valueFrom.secretKeyRef
- Type:
object - Required: yes
The Secret and the key in it that hold the value.
secrets[].valueFrom.secretKeyRef.name
- Type:
string - Required: yes
The name of the Secret.
secrets[].valueFrom.secretKeyRef.key
- Type:
string - Required: yes
The key in the Secret that holds the value.
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.
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.
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.
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.
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.
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.
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.
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.
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:
sshKeysshKey,string: For an ssh url: the name of a Secret that you create, with the same keys as the entry'ssshKey.
- Form 2:
tokentoken,string: For anhttps://url: the name of the env var that holds the push token, read as for the entry'stoken.
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,emailsshKey,string: The name of a Secret that you create, which holds the ssh signing key underidentity.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,emailgpgKey,string: The name of a Secret that you create, which holds the GPG signing key underprivate.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.
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.
git.credentials[].committer.name
- Type:
string - Required: yes
The committer's name on the squashed commit.
git.credentials[].committer.email
- Type:
string - Required: yes
The committer's email on the squashed commit.
harness
- Type:
HarnessConfig - Required: no
What the Harness can reach: a custom model provider, gateways for catalog providers, env vars, and CAs. See HarnessConfig.
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.
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.
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).
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.
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://.
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.
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.
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.
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).
harness.provider.models.<name>.maxTokens
- Type:
number - Required: no
- Default: the provider's
maxTokens
The most output tokens in one completion.
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.
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).
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.
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.
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.
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.
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.
harness.envFrom[].prefix
- Type:
string - Required: no
- Default: none
Text put before each key's name, as Kubernetes does.
harness.envFrom[].secretRef
- Type:
object - Required: no
A Secret that you manage, by name: { name }. jr2 up checks that it exists.
harness.envFrom[].secretRef.name
- Type:
string - Required: yes
The name of the Secret.
harness.envFrom[].configMapRef
- Type:
object - Required: no
A ConfigMap that you manage, by name: { name }.
harness.envFrom[].configMapRef.name
- Type:
string - Required: yes
The name of the ConfigMap.
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.
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.
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.
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.
sandbox.tolerations[].key
- Type:
string - Required: no
The taint key. Leave it out, with operator: "Exists", to tolerate every taint.
sandbox.tolerations[].operator
- Type:
"Exists" | "Equal" - Required: no
- Default:
"Equal"
"Equal" matches a taint whose value is value; "Exists" matches any value.
sandbox.tolerations[].value
- Type:
string - Required: no
The taint value, for operator: "Equal".
sandbox.tolerations[].effect
- Type:
"NoSchedule" | "PreferNoSchedule" | "NoExecute" - Required: no
- Default: every effect
The taint effect to tolerate: "NoSchedule", "PreferNoSchedule" or "NoExecute".
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.
sandbox.resources
- Type:
Size - Required: no
- Default: the kit default, memory
2Giand cpu1
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 for the refusals, such as <file> at sandbox.resources.requests is not accepted.
priorityClasses
- Type:
PriorityClasses - Required: no
Existing PriorityClasses to use in place of the two that jr2 up creates. See PriorityClasses.
priorityClasses.control
- Type:
string - Required: no
- Default:
jr2-control, whichjr2 upcreates
An existing PriorityClass for the Orchestrator, the operator and the Repo cache agent.
priorityClasses.sandbox
- Type:
string - Required: no
- Default:
jr2-sandbox, whichjr2 upcreates
An existing PriorityClass for the Sandboxes and the Instance Harness.
registry
- Type:
string - Required: no
- Default: none;
jr2 uploads 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.
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).
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.
operator
- Type:
object - Required: no
Settings of the operator layer, for kit development.
operator.manage
- Type:
boolean - Required: no
- Default:
true
false: jr2 up does not converge the operator, and you run its controller yourself.