# Helm Values Reference

This page documents the configuration values for the Omnia Helm chart. See
[Install with Helm](/how-to/operations/install-helm/) for installation steps.

## Operator Configuration

### Basic Settings

```yaml
replicaCount: 3  # 3+ recommended for HA with leader election

image:
  repository: ghcr.io/altairalabs/omnia-operator
  pullPolicy: IfNotPresent
  tag: ""  # Defaults to Chart appVersion

imagePullSecrets: []  # this chart's own pods only; see global.imagePullSecrets
nameOverride: ""
fullnameOverride: ""
```

### Private registry

```yaml
global:
  imageRegistry: ""     # e.g. registry.internal.example.com
  imagePullSecrets: []  # e.g. [{name: mirror-pull}] or [mirror-pull]
```

| Value | Default | Description |
|-------|---------|-------------|
| `global.imageRegistry` | `""` | Registry every Omnia image is pulled from. The registry host of each reference is replaced and the path kept: `ghcr.io/altairalabs/omnia-operator:<tag>` becomes `<imageRegistry>/altairalabs/omnia-operator:<tag>`. A reference with no registry host (a Docker Hub name such as `busybox:1.36`) is prefixed whole. A reference already under the registry is unchanged. A value with a path, such as `registry.example.com/omnia`, prefixes that path. No scheme. |
| `global.imagePullSecrets` | `[]` | Secret names, as `{name: <secret>}` or a bare string. Added first to every pod the chart deploys, ahead of `imagePullSecrets` and each component's `podOverrides.imagePullSecrets`, each name once. |

`global.imageRegistry` applies to every image the chart renders: the operator,
dashboard, management-api, doctor, Arena controller, PromptKit LSP, compaction
CronJob and the `helm test` pods. It also applies to every image the chart
passes the operator and the Arena controller as a flag, which are the images of
the pods they create:

| Operator flag | Pods |
|---|---|
| `--facade-image` | Agent facade container |
| `--framework-image` | Agent runtime container, one per framework type |
| `--session-api-image`, `--memory-api-image`, `--privacy-api-image` | Per-workspace services |
| `--eval-worker-image` | Eval workers |
| `--policy-broker-image` | Policy broker sidecar (Enterprise) |
| `--egress-proxy-image` | Sandbox egress proxy, and the init containers that deliver a browser session or sign-in grant to a sandbox |
| `--arena-worker-image` (Arena controller) | Arena workers (Enterprise) |

The sandbox classes the operator installs (`sandbox.image`,
`sandbox.promptarenaClass.image`, `sandbox.browserClass.image`) follow it too.
An image set on a custom resource (`spec.framework.image`, `spec.facades[].image`,
or the `image` of a SandboxClass you create) is used as written.

The chart passes `global.imagePullSecrets` to the operator and the Arena
controller as `--image-pull-secrets`. They add the names first to every pod
they create: agents, per-workspace services, eval workers, sandboxes, egress
proxies and Arena workers. A Secret must exist in the namespace of the pod
that uses it, so create it in every workspace namespace as well as the release
namespace.

Helm passes `global` to every subchart. Each observability subchart reads its
own registry setting:

| Subchart | Registry setting |
|---|---|
| `grafana` | `global.imageRegistry`, `global.imagePullSecrets` |
| `loki` | `global.imageRegistry`; pull secrets at `loki.imagePullSecrets` |
| `tempo` | `global.imageRegistry`; pull secrets at `tempo.tempo.pullSecrets` |
| `alloy` | `global.image.registry`, `global.image.pullSecrets` |
| `keda` | `global.image.registry`; pull secrets at `keda.imagePullSecrets` |
| `prometheus` | none: set each `prometheus.*.image.repository` to the full mirrored path; pull secrets at `prometheus.imagePullSecrets` |
| `nfscsi` (csi-driver-nfs) | `nfscsi.image.baseRepo` and `nfscsi.image.nfs.repository`; pull secrets at `nfscsi.imagePullSecrets` |

Render the chart with your values and check every `image:` line before you
install a subchart against a mirror:

```bash
helm template omnia oci://ghcr.io/altairalabs/charts/omnia --devel \
  -f values.yaml | grep 'image:' | sort -u
```

[Install in an air-gapped cluster](/how-to/operations/install-air-gapped/)
builds a bundle of every image and pushes it to these paths.

### Service Account

```yaml
serviceAccount:
  create: true
  annotations: {}
  name: ""  # Defaults to the release fullname when create is true
```

### Pod Configuration

```yaml
podAnnotations: {}

podSecurityContext:
  runAsNonRoot: true
  seccompProfile:
    type: RuntimeDefault

securityContext:
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false
  capabilities:
    drop:
      - ALL
```

### OpenShift

```yaml
openshift:
  enabled: false
```

| Value | Default | Description |
|---|---|---|
| `openshift.enabled` | `false` | Runs every Omnia pod under OpenShift's `restricted-v2` SCC. No chart pod sets `runAsUser`, `runAsGroup` or `fsGroup`, including any set in `podSecurityContext` or `dashboard.podSecurityContext`. The operator and arena controller get `--omit-pod-ids`, so the agent, sandbox, egress-proxy, enrolment and arena worker pods they build leave them unset too. The chart refuses to render with `nfs.csiDriver.enabled` or `dev.enabled`. When `false`, every pod runs as 65532 (the dashboard as 101). |

See [Install on OpenShift](/how-to/operations/install-openshift/).

### Resources

```yaml
resources:
  limits:
    cpu: "1"
    memory: 512Mi
  requests:
    cpu: 100m
    memory: 256Mi
```

### Scheduling

```yaml
nodeSelector: {}
tolerations: []
affinity: {}
```

### Leader Election

```yaml
leaderElection:
  enabled: true
```

### Health Probes

```yaml
probes:
  port: 8081
  liveness:
    initialDelaySeconds: 15
    periodSeconds: 20
  readiness:
    initialDelaySeconds: 5
    periodSeconds: 10
```

### Metrics

```yaml
metrics:
  enabled: true
  port: 8443
  secure: true
```

### Webhooks

```yaml
webhook:
  enabled: false
  port: 9443
```

### RBAC

```yaml
rbac:
  create: true
```

### CRD preflight check

```yaml
crdPreflight:
  enabled: true
```

`crdPreflight` checks at render time that the `omnia-crds` release is
installed and new enough — it does not install CRDs itself. CRDs install from
the separate `omnia-crds` chart; see
[Install with Helm](/how-to/operations/install-helm/).

### PromptPack schema source

```yaml
operator:
  promptPackSchema:
    source: embedded
```

| Value | Behavior |
|-------|----------|
| `embedded` (default) | The operator validates every pack against the schema built into its binary and makes no outbound call. |
| `network` | The operator fetches the version a pack's `$schema` names from `promptpack.org`, and uses the built-in schema if the fetch fails. A pack without `$schema` still uses the built-in schema. |

### Air-gapped install

```yaml
airGapped: false
```

| Value | Behavior |
|-------|----------|
| `false` (default) | Each outbound connection follows its own value. |
| `true` | Turns off install telemetry, licence activation and heartbeat, and the GitHub community templates source, and runs the operator with `--air-gapped`. The render fails if `operator.promptPackSchema.source` is `network`, or if `controlPlane.url`, `awsMarketplace.productSku`, `gcpMarketplace.reportingSecret`, `grafana.plugins` with `grafana.enabled`, or `dashboard.avatars.gravatar` is set. |

Image references do not change. See
[Outbound connections](/reference/platform/outbound-connections/) for every
destination, its default and the value that turns it off.

### Commercial control plane

```yaml
controlPlane:
  url: ""  # Empty disables the integration
  grantSecretName: omnia-installation-grant
  displayName: ""
  environment: production
```

The integration is off unless `url` is set. `grantSecretName` names the Secret,
in the release namespace, that holds the bootstrap grant. The chart never
creates it. `displayName` is the name this installation appears under in the
control-plane console, and `environment` is your own label for it.

Once registered, the operator installs the licence the control plane issues
into the `arena-license` Secret, the same place an uploaded licence goes. It
verifies each document against the embedded public key first, and never
replaces the installed licence with one issued earlier, so an outage or a bad
document leaves the last good licence in force. `license.key` cannot be set
together with `controlPlane.url`: every upgrade would write the key back over
the licence the control plane issued.

## Facade Configuration

```yaml
facade:
  image:
    repository: ghcr.io/altairalabs/omnia-facade
    tag: ""  # Defaults to Chart appVersion
```

### Media Storage

The facade provides media storage for file uploads, configured through
`spec.facades[].extraEnv` on the AgentRuntime:

```yaml
- name: OMNIA_MEDIA_STORAGE_TYPE
  value: local
- name: OMNIA_MEDIA_STORAGE_PATH
  value: /var/lib/omnia/media
```

`OMNIA_MEDIA_STORAGE_TYPE` accepts `none`, `local`, `s3`, `gcs`, or `azure`.
See [Configure Media Storage](/how-to/operations/configure-media-storage/) for
the S3, GCS, and Azure backends.

## Framework Configuration

The framework image is used for the agent runtime container. This naming
aligns with the CRD's `spec.framework` field.

```yaml
framework:
  image:
    repository: ghcr.io/altairalabs/omnia-runtime
    tag: ""  # Defaults to Chart appVersion
```

## Enterprise Features

```yaml
enterprise:
  enabled: false  # Enable enterprise features (Arena Fleet, etc.)
```

`enterprise.enabled` turns on the Arena controllers, workspace
shared-filesystem features, and the Project Editor's LSP validation (gated
separately by `promptkitLsp.enabled`). It does not install the Arena CRDs —
those come from the separate `omnia-crds` chart, gated by that chart's own
`enterprise.enabled` value (default `true`):

- [ArenaProject](/reference/evaluation/arenaproject/) — PromptKit bundle sources
- [ArenaJob](/reference/evaluation/arenajob/) — test execution
- [ArenaTemplateSource](/reference/evaluation/arena-template-source/) — project templates

:::note[License required]
Enterprise features require a valid license. See [Installing a License](/how-to/operations/install-license/).
:::

## Eval Worker Configuration

The eval worker runs realtime evals for non-PromptKit agents (Pattern A). It
is an **enterprise feature**.

```yaml
workspaceServices:
  evalWorker:
    image:
      repository: ghcr.io/altairalabs/omnia-eval-worker
      tag: ""  # Defaults to Chart appVersion
      pullPolicy: IfNotPresent
```

Whether an eval-worker Deployment exists is derived per Workspace service
group (the `spec.services[].evalWorker.enabled` field on the Workspace CRD),
not a chart toggle. Replica count is hardcoded to 1, and the container has no
configurable resource limits or requests.

The eval worker is namespace-scoped: one Deployment per namespace per service
group, with a namespaced `Role`/`RoleBinding`. A `ClusterRoleBinding` to a
separately provisioned `workspace-reader` `ClusterRole` is added only to read
cluster-scoped Workspace CRDs, gated on `rbac.create` (default `true`).

## Arena Fleet Configuration

Arena Fleet provides distributed testing for PromptKit bundles. This is an
**enterprise feature**.

### Basic Settings

```yaml
enterprise:
  enabled: true  # Required for Arena Fleet
  arena:
    worker:
      image:
        repository: ghcr.io/altairalabs/omnia-arena-worker
        tag: ""  # Defaults to Chart appVersion
        pullPolicy: IfNotPresent
      serviceAccountName: ""  # Empty = a per-job arena-worker SA with no cloud identity
      podLabels: {}           # e.g. {azure.workload.identity/use: "true"}
      resources:
        limits:
          cpu: 1000m
          memory: 512Mi
        requests:
          cpu: 100m
          memory: 128Mi
```

Set `serviceAccountName` to the workspace runtime ServiceAccount when
evaluations call a provider with `auth.type: workloadIdentity`. The worker then
inherits that ServiceAccount's cloud identity. Left empty, the controller
creates a per-job ServiceAccount with no cloud identity, and keyless providers
fail. `podLabels` adds labels to the worker pod template, for example to opt
into a cloud-identity webhook.

### Source Configuration

Source polling interval and timeout are ArenaProject CRD fields, not chart
values: `spec.interval` (required, no default) and `spec.timeout` (default
`60s`). See [ArenaProject](/reference/evaluation/arenaproject/).

See the [Arena Fleet documentation](/explanation/evaluation/arena-fleet/) for
architecture details.

### Arena Queue (Redis)

Arena Fleet uses a work queue for distributing tasks to workers: in-memory
for development, Redis for production.

#### Queue Type

```yaml
enterprise:
  arena:
    queue:
      type: redis  # memory (dev) or redis (production, the default)
```

`memory` runs in the arena-controller process: use a single controller
replica, and queued jobs are lost on restart. `redis` requires a resolvable
Redis URL, or the render fails.

#### Redis target

The queue resolves its Redis target from `enterprise.arena.queue.redis`, then
falls back to the cluster-wide `redis.default`. The first non-empty form wins,
in this order: `existingSecret` (a Secret holding the full URL), `url`, then the
decomposed `host`, `port`, `db` and `user`. Both blocks take the same fields.

```yaml
redis:
  default:
    existingSecret:
      name: redis-credentials
      key: redis-url
    # or: url: "rediss://my-redis.example.com:6379"
    # or: host: my-redis.example.com
    #     port: 6379

enterprise:
  arena:
    queue:
      type: redis
      redis:        # optional per-queue override of redis.default
        url: ""
        host: ""
        port: 6379
        db: 0
        user: ""
        tls:
          caExistingSecret:
            name: ""
            key: ""
```

This is the operator-wide default. A Workspace service group that sets its own
`redis` puts the runs in that group on it instead, controller and workers both;
see [`services[].redis`](/reference/core/workspace/#servicesredis). No chart
value is needed for that. The arena-controller's ClusterRole already lets it
read Workspaces and read and write Secrets in every namespace, which it uses
to write each such run's `<run>-queue-redis` Secret.

An empty `host` in both blocks leaves the queue without a Redis target. The
decomposed form is cleartext only; a TLS connection needs the URL form. `tls.caExistingSecret` names a Secret holding
the CA bundle for a Redis with a self-signed certificate.

Give the queue's Redis a `maxmemory-policy` of `noeviction` (or a `volatile-*`
policy). The queue's keys are work, not cache: under `allkeys-lru` or
`allkeys-lfu` Redis evicts a running run's pending list or item records when
memory runs short, and the run loses work without an error. Size `maxmemory`
for about 1.1 KB per work item of the runs in flight; a finished run's keys
expire an hour after it ends, and a deleted run's keys are deleted with it.

#### In-cluster dev Redis

There is no Redis in the `omnia` chart. The `omnia-dev` subchart deploys a
dev-only in-cluster Redis: a single-replica StatefulSet with no auth and no
persistence, `noeviction`, `maxmemory` 512mb under a 768Mi limit. Its Service is named `<fullname>-redis-master`, where `<fullname>` is the release name when it
contains `omnia` and `<release>-omnia` otherwise.

```yaml
dev:
  enabled: true
  redis:
    enabled: true
```

#### Redis dashboard for Grafana

When `dev.grafanaDashboards.redis.enabled` is true, the `omnia-dev` subchart
ships a ConfigMap labelled `grafana_dashboard: "1"` holding a Redis Overview
dashboard (used memory, total keys, uptime, commands per second, memory usage
and the Arena queue). The dashboard reads a Redis datasource that you provision
in Grafana.

### Arena Controller

The Arena controller manages ArenaProject, ArenaJob, ArenaRun and ArenaTemplateSource resources.

```yaml
enterprise:
  arena:
    controller:
      replicaCount: 1
      image:
        repository: ghcr.io/altairalabs/omnia-arena-controller
        tag: ""  # Defaults to Chart appVersion
        pullPolicy: IfNotPresent
      resources: {}
      workspaceIdleAfter: 336h
      serviceAccount:
        create: true
        annotations: {}
        name: ""  # Empty means <release>-arena-controller
```

`workspaceIdleAfter` is how long a user's working clone of an ArenaProject may
go unused before the controller judges it. A clone with nothing unpushed is
deleted, and opening the project clones it again. A clone with unsaved work is
flagged in the project's `status.workspaces` and never deleted. The value is a
Go duration.

### Community Templates

```yaml
enterprise:
  communityTemplates:
    enabled: true  # airGapped: true turns off the GitHub default
    name: community-templates
    url: https://github.com/AltairaLabs/promptkit-templates.git
    branch: main
    templatesPath: ""  # Path within fetched content where templates live; empty = root
    syncInterval: 1h
    timeout: 2m
    trust: community  # official | partner | community; empty leaves it out
```

The source is created in the default workspace's namespace
(`defaultWorkspace.namespace.name`) and only when `defaultWorkspace.enabled` is
true, because a template source must belong to a workspace. Other workspaces
add their own. See [ArenaTemplateSource CRD](/reference/evaluation/arena-template-source/) for details on template sources.

With `airGapped: true`, the source is not created while `url` is the GitHub
default. Point `url` at an in-cluster mirror to keep it.

`trust` is the source's tier in the gallery, `community` by default. The
repository is AltairaLabs' own, so `official` is an opt-in for an installer
allowed the `curate` verb on `arenatemplatesources` (cluster-admin is).
Admission refuses a tier above `community` from any other identity, a scoped
Argo CD or Flux ServiceAccount included, and on `helm upgrade --atomic` that
rolls the whole release back.

### PromptKit LSP

The PromptKit LSP server provides real-time YAML validation and code intelligence for the Project Editor.

```yaml
enterprise:
  promptkitLsp:
    enabled: true  # On by default; has no effect unless enterprise.enabled is also true
    replicaCount: 1
    image:
      repository: ghcr.io/altairalabs/omnia-promptkit-lsp
      tag: ""  # Defaults to Chart appVersion
      pullPolicy: IfNotPresent
    service:
      port: 8080
    resources:
      requests:
        cpu: 50m
        memory: 64Mi
      limits:
        cpu: 500m
        memory: 512Mi
```

Provides real-time YAML and PromptKit schema validation, semantic token
highlighting, and diagnostics in the Project Editor. See the
[PromptKit documentation](https://promptkit.altairalabs.ai) for the
configuration format.

## Dashboard Configuration

The Omnia Dashboard provides a web UI for monitoring and managing agents.

### Arena builder

The agent and sandbox the Arena builder ("Generate with AI") talks to in a
workspace, as the install's defaults. A Workspace overrides each in
[`spec.builder`](/reference/core/workspace/#builder). The defaults match the
`omnia-arena-builder` chart; set them when that chart is installed under
another `agent.name`, or for a builder of your own.

| Value | Default | Description |
|---|---|---|
| `dashboard.builder.agent` | `arena-builder` | The builder's AgentRuntime in the workspace namespace |
| `dashboard.builder.sandbox` | `sandbox` | The sandbox, in that agent's ToolRegistry, a builder session declares its project mount on |
| `dashboard.builder.projectPath` | `/workspace` | Where that sandbox mounts the project, as the builder agent is told it |
| `dashboard.builder.mountRole` | `project` | The sandbox mount role a builder session declares the project on: one of `operator.sandboxAPI.mountRoles`, or one the Workspace configures. Reaches management-api as `OMNIA_BUILDER_MOUNT_ROLE` |

An empty value leaves the builder unconfigured for every workspace that does
not set it, and the dashboard says so.

### Basic Settings

```yaml
dashboard:
  enabled: true

  image:
    repository: ghcr.io/altairalabs/omnia-dashboard
    pullPolicy: IfNotPresent
    tag: ""  # Defaults to Chart appVersion

  replicaCount: 1

  service:
    type: ClusterIP
    port: 3000

  resources:
    limits:
      cpu: 500m
      memory: 128Mi
    requests:
      cpu: 25m
      memory: 32Mi
```

The dashboard pod is nginx. It serves the dashboard's static files and passes
`/api/` to management-api, which does the work, so it needs little CPU or
memory, and it keeps no state, so it scales freely. Sign-in sessions are
management-api's: more than one management-api replica needs a Redis session
store (`redis.default` or `dashboard.session.redis`).

### Open tabs across upgrades

```yaml
dashboard:
  staticAssets:
    keepBuilds: 3
    persistence:
      enabled: false
      storageClassName: ""
      accessModes: [ReadWriteMany]
      size: 1Gi
      existingClaim: ""
    resources:
      limits: { cpu: 200m, memory: 64Mi }
      requests: { cpu: 10m, memory: 16Mi }
  updateStrategy: {}
```

Each build of the dashboard names its JavaScript and CSS files by their
content, under `/_next/static/`. A browser tab opened before an upgrade asks for
the files of the build it loaded. A pod of the new build does not have them, so
the tab's next navigation can fail. With the store off, the dashboard reloads
the page once when that happens, and shows a notice when it sees that a newer
build is running, so the user can reload on their own terms.

`staticAssets.persistence.enabled: true` keeps earlier builds' files instead.
Every dashboard pod runs an init container, from the dashboard image, that
copies the pod's `/_next/static/` into one shared volume and deletes the files
of all but the newest `keepBuilds` builds. nginx serves a file from the pod's
own image first and from the volume second, so an open tab finds its files on
any replica, during the rollout and after it.

The store keeps files, not page data. When an open tab moves to a page whose
data it has not fetched yet, Next.js fetches that data from the running
build, sees a different build, and loads the page in full, as a link to it
would. That is a page load, not an error, and the tab's state on the page it
left is lost.

| Value | Default | Description |
|-------|---------|-------------|
| `staticAssets.persistence.enabled` | `false` | Turn the store on |
| `staticAssets.persistence.accessModes` | `[ReadWriteMany]` | Access modes of the claim: `ReadWriteMany`, `ReadWriteOnce` or `ReadWriteOncePod`. More than one replica needs `ReadWriteMany`, and the chart refuses to render without it. A claim's access modes cannot change once it exists, so changing this value on an install fails the upgrade: switch the store off, which deletes the claim, then on again with the new modes |
| `staticAssets.persistence.storageClassName` | `""` | Storage class; empty uses the cluster default |
| `staticAssets.persistence.size` | `1Gi` | Claim size. A build's files are tens of MiB. Each pod prunes the volume to `keepBuilds - 1` other builds before it copies its own, so the volume holds at most `keepBuilds` builds |
| `staticAssets.persistence.existingClaim` | `""` | Use this claim instead of creating one. Set `accessModes` to its access modes, which the chart reads to choose the update strategy |
| `staticAssets.keepBuilds` | `3` | Builds kept, newest first. The pod's own build is always kept, and a build a pod starts again, as in a rollback, counts as the newest |
| `staticAssets.resources` | see above | Resources of the init container |
| `updateStrategy` | `{}` | The Deployment's `strategy`. Empty lets the chart choose (see below) |

Access modes and the update strategy:

- **`ReadWriteMany`**, any number of replicas: the Deployment keeps the
  default `RollingUpdate`. Old and new pods mount the volume at once, on any
  node.
- **`ReadWriteOnce`, one replica**: the chart sets `strategy: Recreate`. A
  `ReadWriteOnce` volume attaches to one node, so a rolling update's new pod,
  scheduled on another node, would wait for the volume while the old pod waits
  for the new one to be ready. `Recreate` stops the old pod first. The
  dashboard is unavailable until the new pod is ready, and open tabs keep
  their files once it is, because the old build is already in the volume.
  Where the volume can attach to both pods at once, such as a single-node
  cluster, set `updateStrategy` to `RollingUpdate` yourself.
- **`ReadWriteOnce`, more than one replica**: refused at render time.

The init container runs as the pod does: as uid 101, with a read-only root
filesystem and no capabilities. It writes the volume through the pod's
`fsGroup`, 101 by default, so the storage class must honour `fsGroup` or give
uid 101 write access. A `dashboard.podSecurityContext` you set replaces the
chart's whole pod security context, so it must keep `fsGroup` (101, or a
group that can write the volume). Under `openshift.enabled` the platform
assigns the `fsGroup`.

The store is a cache, so the init container fails open. When the volume is
full, cannot be written, or another pod holds the lock past the wait limit,
it logs a `WARNING` and exits successfully, and the pod starts and serves its
own build. It also stops after 300 seconds in all, so a hung volume cannot keep
the pod initialising. Only the open tabs of earlier builds lose anything: they recover
by reloading, as with the store off. This matters most under `Recreate`,
where no old pod is left serving.

The store covers files a build already had in the volume, so it protects
upgrades from the first release that ran with it on, not the upgrade that
turns it on. Every file is written under a temporary name and renamed into
place, so nginx never serves a partial file. Pods that start at the same
time take turns through a lock directory on the volume. The pod holding it
updates a heartbeat in it every 15 seconds; a waiting pod breaks the lock
only after watching that heartbeat stand still for 60 seconds of its own
time, and gives up waiting after 180 seconds. No two pods' clocks are
compared, so clock skew between nodes does not matter. A pod frozen for
longer than 60 seconds while holding the lock, as a node freeze can do,
loses it, and on waking it notices at its next step and registers nothing.
NFS offers no fencing, so a check and the step after it are never one: if the
freeze falls exactly between the check and a delete, that one delete still
happens, and another build can be left without one of its files. A tab that
asks for it gets a 404, which the dashboard handles by reloading the page,
and the file stays missing until a pod of that build starts again and copies it back. nginx never serves a partial file.

### Avatars

```yaml
dashboard:
  avatars:
    gravatar: false
```

| Value | Behavior |
|-------|----------|
| `false` (default) | The user menu shows the signed-in user's initials. The browser makes no avatar request. |
| `true` | The user menu loads the user's picture from `www.gravatar.com`. The user's browser makes the request and sends a SHA-256 hash of the user's email. The initials show if the image does not load. |

`airGapped: true` fails the render when `gravatar` is `true`. Earlier releases
loaded Gravatar avatars with no setting to turn them off.

### Ingress

```yaml
dashboard:
  ingress:
    enabled: false
    className: ""  # Empty uses the cluster's default IngressClass
    annotations: {}
    host: dashboard.example.com
    tls:
      - secretName: dashboard-tls
        hosts:
          - dashboard.example.com
```

The Ingress, and the HTTPRoute the chart creates for the dashboard on the
external or internal Gateway, send three path prefixes to management-api's
Service (`managementApi.service.port`, 8080) and everything else to the
dashboard: `/api/v1/agents/` (agent consoles), `/api/v1/lsp` (the Arena editor's
language server) and `/api/v1/sandboxes/` (a browser sign-in's live view). These
are the browser's WebSockets. management-api serves them and authenticates
each from the browser's own session cookie, proxy headers or API key. The
dashboard has no WebSocket port.

A proxy in front of these routes must pass the browser's `Origin` and `Host`
through, or name the host as `X-Forwarded-Host`. management-api refuses an
upgrade whose `Origin` is not `dashboard.baseUrl`, or, with that unset, not on
the request's host name.

```yaml
dashboard:
  wsProxy:
    url: ""  # Base URL the browser opens these WebSockets against
```

Leave `dashboard.wsProxy.url` empty unless the WebSockets are served at a
different origin from the pages. The empty default uses the page's own
origin, which the routes above send to management-api. Local development sets
`ws://localhost:3002`, which Tilt forwards to management-api.

### Authentication

```yaml
dashboard:
  auth:
    # Mode: anonymous, proxy, oauth, or builtin. No default: the chart
    # fails to render until you set one.
    mode: oauth

    # Required to render with mode: anonymous; the chart fails without it.
    allowAnonymous: false

    # Session configuration. Both are optional: left empty, the chart
    # generates a secret on first install and keeps it across upgrades.
    sessionSecret: ""           # A literal. Visible in `helm get values` — avoid in production.
    existingSessionSecret: ""   # Name of a Secret with the key OMNIA_SESSION_SECRET. Preferred.
    cookieName: omnia_session
    ttl: 86400  # 24 hours
    cliTokenTtlSeconds: 3600  # TTL of tokens minted for the deploy CLI login

    # Role mapping
    anonymousRole: viewer
    roleMapping:
      adminGroups: []
      editorGroups: []
```

#### Proxy Authentication

For use with external authentication proxies (e.g., oauth2-proxy):

```yaml
dashboard:
  auth:
    mode: proxy
  proxy:
    headerUser: X-Forwarded-User
    headerEmail: X-Forwarded-Email
    headerGroups: X-Forwarded-Groups
    headerDisplayName: X-Forwarded-Preferred-Username
```

#### OAuth Authentication

For direct OAuth/OIDC integration:

```yaml
dashboard:
  auth:
    mode: oauth
  oauth:
    provider: generic  # generic, google, azure or okta
    issuerUrl: https://your-tenant.auth0.com/  # Required for generic
    clientId: your-client-id
    clientSecret: your-client-secret  # Or use existingSecret
    existingSecret: ""  # Secret with OMNIA_OAUTH_CLIENT_ID and OMNIA_OAUTH_CLIENT_SECRET
    scopes: ""  # Comma-separated; empty uses the provider's preset
    azureTenantId: ""  # azure provider
    oktaDomain: ""     # okta provider
    claims:            # Each empty field uses the provider's preset
      username: ""
      email: ""
      displayName: ""
      groups: ""
```

### API Keys

API keys authenticate programmatic clients (CI, the deploy adapter, scripts).
The store backend is selected with `dashboard.apiKeys.store`. See
[Durable API keys](/how-to/security/api-keys/) for a full walkthrough of the
three stores.

```yaml
dashboard:
  apiKeys:
    enabled: true                # Enable API key authentication
    # Store backend:
    #   memory   — ephemeral, dev only (keys lost on restart)
    #   file     — read-only Kubernetes Secret, GitOps-friendly
    #   postgres — durable, keys (bcrypt-hashed) live in Postgres
    store: memory

    # File store (store: file)
    filePath: /etc/omnia/api-keys/keys.json  # Where the keys Secret is mounted
    keysSecret: ""                           # Name of an existing Secret holding keys.json

    # Postgres store (store: postgres)
    postgresUrl: ""             # Connection URL; falls back to builtin.postgresUrl when empty
    existingPostgresSecret: ""  # Existing Secret with an OMNIA_AUTH_API_KEYS_POSTGRES_URL key

    maxPerUser: 10        # Maximum keys per user
    defaultExpiration: 90 # Default expiration in days (0 = never)
```

A `store` left at `memory` is auto-promoted to `postgres` when a
`postgresUrl` or `existingPostgresSecret` is set, so a wired durable backend
is never silently ignored.

### Management API sign-in and keys

management-api is the Go service that serves the dashboard's whole API. It
serves every sign-in route and decides who each request is from. It reads the
dashboard's auth values above (`dashboard.auth`, `dashboard.oauth`,
`dashboard.proxy`, `dashboard.session`, `dashboard.apiKeys` and
`dashboard.builtin`), under the same variable names, so there is nothing to
configure twice and nothing to switch on.

```yaml
managementApi:
  servesManagementPlaneKeys: true
  acceptLegacyIdentityTokens: false
  database:
    existingSecret: ""  # Secret with a POSTGRES_CONN key: management-api's own database
```

| Value | Default | Description |
|-------|---------|-------------|
| `managementApi.servesManagementPlaneKeys` | `true` | Point the operator's `--mgmt-plane-jwks-url` (and so every facade) at management-api's `/api/v1/auth/jwks`, and Doctor's and arena-controller's service-token mint at its `/api/v1/auth/service-token`. Must stay `true` with the dashboard enabled, which serves no keys: the chart refuses `false`. Ignored when management-api is not deployed |
| `managementApi.acceptLegacyIdentityTokens` | `false` | Accept an identity token minted by a dashboard pod from before management-api took over authentication. Off, a request carrying one is resolved from its cookie, proxy headers or API key alone, and the token is ignored. Set `true` only for an upgrade from a build whose dashboard still mints these tokens (a `main` build from before the cut-over; no release is one), so dashboard pods that have not rolled yet keep working, then set it back to `false`. Those tokens verify against the dashboard's old keypair, which the chart does not trust by itself, so list `<release>-dashboard-signing-keypair` in `managementApi.signingKey.additionalCertSecrets` for the same upgrade |
| `managementApi.database.existingSecret` | `""` | management-api's own database, holding builtin users and, when `dashboard.apiKeys` names no database, API keys |

The chart refuses to render when:

- `managementApi.replicaCount` is above 1 and no Redis session store resolves
  (`redis.default` or `dashboard.session.redis`). management-api writes the
  sessions and reads them back, and the session cookie names a record in the
  store, so every replica must read the same one. One replica keeps them in
  memory.
- `dashboard.auth.mode` is `builtin` and `managementApi.database.existingSecret`
  is empty, because management-api keeps builtin users in its own database.

The dashboard keeps no user store and signs nothing, so these values are gone or going.
Each still renders when an upgrade's values file sets it, and the install notes
name it:

| Value | Status |
|-------|--------|
| `dashboard.builtin.storeType`, `dashboard.builtin.sqlitePath` | Removed. They chose the dashboard's user store |
| `dashboard.persistence` | Deprecated, and mounts nothing. For one release the chart still renders `<release>-dashboard-data` when `enabled` is `true` and `existingClaim` is empty, now annotated `helm.sh/resource-policy: keep`: v0.9.0-beta.20 created the claim without that annotation, and Helm reads it from the live object, so dropping the claim outright would delete the old users. Delete the claim yourself once you will not roll back. A claim named by `existingClaim` was never the chart's, and the chart leaves it alone. An install of v0.9.0-beta.20 with `enabled: true` must upgrade through this release, not past it: the release after drops the claim, and Helm deletes a claim that never got the annotation |
| `dashboard.signingKey` | Removed. The chart neither renders nor trusts the dashboard's keypair. The `<release>-dashboard-signing-keypair` Secret an earlier release created is kept, so delete it yourself, or list it in `managementApi.signingKey.additionalCertSecrets` to keep trusting tokens it signed through a rollout |

management-api holds the only management-plane signing key. Its JWKS publishes
that key and any `managementApi.signingKey.additionalCertSecrets`.
`dashboard.jwksUrl`, `dashboard.serviceTokenUrl` and `doctor.mgmtPlaneToken.url`
still win when set.

### Branding (White-label)

White-label branding is an **enterprise feature**. The chart emits
`NEXT_PUBLIC_BRAND_*` environment variables in the dashboard ConfigMap, which management-api reads, when
`enterprise.enabled` is `true` and `dashboard.branding.productName` is set.
Helm cannot evaluate a runtime license, so the license's `whiteLabel`
entitlement is enforced afterward, at runtime, by management-api before `/api/v1/config` hands the brand to the browser. Leave
`productName` empty to keep the default Omnia brand.

```yaml
dashboard:
  branding:
    productName: ""    # Product name in the sidebar, page title and auth screens
    logoDarkUrl: ""    # Logo URL (sidebar and link-preview card)
    faviconUrl: ""     # Favicon URL
    colors: {}         # Colour map, both themes (primary, accent, sidebar, ...)
    colorsDark: {}     # Colour map overrides for the dark theme
    fonts:
      family: ""       # Font family name applied to --font-sans
      mono: ""         # Monospace family applied to --font-mono
      url: ""          # Optional web-font stylesheet URL to load the family
    links:
      sales: ""        # Sales contact (email or URL)
      upgradeUrl: ""   # Upgrade/enterprise URL shown on license gates
    customCss: ""      # Token-scoped custom CSS appended to :root only
```

For the accepted colour keys, precedence, content-security-policy rules and
asset serving, see
[Dashboard branding reference](/reference/platform/dashboard-branding/). For the
steps, see
[White-label the dashboard](/how-to/operations/white-label-the-dashboard/).

## Observability Stack

All observability components are optional and disabled by default.

### Prometheus

```yaml
prometheus:
  enabled: false
  server:
    persistentVolume:
      enabled: false  # Enable for production
    prefixURL: /prometheus
    baseURL: /prometheus
  alertmanager:
    enabled: false
  prometheus-pushgateway:
    enabled: false
  prometheus-node-exporter:
    enabled: false
  kube-state-metrics:
    enabled: false
```

### Grafana

```yaml
grafana:
  enabled: false
  adminPassword: admin  # Change in production
  grafana.ini:
    server:
      root_url: "%(protocol)s://%(domain)s:%(http_port)s/grafana/"
      serve_from_sub_path: true
  sidecar:
    dashboards:
      enabled: true
      label: grafana_dashboard
      searchNamespace: ALL
    datasources:
      enabled: true
      label: grafana_datasource
      searchNamespace: ALL
  service:
    type: ClusterIP
```

### Loki

:::note[Docker Desktop]
Loki's ruler component can fail with "no space left on device" on Docker
Desktop with the local-path provisioner. The ruler is disabled by default to
avoid this.
:::

```yaml
loki:
  enabled: false
  deploymentMode: SingleBinary
  loki:
    auth_enabled: false
    useTestSchema: true
    storage:
      type: filesystem
    commonConfig:
      replication_factor: 1
  singleBinary:
    replicas: 1
    persistence:
      enabled: true
      size: 10Gi
  backend:
    replicas: 0
  read:
    replicas: 0
  write:
    replicas: 0
  gateway:
    enabled: false
  chunksCache:
    enabled: false
  resultsCache:
    enabled: false
  ruler:
    enabled: false
```

Enable the ruler for log-based alerting with a writable `emptyDir` volume:

```yaml
loki:
  ruler:
    enabled: true
  singleBinary:
    extraVolumes:
      - name: rules
        emptyDir: {}
    extraVolumeMounts:
      - name: rules
        mountPath: /var/loki/rules
```

### Alloy

```yaml
alloy:
  enabled: false
  alloy:
    configMap:
      content: |
        // Kubernetes pod discovery and log collection
        // See values.yaml for full configuration
```

### Tempo

```yaml
tempo:
  enabled: false
  tempo:
    reportingEnabled: false
  tempoQuery:
    enabled: false
  persistence:
    enabled: true
    size: 5Gi
```

## Gateway API

### External Gateway

For exposing agents externally:

```yaml
gateway:
  enabled: false
  name: agents
  className: istio
  listeners:
    http:
      port: 80
      protocol: HTTP
    https:
      enabled: false
      port: 443
      protocol: HTTPS
      tlsSecretName: ""
  annotations: {}                 # On the Gateway resource only
  infrastructureAnnotations: {}   # spec.infrastructure.annotations
```

`annotations` land on the Gateway resource. Cloud load-balancer annotations
(static IP, resource group) belong in `infrastructureAnnotations`: the Gateway
API controller (Gateway API v1.1+) forwards them to the Service it provisions.

### Per-agent exposure

`defaultExposure` has the operator create an HTTPRoute for each agent that
sets `facades[].expose.enabled`. The chart cannot template these routes,
because agents are created at runtime. Exposure adds no authentication;
`spec.externalAuth` still decides who may connect.

```yaml
defaultExposure:
  enabled: false
  baseDomain: ""        # Required when enabled; hosts are {name}.{namespace}.{baseDomain}
  gateway:
    name: ""            # Required when enabled; e.g. the chart's {fullname}-agents
    namespace: ""       # Empty = the agent's namespace
    sectionName: ""     # Optional listener name
```

A Gateway in another namespace needs a ReferenceGrant, which the operator does
not create. See [Expose agents](/how-to/agents/expose-agents/).

### Internal Gateway

For observability tools:

```yaml
internalGateway:
  enabled: false
  name: internal
  className: istio
  port: 8080
  grafana:
    enabled: true
    path: /grafana
  prometheus:
    enabled: true
    path: /prometheus
```

## Istio Integration

```yaml
istio:
  enabled: false
  tempoService: omnia-tempo.omnia-system.svc.cluster.local
  tempoPort: 4317
```

## Authentication

`authentication.enabled` renders an Istio `RequestAuthentication` and two
`AuthorizationPolicy` resources that require a valid JWT on agent pods. It
requires `istio.enabled: true`.

These policies cover agent pods in the release namespace only. The chart
renders them in the namespace you install Omnia into, and Istio applies a
selector-based policy only to pods in its own namespace. Agents in workspace
namespaces are not covered, and neither is the edge-trust header strip under
`authentication.edgeTrust`. To require a JWT on an agent in any namespace, set
`spec.externalAuth.oidc` on the AgentRuntime. See
[Configure authentication](/how-to/security/configure-authentication/).

```yaml
authentication:
  enabled: false
  jwt:
    issuer: ""  # e.g., https://your-tenant.auth0.com/
    jwksUri: ""  # Defaults to {issuer}/.well-known/jwks.json
    audiences: []
    forwardOriginalToken: true
    outputClaimToHeaders: []
    # - header: x-user-id
    #   claim: sub
  authorization:
    requiredClaims: []
    # - claim: "scope"
    #   values: ["agents:access"]
    excludePaths:
      - /healthz
      - /readyz
```

## KEDA

Advanced autoscaling with scale-to-zero support.

:::caution[Existing KEDA installation]
If KEDA is already installed in the cluster, keep `keda.enabled=false` to
avoid CRD ownership conflicts. The chart detects an existing KEDA
installation and fails with an error if you enable the subchart anyway.
Omnia's ScaledObject resources work with that existing installation.
:::

```yaml
keda:
  enabled: false  # Set to true only if KEDA is not already installed
  operator:
    watchNamespace: ""  # Empty = all namespaces
  prometheus:
    serverAddress: "http://omnia-prometheus-server.omnia-system.svc.cluster.local"
```

## Sandbox Configuration

### Sandbox API

Agent runtimes get their sandboxes from the operator's Sandbox API. The
operator creates every `SandboxClaim` an agent uses. The API runs on every operator replica and is exposed as the
`sandbox-api` port of the `<release>-operator` Service. The chart passes that
Service's URL to the operator, which sets it as `OMNIA_SANDBOX_API_URL` on the
runtime container of every AgentRuntime with `spec.sandbox.enabled`.

```yaml
operator:
  sandboxAPI:
    port: 8087
    claimPolicyGrace: 60s
    mountRoles:
      - name: project
        path: "arena/workspaces/{id}/{user}"
        mustExist: true
        minRole: editor
```

`operator.sandboxAPI.claimPolicyGrace` is the time after the chart's claim
admission policy is created during which the Sandbox API holds back a sandbox
that mounts a project, binds a Connection or syncs back.

`operator.sandboxAPI.mountRoles` are the mount roles a session may declare on
its sandbox, beside the built-in `scratch` and `session`. The chart renders
them as the operator's `--sandbox-mount-roles` flag, and the operator refuses
to start when one is invalid. A Workspace replaces a role by name, or adds
one, in [`spec.sandboxMountRoles`](/reference/core/workspace/#sandboxmountroles).
The default is one role, `project`, on which the Arena builder declares its
project mount.

| Field | Default | Description |
|---|---|---|
| `name` | — | The role a declaration and a class's `mounts` name. A DNS label, not `scratch` or `session` |
| `path` | — | The directory a mount of this role binds, relative to the workspace's content. Exactly one whole segment is `{id}`, the id the declaration names. Not absolute, no `..`, never reaching or containing `sandboxes/`, `sessions/`, `arena/sources/`, `arena/snapshots/`, `arena/deployed/`, `arena/template-sources/`, `arena/template-indexes/`, `skills/` or `manifests/`, and not overlapping another role's path |
| `mustExist` | `true` | Refuse a declaration whose directory is not there (`MountNotFound`). `false` has the operator create it as the claim binds |
| `minRole` | `editor` | The least workspace role (`viewer`, `editor`, `owner`) a verified user needs to declare the mount |

See [SandboxClaim mount roles](/reference/core/sandboxclaim/#mount-roles).

### Sandbox class shapes

The operator knows nothing about any sandbox image, so each class the chart
ships carries its image's own shape in values: `args`, `env`, `ports`,
`filesystem` (and `readiness`, `connections`). These are the codegen class's:

```yaml
sandbox:
  handlerPort: mcp     # the class port the reference registries dial
  handlerPath: /sse    # the path on it
  codegenClass:
    args: ["-addr=:8080", "-workspace=/workspace"]
    env:
      - {name: HOME, value: /sandbox-cache}
      - {name: GOPATH, value: /sandbox-cache/go}
      - {name: GOCACHE, value: /sandbox-cache/go-build}
      - {name: GOMODCACHE, value: /sandbox-cache/go/pkg/mod}
    ports:
      - {name: mcp, port: 8080}
    filesystem:
      workspace: {mountPath: /workspace}
      scratch:
        - {name: cache, mountPath: /sandbox-cache, sizeLimit: 4Gi}
```

`promptarenaClass` has the same shape (its image is built on codegen-sandbox),
and `browserClass` its own, below. A class with no `ports` fails to render.
See [SandboxClass](/reference/core/sandboxclaim/#the-pod-shape) for each field.

### PromptArena Sandbox Class

An opt-in, second `ClusterSandboxClass` carrying the promptarena/packc CLIs,
built on the codegen-sandbox base, for authoring against a mounted
PromptArena project instead of scratch space. Off by default; a claim naming
this class gets a `project` mount bound read-write to `/workspace`, and does
not also get scratch space. See [SandboxClaim](/reference/core/sandboxclaim/).

```yaml
sandbox:
  promptarenaClass:
    enabled: false
    name: promptarena
    image:
      repository: ghcr.io/altairalabs/omnia-promptarena-sandbox
      tag: ""
    resources:
      requests:
        cpu: 100m
        memory: 256Mi
      limits:
        cpu: "2"
        memory: 2Gi
```

`image.repository` is required at render time when `enabled` is `true`; an
empty `image.tag` means the chart's appVersion. `image` is an object: the
schema refuses a plain string. `resources.limits` for `cpu` and
`memory` are mandatory: the SandboxClass CRD rejects an untrusted class without
both.

The image builds from `Dockerfile.promptarena-sandbox` in the Omnia repository
and is published with every release as
`ghcr.io/altairalabs/omnia-promptarena-sandbox`. Tilt runs with
`ENABLE_SANDBOXES=true` build it locally.

### Browser Sandbox Class

An opt-in `ClusterSandboxClass` for a browser: structured browser control over
MCP (agent-browser behind an allowlisting bridge) on a headless Chromium, with
no shell. Its values give it a read-only root, no workspace, and an in-memory
`/tmp` for the browser profile. It has no egress, so it reaches nothing until
Connections bind hosts to a claim.

```yaml
sandbox:
  browserClass:
    enabled: false
    name: browser
    image:
      repository: ghcr.io/altairalabs/omnia-browser-sandbox
      tag: ""          # empty = the chart's appVersion
    resources:
      requests:
        cpu: 250m
        memory: 512Mi
      limits:
        cpu: "2"
        memory: 2Gi
    runtimeClassName: ""   # gVisor or Kata, where the nodes support one
    args: ["-addr=:8080"]
    env:
      - {name: HOME, value: /tmp/home}
    ports:
      - {name: mcp, port: 8080}
    filesystem:
      readOnlyRoot: true
      scratch:
        - {name: tmp, mountPath: /tmp, sizeLimit: 512Mi, medium: Memory}
    connections:           # roles; a role taking a browserSession declares `session`
      - role: site
        session:
          stateFile: /tmp/state/state.json
          args:
            - "-connection={{.Connection}}"
            - "-state-required"
            - "{{if .SignedOutPrefixes}}-signed-out-prefixes={{.SignedOutPrefixes}}{{end}}"
            - "{{if .StateFile}}-state-file={{.StateFile}}{{end}}"
    toolRegistry:
      enabled: false
      name: browser-sandbox
      allowedTools: [agent_browser_open, agent_browser_read, ...]  # all 33 by default
```

`connections` declares the `site` role. An AgentRuntime binds a Connection to
it in `spec.sandbox.connections`; any credential kind binds, and a
`browserSession` is delivered as `session` says. Unbound, the browser is
anonymous and reaches nothing.

`toolRegistry.enabled` installs a ToolRegistry named `toolRegistry.name` in
`sandbox.toolRegistry.namespace`. Its one sandbox, `browser`, names this class
and binds no Connection. Its one `mcp` handler dials the class's `mcp` port over
SSE, with `toolRegistry.allowedTools` as its allowlist. The default list is
every tool the browser bridge serves. The chart fails to render the registry
without `browserClass.enabled`, a `name`, or at least one tool. See
[Give an agent a browser](/how-to/tools/give-an-agent-a-browser/).

Chrome runs with `--no-sandbox`, because restricted Pod Security denies it the
user namespaces its own sandbox needs. The pod is the containment, and a
sandboxed RuntimeClass is the recommended next rung.

#### Browser sign-in class

With the browser class on, the chart also installs `browser-enrol`, the class
a person signs in to a browserSession Connection in (an
[enrolment](/reference/core/sandboxclaim/#enrolment)). It runs the browser
class's image, tag, resources and RuntimeClass, with its own port, because a
class's ports cannot change. With it installed, the operator serves its
enrolment endpoint and admits management-api's pods, and nothing else, to an
enrolment's view port.

```yaml
sandbox:
  browserClass:
    enrolment:
      enabled: true
      name: browser-enrol
      maxPerWorkspace: 5   # live enrolments per workspace
      maxDuration: ""      # 1m-20m; empty is 15m
      args: ["-addr=:8080"]
      env:
        - {name: HOME, value: /tmp/home}
      ports:
        - {name: view, port: 8080}
      view: {port: view}
      readiness: {port: view, httpGet: {path: /healthz}}
      filesystem:
        readOnlyRoot: true
        scratch:
          - {name: tmp, mountPath: /tmp, sizeLimit: 512Mi, medium: Memory}
      connections:
        - {role: site, session: {}}
      enrolmentArgs: [...]  # the bridge's interactive flags, templated
```

`enrolmentArgs` becomes the class's `spec.enrolment.args`; see
[Enrolment classes](/reference/core/sandboxclaim/#enrolment-classes) for what
they may use. The chart fails to render a class without them.

#### Connections: the browser-session key

The dashboard offers **Sign in with a browser** only when this class is
installed under Enterprise and the install names the KMS key browser
Connections seal their sessions with. management-api writes that key into each
browser Connection created in the dashboard, so nobody is asked about KMS.

```yaml
connections:
  browserSession:
    encryption:
      kmsProvider: aws-kms            # aws-kms, azure-keyvault, gcp-kms, vault or local
      keyID: alias/omnia-browser-sessions
      secretRef: {name: ""}           # a Secret in each workspace namespace; omit for workload identity
```

`local` keeps the key in a Kubernetes Secret and is for development only
(`hack/dev-kms-secret.sh`); the chart fails to render `local` without
`secretRef.name`.

### Sandbox egress proxy (test-only switch)

Every sandbox whose Connections name hosts gets its own egress proxy, which
dials public addresses only. The one setting here is for end-to-end test
clusters, which need a sandbox's browser to sign in to an in-cluster TLS login
fixture. Never set it on an install that serves real users.

```yaml
sandbox:
  egressProxy:
    insecureTestHosts: []              # [{host, namespace, service}], TEST ONLY
    acknowledgeInsecureTestOnly: false # must be true, or rendering fails
    insecureTestSPKI: ""               # base64 SHA-256 of the fixture cert's SPKI
```

Each entry names a fixture host, a lowercase DNS name, and the Service serving
it on port 443; `namespace` and `service` are DNS-1123 labels, and the chart
fails to render anything else. The proxy dials the Service's cluster IP and
never resolves the host, so no DNS rewrite is needed for it. The chart passes
them to the operator as `--sandbox-egress-insecure-test-hosts`, and prints a
warning in its notes. See
[Test-only: in-cluster fixture hosts](/reference/core/connection/#test-only-in-cluster-fixture-hosts)
for what the switch does and does not open.

## Demo content

The `omnia` chart installs no demo agents. The public demos ship as the
`omnia-demo` chart, at `charts/omnia-demo` in the
[AltairaLabs/omnia-demo](https://github.com/AltairaLabs/omnia-demo)
repository. Its values are documented there.

## Example Configurations

See [Configure Helm for common scenarios](/how-to/operations/configure-helm/)
for minimal, production, and observability-only value sets.

## Related pages

- [Getting started](/tutorials/getting-started/)
- [Install with Helm](/how-to/operations/install-helm/)
- [Configure Helm for common scenarios](/how-to/operations/configure-helm/)
- [Configure Media Storage](/how-to/operations/configure-media-storage/)
- [Durable API keys](/how-to/security/api-keys/)
- [Installing a License](/how-to/operations/install-license/)
- [White-label the dashboard](/how-to/operations/white-label-the-dashboard/)
- [Arena Fleet](/explanation/evaluation/arena-fleet/)