Skip to content

AgentRuntime CRD

The AgentRuntime custom resource defines an AI agent deployment in Kubernetes.

apiVersion: omnia.altairalabs.ai/v1alpha1
kind: AgentRuntime

Short names: agent, ar (e.g. kubectl get agent).

Selects the runtime shape. Defaults to agent when unset, preserving the pre-Functions behaviour. See Define Functions for when to pick which.

ValueBehaviour
agentLong-lived conversational runtime. Default. Allows only websocket, a2a and custom facades.
functionOne-shot HTTP runtime served over POST /functions/{name}. Requires inputSchema + outputSchema. Allows only rest and mcp facades, and requires exactly one rest facade.
spec:
mode: function

JSON Schema (Draft 2020-12 unless the schema’s $schema says otherwise) the function’s request body is validated against. Required when mode: function; forbidden when mode: agent (CEL-gated). Stored as a raw JSON object — the facade compiles it with santhosh-tekuri/jsonschema/v6 at startup and validates every request; the runtime is schema-agnostic.

spec:
inputSchema:
type: object
required: ["text"]
properties:
text:
type: string

JSON Schema the function’s response is validated against before being returned to the caller. On output-validation failure the facade returns HTTP 502 output_invalid with the raw model output embedded so the pack author can debug schema drift (no in-runtime retry). Required when mode: function; forbidden when mode: agent (CEL-gated).

spec:
outputSchema:
type: object
required: ["summary"]
properties:
summary:
type: string

Controls how the runtime constrains the model’s response in function mode. Optional; forbidden when mode: agent (CEL-gated). When unset on a function-mode runtime it defaults to json_schema.

ValueBehaviour
textFree-form generation; the facade still validates the output against outputSchema (HTTP 502 on mismatch).
jsonProvider JSON mode — valid JSON object, shape not enforced.
json_schemaProvider structured output bound to outputSchema (default).

The runtime sets the provider response format via PromptKit. If the configured provider/model cannot honour the requested format, the invocation fails (no silent fallback) — set outputFormat: text (or json), or use a provider that supports structured outputs.

spec:
mode: function
outputFormat: json_schema

Reference to the PromptPack containing agent prompts.

FieldTypeRequired
promptPackRef.namestringYes
promptPackRef.versionstringNo
promptPackRef.trackstable | prereleaseNo

name is the pack’s logical spec.packName, not a PromptPack object name: the operator lists the PromptPacks labelled with that pack name in the agent’s namespace and picks one. version pins an exact version, compared as a string with build metadata included, so 1.0.0 does not match a pack at 1.0.0+gAAAA. track follows a channel — stable selects the highest non-prerelease version, prerelease the highest version overall. version and track are mutually exclusive (CEL-validated); with neither set, the agent follows the stable channel.

spec:
promptPackRef:
name: my-prompts
version: "1.0.0"

When no PromptPack matches, PromptPackReady is False with reason PromptPackNotFound and the agent’s phase is Failed. A resolved pack that failed source or schema validation also fails the agent (PromptPackInvalid).

A pack name that has a live PromptPack resolves to it, and only a bare ref (no version, no track) may name it. A version or track on such a ref, or a spec.rollout whose stable pack or rollout.candidate.promptPackRef is live, sets PromptPackReady False with reason InvalidPromptPackRef and the phase to Failed. The validating webhook, when enabled, rejects the same specs at admission. A pack name with both a live pack and published versions resolves for no ref: PromptPackReady is False with reason PromptPackNameConflict.

An agent on a live pack runs its runtime in compile mode. The operator mounts the project read-only from the workspace content volume and gives the runtime a writable directory for the compiled pack. The facade tags every session kind:dev and arena-project:<id>, with the project id as the pack name. The pods carry no pack version or digest, and the agent records no composition. If the project can’t be mounted (the operator has no workspace content storage, or the workspace content claim workspace-<namespace>-content does not exist yet), DeploymentReady is False with reason AwaitingPodTemplateInputs, and the message names the project or the missing claim. No Deployment is created while held back; the controller watches the claim and re-reconciles as soon as it appears, so a Workspace whose first arena use is a dev agent starts once the claim is created rather than sitting Pending on PersistentVolumeClaimNotFound.

A list of named provider references. Each entry maps a logical name to a Provider CRD. This enables centralized credential management, consistent configuration across agents, and explicit judge provider mapping for evals.

FieldTypeRequired
providers[].namestring (pattern ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$, unique)Yes
providers[].providerRef.namestringYes
providers[].providerRef.namespacestringNo. If set, must equal the AgentRuntime’s own namespace; any other value is refused (ProviderCrossNamespace)
providers[].roleProvider roleNo (default llm)
providers[].requiredCapabilities[]capabilityNo

role is the role the referenced Provider must have: a Provider whose spec.role differs sets ProviderReady=False (reason RoleMismatch) and puts the agent in phase Failed. requiredCapabilities lists capabilities the Provider must advertise; any missing sets ProviderReady=False (reason CapabilityMismatch) and holds the agent Pending. A Provider that is not Ready also holds the agent Pending.

Every entry other than the default is wired into the runtime by its role: embedding, tts, stt, image or inference. A platform-hosted embedding Provider (spec.platform set), such as Azure OpenAI with workload identity, works as a non-default entry the same way it works as the default: the runtime authenticates it through the cloud platform with the Provider’s spec.auth. The Provider CRD admits spec.platform only on the llm and embedding roles.

The name field is a logical identifier for looking up providers by role:

NamePurpose
defaultPrimary LLM provider for the runtime
judgeLLM judge for eval execution
Any custom nameReferenced by name in PromptPack eval definitions
spec:
providers:
- name: default
providerRef:
name: claude-sonnet
- name: judge
providerRef:
name: claude-haiku

See the Provider reference for details on configuring Provider CRDs (types, secrets, defaults, etc.).

Agent framework configuration. Specifies which runtime framework the agent uses.

FieldTypeDefaultRequired
framework.typestringpromptkitNo
framework.imagestring-No
spec:
framework:
type: promptkit
image: myregistry.io/omnia-runtime:v1.0.0 # Optional image override
TypeDescription
promptkitDefault framework using PromptKit — the only framework with a built-in image
langchainA LangChain runtime image you supply; it must implement the omnia.runtime.v1 contract
customAny image you supply that implements the omnia.runtime.v1 gRPC contract

The runtime image is resolved in this order: framework.image; the operator’s --framework-image=<type>=<image> entry for the type; the built-in image (promptkit only). A langchain or custom agent with neither of the first two blocks with FrameworkReady=False, reason FrameworkImageUnavailable — the operator never substitutes the PromptKit image. See the facade ↔ runtime protocol for the contract a non-PromptKit image must serve.

A list of facade entries (1 to 4, no two of the same type). Each entry exposes the runtime over one protocol. An agent can expose several protocols at once by listing multiple entries — for example a websocket surface for browsers plus an a2a surface for agent-to-agent invocation, or a rest surface plus an mcp surface for a function.

FieldTypeDefaultRequired
facades[].typestring (websocket | a2a | rest | mcp | custom)websocketYes
facades[].portinteger (1–65535)8080No
facades[].handlerstringruntimeNo
facades[].imagestring-No (required for type: custom)
facades[].extraEnv[]EnvVar-No
facades[].drainTimeoutduration string30sNo
facades[].clientToolTimeoutduration60sNo
facades[].managementPlanebooleantrueNo
facades[].dataPlanebooleantrueNo
facades[].a2aobject-No (only for type: a2a)
facades[].mcpobject-No (only for type: mcp)
facades[].asyncobject-No (function mode)
facades[].exposeobject-No

Each entry’s type must match how the runtime is invoked (CEL-validated). mode: agent runtimes use websocket, a2a and/or custom; mode: function runtimes serve HTTP at POST /functions/{name} and must have exactly one rest facade, optionally alongside an mcp facade to expose the function as an MCP tool.

handler, drainTimeout and clientToolTimeout are read from the primary facade — websocket if present, else rest, else a2a. drainTimeout is how long the facade keeps serving active realtime calls after SIGTERM before tearing the rest down; new calls stop immediately. clientToolTimeout is the maximum wait for a client tool’s result per turn. A call that is not answered in time is denied, and the turn continues. extraEnv adds environment variables to the facade container (e.g. LOG_LEVEL=debug). Add LOG_SAMPLING=off to keep every log line. By default the log keeps 1 in 100 repeats of a message per second, so under load a line can be missing for an event that happened.

The facade cancels a turn when the runtime sends nothing for 120 seconds (runtime stream inactivity timeout). The runtime sends a heartbeat every 30 seconds while a text turn runs, so a long server-side tool call, such as a sandbox cold start, does not trip it; only a runtime that has stopped answering does. To change the window, set OMNIA_RUNTIME_STREAM_INACTIVITY_TIMEOUT (a duration, at least the default 120s) in the primary facade’s extraEnv. A value that does not parse or is below 120s stops the facade from starting.

A function-mode facade also reads two bounds on Accept: text/event-stream invocations from extraEnv. OMNIA_FUNCTION_STREAM_MAX_DURATION (a duration, default 270s, at most 290s) is the longest one stream may run; at the limit the run is cancelled and the stream ends with a timeout error event. OMNIA_FUNCTION_STREAM_MAX_CONCURRENT (a positive integer, default 32) is how many streams the facade serves at once; over it a request gets 429. A value that does not parse or is out of range stops the facade from starting. See Define functions.

extraEnv is valid only on the primary facade (CEL-validated). Every facade in the pod runs in one facade container, so there is one environment to set, and the API server rejects the field on an a2a entry beside websocket, or on an mcp entry.

expose is per facade. Each facade has its own Service port (facade 8080, a2a 9999, mcp 9998 by default), and each facade that sets expose.enabled gets its own operator-managed HTTPRoute to that port: <agent>-facade for the primary facade on <agent>.<namespace>.<baseDomain>, and <agent>-<type> for a secondary one on <agent>-<type>.<namespace>.<baseDomain>. Set expose.host on either to override. Every route matches the root path, so two of an agent’s facades must not share a host: the Gateway would serve only the older route.

OMNIA_PACK_MODE, OMNIA_PACK_SOURCE_DIR, OMNIA_PACK_CONFIG_FILE, OMNIA_PACK_OUT_DIR, OMNIA_SESSION_TAGS, OMNIA_SESSION_PACK_NAME and OMNIA_IDLE_TIMEOUT are reserved. They select compile mode, stamp dev-session identity on session records, or switch on idle renewal (see Idle reaping), and only a controller sets them.

  • When the AgentRuntime admission webhook is enabled, it rejects an AgentRuntime that names one of these keys in facades[].extraEnv, runtime.extraEnv or podOverrides.extraEnv. On an update it rejects only a key that was not already there.
  • Whether or not the webhook runs, the operator drops these entries from the pod, and the AgentRuntime’s ReservedEnvClean condition names the key.
  • A value for one of these keys from a podOverrides.extraEnvFrom ConfigMap or Secret is overridden. The operator sets OMNIA_PACK_MODE=static on the runtime container and empty OMNIA_SESSION_* values on the facade container as explicit env entries, and Kubernetes prefers those over envFrom.

type: custom is a bring-your-own facade container: it speaks the facade↔runtime gRPC contract inward and any protocol outward. It is a long-lived connection surface, valid only in mode: agent (like websocket), and requires facades[].image to point at your container. Custom facades are an enterprise-licensed capability, enforced at admission.

A custom facade must be the only entry in spec.facades (CEL-validated). It replaces the built-in facade container, so beside a websocket or a2a entry its image would never run. A custom container receives neither drainTimeout nor clientToolTimeout. A custom primary’s drainTimeout still sets the pod’s terminationGracePeriodSeconds: the drain timeout (30s when unset, at most 600s) plus 30s.

spec:
facades:
- type: websocket
port: 8080
handler: runtime
image: myregistry.io/omnia-facade:v1.0.0 # Optional override
ModeDescriptionCalls the runtime
runtimeProduction mode using the runtime frameworkYes
demoDemo mode with simulated streaming responsesNo
echoSimple echo handler for testing connectivityNo

Controls whether this facade gets an internal management-plane twin listener: the one the dashboard’s “Try this agent” view and other in-cluster control-plane callers dial. Defaults to true: the operator allocates an internal port (18080 for the WebSocket / function surface, 19999 for A2A, 19998 for MCP), publishes it under status.managementEndpoints, and the facade serves a management-plane-only auth chain there. Set false for an external-only facade: no internal listener, no *-mgmt Service port, no status entry. The external listener is unaffected either way — it runs only the data-plane validators from spec.externalAuth and never accepts management-plane tokens. Because the flag is per facade, you can keep the management plane on one surface (e.g. the WebSocket debug view) while isolating another (e.g. an external A2A endpoint).

spec:
facades:
- type: websocket
managementPlane: true # dashboard debug view works
- type: a2a
managementPlane: false # external A2A surface, data-plane only
a2a:
taskTTL: "1h"

A facade with managementPlane: false and no data-plane validator configured can accept no callers at all; the ExternalAuth condition reports it.

Controls whether this facade gets its external, data-plane listener at all: the ordinary client-facing port (facade / a2a / mcp Service port) that spec.externalAuth and facades[].expose govern. Defaults to true. Set false for a facade that must be reachable only over the management-plane twin: an internal-only agent used solely from the dashboard (a dev/authoring agent is the motivating case, but this is a general option; any agent can set it). No external Service port is created for this facade, the facade container’s external port is omitted, and the facade process never binds it. Readiness and liveness are unaffected: they run on the separate health port, not the data-plane one.

dataPlane: false requires managementPlane to not be false on the same facade (CEL-validated) — a facade with both planes off would be reachable by nothing at all. facades[].expose.enabled is ignored when dataPlane is false: with no external Service port to route to, exposure would point an HTTPRoute at a backend port that does not exist, so the operator never creates the route.

This is a per-facade field, not a primary-facade one: on a dual-protocol agent (websocket + a2a) or a function agent (rest + mcp), each facade’s own dataPlane governs only that facade’s own external port. Disabling it on a secondary a2a or mcp facade removes only that facade’s a2a / mcp Service port and container port, and stops the facade process binding only that listener; the primary facade’s data plane is unaffected.

spec:
facades:
- type: websocket
dataPlane: false # management-plane-only; managementPlane stays true (default)
spec:
facades:
- type: websocket # stays externally reachable
- type: a2a
dataPlane: false # a2a is management-plane-only; websocket is untouched
a2a: {}

A2A (agent-to-agent) configuration for a type: a2a facade entry; the entry’s presence is what turns A2A on.

FieldTypeDefaultDescription
portinteger9999Listener port when a websocket facade is also present. When a2a is the only agent-mode facade it is the primary listener, on the facade’s own port (default 8080)
agentCardobjectname onlyAgent Card served without authentication at /.well-known/agent-card.json and /.well-known/agent.json: name (required), description, version, organization, skills[] (id, name, description, tags, examples), defaultInputModes, defaultOutputModes. capabilities is ignored: the card always advertises streaming and never push notifications. Its securitySchemes come from spec.externalAuth (clientKey, oidc, edgeJWT), and it is served in the A2A version the caller asks for (1.0 or 0.3)
taskTTLduration string1hHow long completed/failed/canceled tasks are retained
conversationTTLduration string30mDeprecated and ignored: the A2A facade holds no conversations
taskStore.typememory | redismemoryTask persistence backend. Use redis when the agent runs more than one replica: in memory, tasks/get, tasks/cancel and tasks/subscribe work only on the replica that created the task, and the A2ATaskStoreShared condition turns False. With Redis, a cancel stops the turn on whichever replica runs it and a subscriber on any replica sees the task’s updates. An unreachable Redis fails facade startup rather than falling back to memory
taskStore.redisURLstring-Redis URL for type: redis
taskStore.redisSecretRef.namestring-Secret whose url key holds the Redis URL; takes precedence over redisURL
clients[]list-Deprecated: use spec.a2aClients, which takes the same entries and does not need an a2a facade. Still honoured while spec.a2aClients is unset, with an admission warning; setting both is refused
spec:
facades:
- type: websocket
- type: a2a
a2a:
port: 9999
taskTTL: "1h"

MCP (Model Context Protocol) configuration for exposing a function as a typed MCP tool. Present only on a type: mcp facade entry, listed alongside the rest facade of a function-mode runtime; the entry’s presence is what turns the MCP server on, and it listens on mcp.port (default 9998). See Expose Functions as MCP Tools.

spec:
facades:
- type: rest
port: 8080
- type: mcp
mcp:
port: 9998 # defaults to 9998

Fire-and-forget invocation for a function: POST /functions/{name}?mode=async answers 202 with an invocation_id, and GET /functions/{name}/invocations/{id} returns its state and output. The work goes on a durable Redis-backed queue and is run by any replica of the agent.

FieldTypeDefaultDescription
async.enabledbooleanfalseTurns on the async routes
async.workersinteger (1–64)1Invocations each pod runs concurrently (per pod, so the real ceiling is workers × replicas)

Async requires a queue backend: the operator’s Redis, from the chart’s redis.default as url, host or existingSecret. With existingSecret, the Secret sits in the install namespace, so the operator copies its URL into agent-<name>-async-redis (key url) in the agent’s namespace. That Secret is owned by the AgentRuntime, is rewritten when the source changes (rolling the pods), and is deleted when async is turned off. If the source Secret or its key is missing, the condition is False with reason QueueSecretUnavailable. Without any backend the AsyncInvocationReady condition is False (reason QueueBackendMissing) and the async routes answer 501; the operator never falls back to an in-process queue, which would let two replicas run the same invocation. Without async.enabled the async routes also answer 501. An invocation whose worker dies mid-run ends as interrupted rather than being retried.

Opts the agent into operator-provisioned external exposure.

FieldTypeDefaultDescription
expose.enabledbooleanfalseCreate a host-based HTTPRoute targeting this agent’s facade Service
expose.hoststring{name}.{namespace}.{baseDomain}Hostname override; must be one the Gateway’s listener accepts

The route is created only when the platform has a default-exposure Gateway configured (Helm defaultExposure); otherwise enabled does nothing. The resulting URLs appear under status.facade.endpoints. Exposure adds no authentication — spec.externalAuth is still the gate.

Optional reference to a ToolRegistry resource.

FieldTypeRequired
toolRegistryRef.namestringYes (when toolRegistryRef is set)
toolRegistryRef.namespacestringNo

Cross-namespace references are not supported: namespace must be omitted or equal the AgentRuntime’s own namespace. The agent pod’s Role is namespace-scoped, so a foreign registry would be unreadable from the pod. An AgentRuntime naming another namespace gets ToolRegistryReady=False with reason ToolRegistryCrossNamespace and is held Pending.

spec:
toolRegistryRef:
name: agent-tools

Other A2A agents this agent may call. Calling agents is the runtime’s work, so any agent can have clients — a WebSocket- or REST-only agent included; it does not need an a2a facade of its own. Replaces the deprecated spec.facades[].a2a.clients: an agent that sets both is refused (spec.a2aClients and the deprecated spec.facades[].a2a.clients are mutually exclusive), so one list is always the whole list.

FieldTypeDefaultDescription
namestringrequiredUnique within the list (at most 32 clients)
agentRuntimeRef.namestring-An AgentRuntime in the same namespace; resolved to its A2A endpoint. It must have an a2a facade, or the client is unresolved
agentRuntimeRef.namespacestringownOnly the agent’s own namespace; any other is refused (CrossNamespaceRef)
urlstring-An external agent’s base URL, used instead of agentRuntimeRef
exposeAsToolsbooleanfalseEach skill on the agent’s card becomes an a2a__<agent>__<skill> tool, offered without being declared in the pack. Each call is decided by the policy broker under the registry a2a, so on an Enterprise agent a ToolPolicy with selector.registry: a2a must permit it
authentication.secretRef.namestring-Secret whose token key is the bearer token sent to the agent. Mounted into the runtime as a file and read on every call, so rotating the Secret takes effect without a restart once the kubelet syncs it (typically within a minute). A missing Secret does not hold the pod: each call to the agent fails until it exists. An empty token fails each call
timeoutduration10s / 60sBounds fetching the agent’s card when a conversation opens, and each call. A remote turn longer than this fails the tool call while the remote agent runs on

Consecutive calls from one conversation to one agent continue one remote conversation: the runtime sends the contextId the agent answered the first call with, and runs those calls one at a time. That memory is per runtime pod, in memory, and dropped when the conversation ends, after 50 minutes idle or when a call fails; losing it only means the next call starts a new remote conversation. A remembered contextId the agent refuses as expired is dropped and the call resent without it. Two clients that resolve to the same endpoint are refused; the second is reported unresolved. Calls use the runtime’s HTTPS_PROXY and NO_PROXY, carry headers a ToolPolicy injects, and fail on a response body over 10 MiB. A call to an agentRuntimeRef target also carries a delegation token (X-Omnia-Delegation) naming the conversation’s end user and the calling agent, requested on every call for the target’s facade audience agent-facade:<namespace>/<target>. A url target is never sent one. An a2a facade verifies an inbound token against the operator’s JWKS and refuses one that fails with 401; see Call other agents.

Resolution is reported in status.a2a.clients[] and the A2AClientsResolved condition. An unresolved client is left out of the runtime, and the agent starts without it.

spec:
facades:
- type: websocket
a2aClients:
- name: billing
agentRuntimeRef:
name: billing-agent
exposeAsTools: true
authentication:
secretRef:
name: billing-agent-key
timeout: 2m

Optional. Says whether this runtime may hold codegen sandboxes — each a network-isolated pod exposing the full codegen tool surface (shell included) over MCP, so an agent can author and check project files without reaching the rest of the cluster by default. (A SandboxClass may open egress to specific in-cluster Services; see Sandbox security.)

It does not create one. Sandboxes come from SandboxClaim resources, which the operator creates when the runtime asks for a sandbox; this block is the envelope every claim naming this runtime is checked against. What each sandbox is (its class, instance count, allocation and sync-back target) is set on the ToolRegistry’s spec.sandboxes. The operator sets OMNIA_SANDBOX_API_URL on the runtime container when enabled is true, and owns every claim it creates to this AgentRuntime, so deleting the AgentRuntime deletes its claims and sandboxes without syncing their work back.

FieldTypeRequiredDefault
sandbox.enabledboolNofalse
sandbox.maxConcurrentint (min 1)No4
sandbox.maxPerSessionint (min 1)No1
sandbox.idleTimeoutdurationNo30m
sandbox.releaseGracedurationNo2m
sandbox.mountReleaseGracedurationNo5m
sandbox.connections[]{sandbox, role, connection or unbind}Nonone
spec:
sandbox:
enabled: true
maxConcurrent: 4
maxPerSession: 1
idleTimeout: 30m
releaseGrace: 2m
mountReleaseGrace: 5m

A claim naming a runtime with enabled unset is failed, not queued.

maxConcurrent caps bound claims across all of the runtime’s sessions; maxPerSession caps them within one session. A claim past either queues (status.phase: Queued) and binds when a slot frees, rather than failing. When the runtime asks for a new sandbox and the session already holds maxPerSession live claims, of any class, the operator refuses the request (MaxPerSession) instead of creating a claim that would queue. That holds across operator replicas: a claim two replicas let through at once is queued by the controller with reason SessionLimitReached, and the request that created it deletes it and is refused the same way (see SandboxClaim).

maxPerSession caps the session’s total across sandboxes. Each ToolRegistry sandbox’s maxInstances caps that sandbox’s instances within the session.

idleTimeout is how long a bound claim may go untouched before it is deleted.

releaseGrace is how long a conversation’s sandboxes outlive the conversation. The grace starts when the conversation’s WebSocket connection closes and its last turn has finished; the operator then marks the session’s claims to be released after it. A new message in the session, an acquire or a sandbox tool call within the grace keeps them, so a client that reconnects finds the sandbox it left. Conversations over the REST and A2A facades have no connection to close, so nothing starts the grace and their sandboxes go at idleTimeout. A sandbox that mounts a project or the session workspace uses mountReleaseGrace instead. Unset is 2m; the value must be greater than zero. The idle reaper counts from the claim’s last renewal, which is never later than the conversation’s end, so with a grace at or above idleTimeout the idle reaper always deletes the claim first. See Release at conversation end.

mountReleaseGrace is releaseGrace for a sandbox that mounts a project or the session workspace. Its files live on the workspace volume, so releasing it costs the pod’s own state (caches, anything written outside the mount) and a cold start: the session’s next tool call gets a new sandbox on the same files. It is longer than releaseGrace because a person comes back to such a sandbox. Unset is 5m; the value must be greater than zero.

connections binds a Connection to one role of one sandbox in this agent’s ToolRegistry, keyed by sandbox and role. Each entry overrides the sandbox’s default for that role, so agents that share a ToolRegistry can reach the same service as different accounts. A role bound by neither is left unbound. An entry with unbind: true in place of connection drops the sandbox’s default for that role, so this agent does not reach the service at all. Each entry sets exactly one of connection and unbind: true. The SandboxConnectionsValid condition reports, before any session starts, a binding that names a sandbox the registry does not declare (ConnectionSandboxUnknown), an unbind of a required role (ConnectionRequiredUnbound), a required role that ends up unbound, or any other check that fails.

spec:
sandbox:
enabled: true
connections:
- sandbox: workspace
role: vcs
connection: github-acme
- sandbox: workspace
role: demo_sharepoint_account
unbind: true

Each sandbox lives on the workspace content PVC workspace-<namespace>-content. When it does not exist yet and the namespace’s Workspace has storage enabled, the operator creates it and the claim waits in Pending (reason WaitingForStorage). A namespace that will never have one (no Workspace, storage disabled, or a direct-NFS install) fails the claim with reason NoWorkspaceStorage rather than leaving a pod stuck. Each sandbox’s filesystem lives on it at <workspace>/<namespace>/sandboxes/<claim>/ and survives the pod; the generated claim name in that path keeps concurrent sandboxes from sharing a filesystem.

See Sandbox security for what a sandbox can and cannot reach, and why.

Runtime context store configuration (working LLM context across turns).

FieldTypeDefaultRequired
context.typestringmemoryNo
context.ttlduration24hNo
context.storeRef.namestring-Yes for redis (CEL-validated)

storeRef names a Secret that must hold the Redis connection string under the key url. A durable (redis) store lets a fresh pod resume a conversation’s working context.

spec:
context:
type: redis
ttl: 24h
storeRef:
name: redis-credentials

Context store types:

  • memory - In-memory (not recommended for production)
  • redis - Redis backend (recommended)

Media configuration: mock:// URL resolution for the mock provider, and the media-storage backend for uploads and attachment references.

FieldTypeDefaultRequired
media.basePathstring/etc/omnia/mediaNo
media.storageobject-No
spec:
media:
basePath: /etc/omnia/media

The runtime reads basePath from the AgentRuntime and resolves mock:// URLs against it — primarily used with the mock provider for testing multimodal responses. When media.storage is absent, the facade also uses basePath as a local media storage path. media.storage.type: none disables media storage even though basePath has a default.

Selects the media-storage backend the operator provisions into the facade (uploads) and runtime (attachment-reference resolution) containers, as OMNIA_MEDIA_STORAGE_* environment variables. Unset or type: none disables media storage.

FieldTypeDescription
typenone | local | s3 | gcs | azureBackend (default none). Each non-none type requires its matching block (CEL-validated)
local.basePathstringDirectory the facade serves media from and the runtime resolves against; the mount path when volumeClaim is set
local.volumeClaimstringExisting PVC mounted at basePath in both facade and runtime. Use ReadWriteMany; without it the local backend needs a writable basePath from elsewhere, since the containers have a read-only root filesystem
s3.bucket / region / prefix / endpointstringS3 or S3-compatible storage. bucket and region are required (CEL-validated); endpoint (e.g. MinIO) forces path-style addressing
gcs.bucket / prefixstringGoogle Cloud Storage via workload identity
azure.account / container / prefixstringAzure Blob Storage
defaultTTLdurationRetention TTL for stored media (default 24h)
uploadURLTTL / downloadURLTTLdurationBounds on presigned upload / download URLs (defaults 15m / 1h)
maxFileSizeBytesint64 (min 1)Cap on a single stored object (default 104857600, 100 MiB)
secretRef.namestringExplicit credentials: AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY for S3, AZURE_ACCOUNT_KEY for Azure. Omit for keyless workload identity; GCS uses workload identity only
spec:
media:
storage:
type: s3
s3:
bucket: agent-media
region: us-east-1
maxFileSizeBytes: 10485760

A ceiling on how often this agent may be invoked. Optional; omitted means no limit, which is what every agent has unless it says otherwise.

FieldTypeDefaultRequired
rateLimit.maxInvocationsPerMinuteint32— (unlimited)No
rateLimit.burstint32one minute’s worthNo
spec:
rateLimit:
maxInvocationsPerMinute: 60
burst: 20

An invocation over the limit is refused before it reaches the runtime, so it is never billed. How the refusal reaches the caller:

SurfaceRefusal
REST / function (POST /functions/{name})429 with Retry-After, body {"error":"rate_limited"}
MCPtool error rate_limited: … (MCP reports tool failures at HTTP 200)
WebSocket / A2Athe turn ends with the rate-limit error
Async (?mode=async)not refused — queued items wait, see below

The refusal is not 502 runtime_error: the runtime was never invoked, so nothing ran and the call works later.

Refusals are also counted in status.invocations as failed invocations with the limit as lastError, so a runaway loop is visible in kubectl get agentruntime rather than only in logs. The facade logs one line per 30s while throttling, with a count of the suppressed refusals.

A queued invocation waits rather than being refused: the async path does not retry, so a 202-accepted item is paced at the ceiling instead of failing. The wait is bounded at 30s; past that it is refused like anything else. A shutdown while waiting is reported as a cancellation, not as the rate limit.

The facade reads the ceiling from the CRD at startup, so it is part of the Deployment’s config hash; changing the limit rolls the pod.

  • The limit is per pod, not per agent: each facade process holds its own bucket, so an agent with three replicas admits up to three times this rate.
  • It counts invocations, not spend: a ceiling on frequency, not a currency budget.
  • burst absorbs a batch that fans out at once and then goes quiet; unset, it defaults to one minute’s worth.

It applies to every facade an agent serves (websocket, a2a, rest, mcp) from one shared bucket: the limit is on the agent, not on a protocol.

Deployment-related settings including replicas, resources, and autoscaling.

FieldTypeDefaultDescription
runtime.replicasint32 (min 0)1Desired pods; ignored when autoscaling is enabled
runtime.resourcesResourceRequirementsrequests 100m CPU, 256Mi memoryRuntime container resources. Replaces the default. The facade (100m, 128Mi) and policy-broker (50m, 64Mi) containers get fixed requests
runtime.nodeSelector / tolerations / affinity--Pod scheduling
runtime.volumes / volumeMounts--See below
runtime.extraEnv[]EnvVar-Extra environment variables for the runtime container (e.g. LOG_LEVEL=debug; LOG_SAMPLING=off keeps every log line under load)
runtime.events.recordall | lifecycle | noneallWhich pipeline telemetry events are recorded. See below
runtime.autoscalingobject-See below
spec:
runtime:
replicas: 3
resources:
requests:
cpu: "500m"
memory: "256Mi"
limits:
cpu: "1000m"
memory: "512Mi"
nodeSelector:
node-type: agents
tolerations:
- key: "dedicated"
operator: "Equal"
value: "agents"
effect: "NoSchedule"

runtime.events.record selects how much pipeline telemetry the runtime records for a session: the runtime events a session’s trace shows. A session’s messages, tool calls, provider calls and eval results are always recorded; whether a session is recorded at all is decided by its service group having a session-api, not by this field.

ValueRecords
all (default)Every pipeline event
lifecycleEverything except the per-stage and per-check events that succeed: stage.started, stage.completed, prompt.template.started, prompt.template.rendered, validation.started, validation.passed. Failures are kept
noneNone of PromptKit’s pipeline telemetry (pipeline.*, stage.*, middleware.*, validation.*, prompt.template.*, context.*, state.*, workflow.*)

At every level Omnia’s own events are recorded: ToolPolicy decisions and tool-call receipts (which become audit-log rows), the client-tool hand-off, and stream interruptions.

Telemetry carries no user content but is most of what a turn writes: about 50 events for a three-turn conversation, more than half of them stage.*. Converting it is a measurable share of the runtime’s CPU under load. An event left out costs nothing.

spec:
runtime:
events:
record: lifecycle

Mount additional volumes in the runtime container for media files, mock configurations, or other data.

FieldTypeDescription
runtime.volumes[]VolumeKubernetes Volume definitions
runtime.volumeMounts[]VolumeMountVolume mounts for the runtime container
spec:
runtime:
volumes:
- name: mock-media
persistentVolumeClaim:
claimName: media-pvc
- name: mock-config
configMap:
name: mock-responses
volumeMounts:
- name: mock-media
mountPath: /etc/omnia/media
readOnly: true
- name: mock-config
mountPath: /etc/omnia/mock
readOnly: true

Supported volume types include:

  • persistentVolumeClaim - Mount a PVC for persistent storage
  • configMap - Mount a ConfigMap as files
  • secret - Mount a Secret as files
  • emptyDir - Temporary storage (cleared on pod restart)

Horizontal pod autoscaling configuration. Supports both standard HPA and KEDA.

FieldTypeDefaultDescription
enabledbooleanfalseEnable autoscaling
typestringhpahpa or keda
minReplicasinteger1Minimum replicas. 0 is KEDA scale-to-zero and requires keda.triggers (CEL-validated)
maxReplicasinteger100Maximum replicas
targetMemoryUtilizationPercentageinteger70Memory target (HPA only)
targetCPUUtilizationPercentageinteger90CPU target (HPA only)
scaleDownStabilizationSecondsinteger300Scale-down cooldown (HPA only)

When an agent sets no runtime.autoscaling, it inherits the autoscaling default declared on its Workspace service group, if any. The AutoscalingReady condition reports which applies: True/Scaling when an HPA or KEDA ScaledObject is reconciled, True/Disabled for static replicas, and False/KEDANotInstalled when type: keda is requested but the KEDA CRDs are absent (the agent then stays at static replicas).

spec:
runtime:
autoscaling:
enabled: true
type: hpa
minReplicas: 2
maxReplicas: 10
targetMemoryUtilizationPercentage: 70
targetCPUUtilizationPercentage: 80
scaleDownStabilizationSeconds: 300
spec:
runtime:
autoscaling:
enabled: true
type: keda
minReplicas: 1 # 0 for scale-to-zero, which requires keda.triggers
maxReplicas: 20
keda:
pollingInterval: 15
cooldownPeriod: 60
triggers:
- type: prometheus
metadata:
serverAddress: "http://prometheus-server:9090"
query: 'sum(omnia_agent_connections_active{agent="my-agent"})'
threshold: "10"

KEDA-specific configuration (only used when type: keda).

FieldTypeDefaultDescription
pollingIntervalinteger (min 1)30Seconds between trigger checks
cooldownPeriodinteger (min 0)300Seconds before scaling down
triggersarray-Custom KEDA triggers (type, metadata)
connectionThresholdinteger (min 1)1000Target active connections per pod for the default trigger; only used when triggers is empty. Half the facade’s per-pod cap of 2000 connections, which it answers with 503 past. Set lower (e.g. 20) for audio/media workloads

If no triggers are specified, a default Prometheus trigger scales on sum(omnia_agent_connections_active{agent="<name>",namespace="<namespace>"}) or vector(0) against connectionThreshold. Its serverAddress is fixed at http://omnia-prometheus-server.omnia-system.svc.cluster.local/prometheus, so set triggers explicitly when Prometheus runs elsewhere.

For type: hpa the operator also sets scaling behaviour that has no field: scale-up has a 0-second stabilisation window and may add 100% of pods or 4 pods every 15 seconds (whichever is larger); scale-down uses scaleDownStabilizationSeconds and removes at most 50% of pods per 60 seconds.

Prometheus trigger:

triggers:
- type: prometheus
metadata:
serverAddress: "http://prometheus:9090"
query: 'sum(rate(requests_total[1m]))'
threshold: "100"

Cron trigger:

triggers:
- type: cron
metadata:
timezone: "America/New_York"
start: "0 8 * * 1-5" # 8am weekdays
end: "0 18 * * 1-5" # 6pm weekdays
desiredReplicas: "5"

Configures realtime eval execution for this agent. When enabled, session events trigger evaluation of live conversations against eval definitions in the referenced PromptPack. See Realtime Evals for the full architecture and Configure Realtime Evals for a step-by-step guide.

FieldTypeDefaultRequired
evals.enabledbooleanfalseNo
spec:
evals:
enabled: true

Split realtime evals between two paths by group. Each eval’s groups are auto-classified by handler type, or declared explicitly on the eval, and decide which path runs it.

FieldTypeDefaultDescription
evals.inline.groupsstring[]["fast-running"]Groups executed inside the runtime process, in the PromptKit SDK’s eval middleware
evals.worker.groupsstring[]["long-running", "external"]Groups executed out-of-band in the eval-worker, which reads session events from Redis streams
spec:
evals:
enabled: true
inline:
groups: ["fast-running"]
worker:
groups: ["long-running", "external"]

The defaults are disjoint — no eval runs on both paths. An absent or empty list falls back to the default; to disable all evals, use enabled: false (an empty groups list is not an off-switch).

Rows written to the eval_results table are tagged source="runtime-inline" from the inline path and source="worker" from the eval-worker, so aggregation over the table can distinguish them.

LLM judge evals resolve their provider from the AgentRuntime’s spec.providers list, using the entry named judge (or a custom name referenced in the PromptPack’s eval definitions). See providers for the reference shape.

The eval worker resolves provider credentials from the referenced Provider CRDs and their associated Secrets.

The following hold for every AgentRuntime:

  • Sampling: no spec.evals field sets a sampling rate. Every session is evaluated except where a PromptPack eval trigger samples it (sample_turns, sample_sessions with sample_percentage; see the PromptPack reference).
  • Rate limiting: the eval-worker applies no throttle to eval executions or concurrent judge calls.
  • Session completion: the eval-worker treats a session as complete, and runs its on_session_complete evals, after 5 minutes with no new message (DefaultInactivityTimeout in ee/pkg/evals/completion_tracker.go).
  • Inline execution: in the runtime, the SDK runs each turn’s inline evals in a background goroutine, so they do not delay the reply. At most 10 run at once (DefaultMaxConcurrentEvals); a turn dispatched while all 10 slots are busy has its inline evals skipped. Inline on_session_complete evals run when the conversation closes.

Customises the eval-worker Pod (see Configure pod overrides). The eval-worker is per (namespace, service group) pair, not per AgentRuntime: a namespace with multiple eval-enabled service groups gets multiple eval-workers, one per group. Reconciliation deterministically picks the first AgentRuntime per group, regardless of which one triggered the reconcile, and a WorkspaceServiceGroup’s own evalWorker.podOverrides takes precedence over this field.

Configures cross-session memory for this agent. Memory is disabled unless enabled: true. The memory-api endpoint is resolved from the Workspace service group selected by spec.serviceGroup.

FieldTypeDefaultDescription
enabledboolfalseEnables memory capability
retrieval.enabledbooltrue when memory is enabledEnables ambient per-turn retrieval, for a pack that renders {{memory_context}}
retrieval.strategyenumunset (keyword)keyword, semantic, or composite. semantic and composite fall back to keyword search when the memory store has no semantic capability
retrieval.limitint3210Maximum episodic memories injected per turn; 1–50
retrieval.accessFilter.denyCELstringemptyCEL deny expression over result metadata
tools.enabledbooltrue when memory is enabledEnables explicit memory tools
tools.notesstringemptyAdvice for this agent, appended to every memory tool’s description under “Notes for this agent:”; at most 2000 characters
facetstringemptyPins the agent’s memory to one container (a project, client or matter). 1 to 128 ASCII letters, digits and . _ : / -, starting with a letter or digit

Ambient retrieval and explicit tools are independent. Set retrieval.enabled: false to keep explicit tools without automatic context injection, or set tools.enabled: false for read-only ambient retrieval. Ambient retrieval also needs the pack’s prompt to render {{memory_context}}; a pack without it is not retrieved for.

tools.notes adds to the workspace’s memory tool guidance, which memory-api renders from the MemoryPolicy; it never replaces it. Use it when agents in one workspace need different advice, such as which identifier to use as about_key. Advice that applies to every agent in the workspace belongs in the MemoryPolicy’s toolGuidance.

facet fixes the container every conversation of the agent saves into and recalls from, on every facade. Without it, a WebSocket client chooses the container with the memory_facet connection parameter, which isolates relevance but does not restrict the user: the same user can connect without it. With it, a connection that names no container uses the agent’s, and one that names another is refused with 403. See Memory tools.

The runtime reads spec.memory when it starts, so the operator rolls the agent’s pods whenever spec.memory changes. An edit takes effect once the new pods are ready.

Memory tools are registered only for conversations with a user identity. An anonymous conversation therefore has neither explicit memory tools nor a user-scoped memory write, even when this configuration is enabled.

spec:
serviceGroup: default
memory:
enabled: true
retrieval:
strategy: composite
limit: 10
accessFilter:
denyCEL: 'metadata.classification == "restricted"'
tools:
enabled: true
notes: "Use the customer's account id as about_key for profile facts."

See Memory tools and the Memory API reference.

Progressive rollout configuration. When rollout.candidate is set and differs from the current spec, the controller creates a candidate Deployment and progresses through the defined steps.

FieldTypeRequiredDescription
rollout.candidateobjectNoSparse overrides for the candidate version. Unset means no rollout is active
rollout.candidate.promptPackRefobjectNoPromptPack for the candidate (name, version or track, as in spec.promptPackRef)
rollout.candidate.providerRefsarrayNoProvider list for the candidate (same shape as spec.providers)
rollout.candidate.toolRegistryRefobjectNoToolRegistry override for the candidate
rollout.stepsarray (min 1)YesOrdered sequence of rollout actions; set exactly one field per step
rollout.steps[].setWeightinteger—Set candidate traffic weight (0-100)
rollout.steps[].pauseobject—Pause the rollout
rollout.steps[].pause.durationstringNoPause duration (e.g., “5m”). Omit to wait for manual promotion
rollout.steps[].analysisobject—Run a RolloutAnalysis template; the rollout advances only if it passes
rollout.steps[].analysis.templateNamestringYesName of the RolloutAnalysis in the agent’s namespace
rollout.steps[].analysis.argsarrayNoname/value argument overrides for the template
rollout.stickySessionobjectNoConsistent routing for experiments; applied only with trafficRouting.istio
rollout.stickySession.hashOnstringYesHeader for consistent hashing (e.g., “x-user-id”)
rollout.rollbackobjectNoRollback configuration
rollout.rollback.modestringNoautomatic, manual (default), or disabled. automatic rolls back when an analysis step fails or the candidate reports ProgressDeadlineExceeded. manual never rolls back on its own. disabled behaves exactly like manual: the controller does not distinguish them
rollout.rollback.progressDeadlineSecondsinteger (min 10)NoSets the candidate Deployment’s spec.progressDeadlineSeconds. Automatic rollback on an unhealthy candidate fires when that deadline is exceeded (ProgressDeadlineExceeded), so this bounds detection time. Unset inherits the Kubernetes default (600s)
rollout.trafficRoutingobjectNoHow the step’s weight is delivered
rollout.trafficRouting.modestringNomesh, replicaWeighted or external. Unset resolves to mesh when the mesh is available, else replicaWeighted; a config that sets istio without mode resolves to external
rollout.trafficRouting.mesh.hostsarrayNoHosts the operator-owned VirtualService matches; defaults to the agent’s own Service DNS name
rollout.trafficRouting.mesh.stableSubset / candidateSubsetstringNoDestinationRule subset names (default stable / canary)
rollout.trafficRouting.mesh.waypointstringNoIstio ambient waypoint; the operator labels the agent Service istio.io/use-waypoint=<waypoint> so the L7 split takes effect
rollout.trafficRouting.istio.virtualService.namestringYesExisting VirtualService to patch (mode: external)
rollout.trafficRouting.istio.virtualService.routesarray (min 1)YesRoute names to manage
rollout.trafficRouting.istio.destinationRule.namestringYesExisting DestinationRule to patch
rollout.trafficRouting.istio.destinationRule.stableSubset / candidateSubsetstringNoSubset names (default stable / canary)
rollout.trigger.promptPackChannelstable | prereleaseYes (when trigger is set)Version-triggered canary: when a version newer than the agent’s active one appears on this channel, the controller sets it as rollout.candidate. Requires a version-pinned spec.promptPackRef and is mutually exclusive with promptPackRef.track (CEL-validated)

replicaWeighted approximates the weight by the ratio of candidate to stable replicas; status.rollout.trafficWeightEnforced is false in that mode, and the TrafficRouting condition reports when the resolved mode degraded from the one requested.

# Canary rollout with analysis
spec:
promptPackRef:
name: customer-support-pack
version: "1.0.0"
rollout:
candidate:
promptPackRef:
name: customer-support-pack
version: "2.0.0"
steps:
- setWeight: 10
- pause:
duration: "5m"
- analysis:
templateName: quality-check
- setWeight: 50
- pause:
duration: "10m"
- setWeight: 100
rollback:
mode: automatic
trafficRouting:
istio:
virtualService:
name: customer-support-vs
routes: [primary]
destinationRule:
name: customer-support-dr

When candidate matches the current spec, the rollout is idle. Promotion copies the candidate overrides into the main spec, keeps 100% of traffic on the candidate until the stable Deployment is healthy on the new config, then cuts back to stable, deletes the candidate Deployment and clears rollout.candidate. Rollback reverts the candidate to match the current spec.

Data-plane authentication for the facades’ external listeners. Each sub-block enables one validator. The external chain runs the enabled validators in the order clientKeys, oidc, edgeTrust. With no sub-block the chain is empty and the external listener returns 401 to every request. For setup, see Configure agent authentication.

FieldTypeDefaultDescription
clientKeysobject-Enables per-caller client keys. An empty object enables the validator
clientKeys.defaultRolestringviewerValue of the role claim for a key whose Secret carries no scopes. Any string is valid
clientKeys.trustEndUserHeaderboolfalseTakes the end user from the X-End-User-Id request header. When false, the end user is the key’s own identity
oidcobject-Verifies RS256 JWTs from your identity provider
oidc.issuerstring-Required. Must equal the token’s iss claim exactly. The controller fetches {issuer}/.well-known/openid-configuration after trimming trailing slashes
oidc.audiencestring-Required. The expected aud claim
oidc.claimMapping.subjectstringsubClaim used as the caller’s subject
oidc.claimMapping.endUserstringsubClaim used as the end user. Falls back to the subject when the token lacks it
oidc.sessionClaimstring-Claim that binds a token to one conversation. A token without it is rejected with 401 (missing_session_claim). A session the token starts on the WebSocket or A2A facade is bound to the claim’s value, and the token can resume only a session bound to the same value. The check fails closed, so a resume needs session-api, a session shed as unrecorded cannot be resumed, and an A2A client starts a conversation by sending no contextId. Function invocations are never resumed
edgeTrustobject-Trusts claim headers that an upstream edge has already verified
edgeTrust.headerMapping.subjectstringx-user-idHeader read as the subject. The request is admitted only when this header is present
edgeTrust.headerMapping.endUserstringx-user-idHeader read as the end user. Falls back to the subject
edgeTrust.headerMapping.emailstringx-user-emailHeader read into the email claim
edgeTrust.claimsFromHeadersmap-Inbound header name (case-insensitive) to the claim name it is exposed as

The edgeTrust role header is always x-user-roles. It is read into the role claim, and the claim is viewer when the header is absent. It is not configurable. Every other claim of an admitted request, including the IdP’s own role and group claims for oidc, is forwarded under its own name and reaches ToolPolicy as principal.claims.

The controller mirrors the issuer’s JWKS into the Secret agent-<agentruntime>-oidc-jwks and refreshes it every six hours. The OIDCJWKSReady condition reports the result, and the facade does not start with oidc enabled until that Secret exists.

A WebSocket connection admitted by an oidc token closes with code 4401 shortly before the token’s exp. The client can keep it open by sending a new token in a credential_refresh frame. See WebSocket protocol.

Each client key is a Secret in the agent’s namespace. hack/mint-client-key.sh writes one; any tool that applies a Secret can write the same shape. The facade watches the Secrets, so a created or deleted Secret takes effect without a change to the AgentRuntime.

PartValue
metadata.nameagent-<agent>-clientkey-<id>. The part after -clientkey- is the key’s subject. A name that does not match uses the full Secret name
metadata.labelsomnia.altairalabs.ai/credential-kind: agent-client-key and omnia.altairalabs.ai/agent: <agent>
metadata.ownerReferencesAn entry naming the AgentRuntime’s UID. The facade skips a Secret without one and logs it
data.keyHashThe raw 32-byte SHA-256 digest of the token, not its hex string. The facade skips any other length and logs it at debug level
data.scopesOptional JSON string map. Becomes the key’s claims
data.expiresAtOptional RFC 3339 timestamp. Without it the key never expires
FieldTypeDescription
externalAuthobjectData-plane authentication for the facades’ external listeners. Unset means the external listener rejects every request, and only the management plane admits callers. See externalAuth
consoleobjectDashboard console settings: allowedAttachmentTypes (client-side default: image/png, image/jpeg, image/gif, image/webp, audio/mpeg, audio/wav, audio/ogg, application/pdf, text/plain, text/markdown, text/javascript, text/typescript, application/javascript, application/typescript, text/x-python, application/x-python-code, text/csv, application/json), allowedExtensions, maxFileSize (bytes on the wire after base64, default 10485760), maxFiles (1–20, default 5), mediaRequirements
duplexobjectRealtime voice. enabled renders the dashboard’s voice call console and makes the agent require the duplex_audio and interruption runtime capabilities; audio (recommendedSampleRate, channels, format, chunkDurationMs) is the format the runtime counter-offers in RuntimeHello; mode (realtime or cascaded) selects whether audio streams to the model or runs through the agent’s stt and tts providers. Unset mode selects cascaded when the default provider’s type cannot stream audio (claude, ollama, vllm) and both speech providers are usable, and realtime otherwise. cascaded without both speech providers fails each session at open. See Add voice to a text-only agent
serviceGroupstringWorkspace service group (spec.services[].name) whose session-api and memory-api the agent uses. Default default; DNS-label pattern, max 63
privacyPolicyRef.namestringA SessionPrivacyPolicy in the agent’s namespace that overrides the service group’s policy for this agent; reported by the PrivacyPolicyResolved condition. See Configure privacy policies
extraPodAnnotationsmapExtra annotations on the agent pods
podOverridesobjectPod-level scheduling, service account, labels, annotations and volumes, plus container-level extraEnv / extraEnvFrom / extraVolumeMounts applied to the facade and runtime containers (not to operator-injected sidecars). See podOverrides

PodOverrides is one object type with six placements. Every field is optional.

CRDFieldPod
AgentRuntimespec.podOverridesFacade and runtime
AgentRuntimespec.evals.podOverridesEval-worker for the agent’s service group
Workspacespec.services[].evalWorker.podOverridesEval-worker for the group; takes precedence over spec.evals.podOverrides
Workspacespec.services[].session.podOverridesManaged session-api
Workspacespec.services[].memory.podOverridesManaged memory-api
ArenaJobspec.workers.podOverridesWorker Job pods
FieldTypeMerge
serviceAccountNamestringReplaces the operator default when non-empty
priorityClassNamestringReplaces the operator default when non-empty
runtimeClassNamestringSets spec.runtimeClassName on the pod when non-empty. Empty leaves the cluster default. DNS-subdomain pattern, max 253. The operator does not check that the RuntimeClass exists. See Run agents under gVisor or Kata
labelsmapMerged. Operator-set keys win on collision
annotationsmapMerged. Your values win on collision
nodeSelectormapMerged by key. Your values win on collision
tolerations[]TolerationAppended
imagePullSecrets[]LocalObjectReferenceAppended
extraVolumes[]VolumeAppended to the pod’s volumes. The schema does not validate the volume; the API server does when the Deployment is applied
extraEnv[]EnvVarAppended to the env of each container
extraEnvFrom[]EnvFromSourceAppended to the envFrom of each container
extraVolumeMounts[]VolumeMountAppended to the volumeMounts of each container

The container-level fields (extraEnv, extraEnvFrom, extraVolumeMounts) apply to every container in the pod except operator-injected sidecars. On an AgentRuntime pod that means the facade and runtime containers and not policy-broker. AgentRuntime drops reserved environment keys from extraEnv.

PodOverrides has no resources, securityContext, affinity, topologySpreadConstraints, hostNetwork, hostPID or hostIPC field. An AgentRuntime sets affinity with runtime.affinity.

On an AgentRuntime pod:

  • runtime.nodeSelector and runtime.tolerations apply first. podOverrides then merges key-wise into the node selector and appends to the tolerations.
  • facades[].extraEnv and runtime.extraEnv target one container each.
  • podOverrides.annotations override extraPodAnnotations on key collision.
  • The pod’s ServiceAccount is podOverrides.serviceAccountName, else the Workspace’s spec.runtime.serviceAccountName, else <agentruntime>-facade. An AgentRuntime that sets its own serviceAccountName takes no Workspace podLabels or podAnnotations defaults.
  • The pod’s RuntimeClass is podOverrides.runtimeClassName, else the Workspace’s spec.runtime.runtimeClassName, else the cluster default. It applies to every container in the pod, policy-broker included. An agent that sets its own serviceAccountName still inherits the Workspace runtimeClassName.

For session-api and memory-api, RBAC bindings follow serviceAccountName.

See Configure pod overrides for worked examples.

The omnia.altairalabs.ai/idle-timeout annotation makes an agent temporary. Its value is a Go duration such as 30m or 2h. Once the agent goes that long without use, the operator deletes it, and garbage collection removes everything it owns.

metadata:
annotations:
omnia.altairalabs.ai/idle-timeout: 2h
  • Use is any invocation through any facade surface. The facade records it in status.lastUsedAt, at most once per half the timeout, so the recorded time lags the last use by up to half the timeout.
  • The deadline is lastUsedAt plus the timeout. An agent that has never been used is judged from its creation, so an agent whose pods never start is still deleted. A lastUsedAt in the future counts as the current time.
  • The effective idle window is between half the timeout and the full timeout after the last use, because renewals are throttled to one per half the timeout.
  • When the deadline passes, the operator deletes the AgentRuntime and records a Normal event with reason IdleReaped.
  • An invalid value sets the IdleTimeoutValid condition to False with reason InvalidIdleTimeout and a Warning event. The agent is not deleted.
  • Without the annotation an agent is never deleted for being idle, and its facade never writes lastUsedAt.

The facade’s Role reaches only its own AgentRuntime: get on agentruntimes and get/patch on agentruntimes/status, both limited by resourceNames to this agent. A pod cannot change another agent’s lastUsedAt.

The omnia.altairalabs.ai/hidden label controls visibility, for any Omnia resource kind, not only AgentRuntimes. An object labelled "true" is left out of lists and pickers. It is still reconciled, and it can still be reached by name.

metadata:
labels:
omnia.altairalabs.ai/hidden: "true"

These surfaces leave hidden objects out:

  • Every dashboard list of workspace and shared resources: agents, PromptPacks, ToolRegistries, ToolPolicies, Providers and skill sources. That includes every page and picker built on them, such as the agents page, topology, the console agent picker, the session filters, the Arena run dialog, the deploy profile and the quality page’s agent picker.
  • The dashboard’s workspace stats, and its credential Secrets list.

Hidden means “not listed”, never “not reachable”. Any URL that names a hidden object shows it. That covers the agent page /agents/<name>, the console’s ?agent=<name> link, and the pack and registry pages. A get by name is never filtered. A workspace list returns hidden objects only when asked with ?includeHidden=true, which the dashboard uses only to resolve names. For example, a hidden registry’s page uses it to show its hidden users.

Hidden is a visibility setting for lists and UI, never a compliance exemption, so audit, compliance and DSAR surfaces such as the privacy API’s agent register still include hidden objects. Nor is it an accounting exemption: a hidden agent’s cost is still tracked, and it keeps its own row on the costs page, and usage telemetry counts hidden objects like any other.

The deploy API labels every object of an Arena project’s dev intent this way, and a project PromptPackSource keeps its live PromptPack’s label in step with its own. See deploy intent. The label says only whether an object is shown. What the object is, such as omnia.altairalabs.ai/dev, is a separate label, and no list filters on it. A dev intent’s objects carry omnia.altairalabs.ai/dev: "true", omnia.altairalabs.ai/arena-project (the project id) and omnia.altairalabs.ai/arena-user (the user pseudonym).

ValueDescription
PendingNo ready replica yet, or a reference is not usable yet (Provider not Ready, missing required capabilities, cross-namespace ToolRegistry)
RunningAt least one replica is ready
FailedA configuration error that will not resolve on its own: no PromptPack matches promptPackRef, the PromptPack failed validation, a referenced Provider does not exist, or a Provider’s role does not match providers[].role
FieldDescription
status.replicas.desiredDesired replicas
status.replicas.readyReady replicas
status.replicas.availableAvailable replicas
FieldDescription
status.activeVersionThe deployed PromptPack version
status.compositionDigestsha256:<hex> over everything resolved for the agent — pack content, skill manifest, tool registry and providers. Empty until every input has published its own identity
status.serviceEndpointInternal facade endpoint, {name}.{namespace}.svc.cluster.local:{port}
status.managementEndpointsInternal management-plane ports (ws, a2a, mcp), one per surface whose facade has managementPlane enabled
status.a2aA2A agentCardURL and endpoint (only with an a2a facade), and per-client resolution of spec.a2aClients (clients[]: name, resolvedURL, ready, error) on any agent with clients
status.facade.endpointsExternal URLs derived from observed Gateway API HTTPRoutes (protocol, url, scheme, host, path, port, routeName, routeNamespace, valid, reason). Empty means in-cluster only
status.runtimeCapabilitiesContract capabilities the running runtime advertises
status.invocationsFacade-reported total, succeeded, lastInvocationTime, lastSuccessTime and lastError. Throttled and approximate — a pod that dies loses up to one 30-second flush interval of counts
status.lastUsedAtWhen the agent was last used, written by its facade. Only an agent with the omnia.altairalabs.ai/idle-timeout annotation has it; see Idle reaping
status.observedGenerationLast generation the controller observed
FieldDescription
status.rollout.activeWhether a rollout is in progress
status.rollout.currentStepCurrent step index (zero-based)
status.rollout.currentWeightCurrent candidate traffic weight
status.rollout.stableVersionVersion serving stable traffic
status.rollout.candidateVersionVersion serving candidate traffic
status.rollout.startedAt / stepStartedAtWhen the rollout, and the current step, began
status.rollout.messageHuman-readable rollout state
status.rollout.trafficRoutingModeThe traffic-routing mode actually in use
status.rollout.trafficWeightEnforcedfalse when the weight is approximated by replica ratio
status.rollout.analysisProgress of the analysis step being executed
status.rollout.promotingA promotion is in progress
TypeDescription
ReadyTrue when at least one replica is ready
DeploymentReadyDeployment is ready
ServiceReadyService is ready
PromptPackReadyReferenced PromptPack resolved and is valid
ProviderReadyReferenced Providers exist, are Ready, match the declared role and advertise the required capabilities
ToolRegistryReadyReferenced ToolRegistry exists and is in the agent’s namespace. A missing registry sets it False and the agent runs without tools
FrameworkReadyA runtime image resolved for framework.type
CapabilitiesSatisfiedThe running runtime advertises every capability the spec requires; while False for the current generation the Deployment is scaled to zero
RuntimeCapabilitiesReportedThe runtime has reported its capabilities
ExternalAuthEffective facade authentication; False when a facade can accept no callers
OIDCJWKSReadyThe OIDC issuer’s JWKS is mirrored (when externalAuth.oidc is set)
AutoscalingReadyWhich autoscaling policy is in effect (see runtime.autoscaling)
AsyncInvocationReadyAsync invocation can run (see facades[].async)
VolumesEvery PersistentVolumeClaim the pod mounts exists
SessionRecordingThe agent’s service group has a session-api, so conversations are recorded. False with WorkspaceNotFound, ServiceGroupNotFound or NoSessionService when it does not; the agent still serves, but writes no history. Never gates Ready
PrivacyPolicyResolvedWhich privacy policy applies
WithinWorkspaceQuotaThe workspace’s spec.quotas.agents applied to this agent. Present only when the Workspace sets an agent quota. True with QuotaSatisfied, or RunningOverAgentRuntimeQuota for an agent admitted before maxAgentRuntimes was lowered beneath it. False with ReplicasClamped when it asks for more replicas than maxReplicasPerAgent: it runs at the cap and is otherwise reconciled as usual. False with AgentRuntimeQuotaExceeded when the Workspace has not admitted it and it has no Deployment: nothing is created for it, phase Pending, Ready reason WorkspaceQuotaExceeded, until it is admitted
IdleTimeoutValidWhether the omnia.altairalabs.ai/idle-timeout annotation is a valid duration. Present only on an annotated agent
RolloutActive / TrafficRoutingRollout progress and traffic-routing health
A2AClientsResolvedEvery spec.a2aClients entry resolved; False names the unresolved ones (reason CrossNamespaceRef or Unresolved). Present only on an agent with clients
A2ATaskStoreSharedA2A tasks are visible to every replica (see facades[].a2a.taskStore). Present only on an agent with an a2a facade

kubectl get agentruntime shows PHASE, READY (ready replicas), VERSION (status.activeVersion), INVOCATIONS, LAST-SUCCESS and AGE.

apiVersion: omnia.altairalabs.ai/v1alpha1
kind: AgentRuntime
metadata:
name: production-agent
namespace: agents
spec:
promptPackRef:
name: customer-service-prompts
version: "2.1.0"
providers:
- name: default
providerRef:
name: claude-production
- name: judge
providerRef:
name: claude-haiku
toolRegistryRef:
name: service-tools
facades:
- type: websocket
port: 8080
handler: runtime
context:
type: redis
ttl: 24h
storeRef:
name: redis-credentials
evals:
enabled: true
runtime:
replicas: 3 # Ignored when autoscaling enabled
resources:
requests:
cpu: "500m"
memory: "256Mi"
limits:
cpu: "1000m"
memory: "512Mi"
autoscaling:
enabled: true
type: keda
minReplicas: 1
maxReplicas: 20
keda:
pollingInterval: 15
cooldownPeriod: 120
triggers:
- type: prometheus
metadata:
serverAddress: "http://omnia-prometheus-server.omnia-system.svc.cluster.local/prometheus"
query: 'sum(omnia_agent_connections_active{agent="production-agent",namespace="agents"}) or vector(0)'
threshold: "10"