Skip to content

ToolRegistry CRD

The ToolRegistry custom resource defines tool handlers available to AI agents. Handlers can expose one or more tools and come in two categories:

  • Self-describing (MCP, OpenAPI): Automatically discover tools at runtime
  • Explicit (HTTP, gRPC, client): Require a tool definition with name, description, and input schema
apiVersion: omnia.altairalabs.ai/v1alpha1
kind: ToolRegistry

Short name: tr (e.g. kubectl get tr).

List of handler definitions (at least one is required). Each handler connects to a tool source and exposes one or more tools.

spec:
handlers:
- name: calculator
type: http
httpConfig:
endpoint: https://api.example.com/calculate
method: POST
tool:
name: calculate
description: "Perform mathematical calculations"
inputSchema:
type: object
properties:
expression:
type: string
required: [expression]
TypeCategoryDescription
httpExplicitHTTP REST endpoint
grpcExplicitgRPC service using the Omnia Tool protocol
mcpSelf-describingModel Context Protocol server
openapiSelf-describingOpenAPI/Swagger-documented service
clientExplicitBrowser-executed tool (runs in the connected client)
platformSelf-describingServed by the operator itself, no backend endpoint. See Platform handler

Common fields for all handler types:

FieldTypeRequiredDescription
namestringYesUnique handler name (^[a-z0-9]([-a-z0-9]*[a-z0-9])?$, max 63 chars)
typestringYesHandler type (http, grpc, mcp, openapi, client, platform)
toolobjectConditionalTool definition — required for http, grpc, and client. Not permitted on platform handlers
httpConfigobjectConditionalRequired when type: http. Not permitted on platform handlers
grpcConfigobjectConditionalRequired when type: grpc. Not permitted on platform handlers
mcpConfigobjectConditionalRequired when type: mcp. Not permitted on platform handlers
sandboxobjectNo{name, port}: serves the handler from one of the registry’s sandboxes. Not permitted on client or platform handlers. See Handlers on a sandbox
openAPIConfigobjectConditionalRequired when type: openapi. Not permitted on platform handlers
clientConfigobjectNo{consentMessage, categories} for a client handler. See Client handler
platformobjectConditionalRequired when type: platform, and not permitted for any other type. See Platform handler
authobjectNoHow the runtime authenticates to the backend (see Authenticating tools). Not permitted on platform handlers
delegation.enabledboolNoDefault false. When true, every call to this handler carries an X-Omnia-Delegation header: a short-lived JWT signed by the operator naming the end user, agent and workspace, verifiable against the operator’s JWKS. Rejected on client handlers. The whole delegation field is rejected on platform handlers — a platform tool call always carries a delegation token, scoped to the workspace rather than the handler
timeoutstringNoPer-invocation wall-clock timeout. Defaults to 30s. The runtime sizes each turn’s execution ceiling to 2m plus the longest handler timeout, so a long timeout, such as 5m for a sandbox cold start, is not cut short by the turn

http, grpc, and client handlers require a tool definition:

FieldTypeRequiredDescription
tool.namestringYesTool name exposed to the LLM (^[a-z][a-z0-9_]*$, max 64)
tool.descriptionstringYesHuman-readable description
tool.inputSchemaobjectYesJSON Schema for input parameters
tool.outputSchemaobjectNoJSON Schema for output (optional)

The endpoint URL lives in httpConfig.endpoint (required):

- name: search-api
type: http
httpConfig:
endpoint: https://api.example.com/search
method: POST
headers:
Content-Type: application/json
contentType: application/json
tool:
name: search
description: "Search the knowledge base"
inputSchema:
type: object
properties:
query:
type: string
description: "Search query"
limit:
type: integer
default: 10
required: [query]
timeout: "30s"
FieldTypeDefaultDescription
endpointstring—Required. HTTP endpoint URL, or a path on the sandbox port when the handler sets sandbox
methodstringPOSTHTTP method
headersmap—Additional HTTP headers
headersFromSecretmap—Map of HTTP header name → secretRef ({name, key}). Value resolved from the Secret at call time; never stored in the CRD or the tools ConfigMap. Not for a header the auth stanza owns (Authorization, or a custom auth.workloadIdentity.header). See Advanced HTTP tools.
contentTypestringapplication/jsonContent-Type header
retryPolicyobject—Retry behaviour. See HTTP retry policy
authTypestring—Deprecated. Use the handler-level auth stanza instead. Auth type (bearer or basic).
authSecretRefobject—Deprecated. Use the handler-level auth stanza instead. Reference to a Secret holding the credential.

The HTTP handler also supports request/response shaping fields — urlTemplate, queryParams, headerParams, staticQuery, staticBody, bodyMapping, responseMapping, and redact. These are not exposed in the dashboard UI; see Advanced HTTP tools.

httpConfig.retryPolicy and openAPIConfig.retryPolicy share this shape. With no retryPolicy, a call is attempted once.

FieldTypeDefaultDescription
maxAttemptsint—Required. Total attempts including the first, 1 to 10. 1 disables retries
initialBackoffduration100msDelay before the first retry
backoffMultiplierstring"2.0"Decimal multiplier between retries, at least 1.0
maxBackoffduration30sUpper bound on the delay. Must be at least initialBackoff; the controller rejects the handler otherwise
retryOn[]int[408, 429, 500, 502, 503, 504]HTTP status codes that trigger a retry. An empty list disables status-based retry
retryOnNetworkErrorbooltrueRetry connection, DNS and timeout errors, which occur before a status code arrives
respectRetryAfterbooltrueOn a retried response, wait at least the Retry-After value (seconds or HTTP date), capped at maxBackoff

Each delay grows by backoffMultiplier with about 10% jitter. grpcConfig.retryPolicy has the same backoff fields plus retryableStatusCodes, a list of gRPC status code names (default UNAVAILABLE, DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED). mcpConfig.retryPolicy has only the four backoff fields.

Authentication is configured with the handler-level auth stanza (a sibling of httpConfig/openAPIConfig/…), so the same shape applies across handler types:

handlers:
- name: my-api
type: http
httpConfig:
endpoint: https://api.example.com
auth:
type: bearer # none | bearer | basic | serviceAccount | workloadIdentity
secretRef:
name: my-tool-credentials # a Kubernetes Secret in the same namespace
key: token # for basic, the value is "username:password"
tool:
name: my_tool
description: "Call the authenticated API"
inputSchema:
type: object
FieldTypeDefaultDescription
auth.typestringnoneAuthentication mechanism: none, bearer, basic, serviceAccount, or workloadIdentity.
auth.secretRefobject-Secret holding the credential. bearer/basic need this or auth.connectionRef.
auth.connectionRefobject-{name, key}: reads the bearer/basic credential from key key of a Connection’s Secret instead. Not with secretRef, and not on a handler that addresses a sandbox. See Reading the credential from a Connection.
auth.serviceAccount.audiencestring-Audience the projected ServiceAccount token binds to (required for serviceAccount).
auth.workloadIdentityobject-Hosted same-cloud identity: cloud (azure), audience (required), header (default Authorization). Required for workloadIdentity.

The auth stanza applies to http, openapi, grpc, and mcp handlers (the runtime attaches the credential as an HTTP Authorization header, gRPC authorization metadata, or an MCP transport header).

  • bearer / basic — the operator resolves secretRef into an operator-managed <agentruntime>-tool-secrets Secret, mounted read-only into the runtime. The token value never enters the tools ConfigMap.
  • serviceAccount — the operator projects an audience-bound Kubernetes ServiceAccount token into the runtime; the tool backend validates it via TokenReview. Sent as Authorization: Bearer <token>.
  • workloadIdentity — resolved by the runtime under the pod’s ambient Azure identity (core); cloud must be azure. The runtime acquires a token for audience and sets it on header (default Authorization). Supported on http, grpc, and mcp (sse / streamable-http) and openapi handlers; any other handler type with workloadIdentity fails the AgentRuntime reconcile. The token is acquired per call, so it survives token expiry. All WIF tools share the pod’s one identity, so that identity must be granted every WIF tool’s API.

For a secret header other than Authorization (a second API key or signing token), use httpConfig.headersFromSecret — it reuses the same <agentruntime>-tool-secrets companion Secret and never-in-ConfigMap guarantee. A key there that collides with a header the auth stanza owns is rejected: Authorization, or the custom auth.workloadIdentity.header when one is set.

A missing Secret/key or an unsupported type fails the AgentRuntime reconcile — it does not silently send an unauthenticated request. A workloadIdentity handler whose token cannot be acquired at call time fails that tool call rather than calling the backend unauthenticated.

See Authenticate tools for Azure workload-identity cluster setup and for a task-oriented walkthrough (creating the credential Secret and verifying the mounted <agentruntime>-tool-secrets).

When the runtime and a sandbox both reach a service, auth.connectionRef lets the handler use the credential of the sandbox’s Connection instead of a second copy in its own Secret. Rotating the Connection’s Secret, or deleting the Connection, then reaches both.

handlers:
- name: crm
type: http
httpConfig:
endpoint: https://crm.example.com/v1/lookup
auth:
type: bearer
connectionRef:
name: crm-shared # a Connection in the AgentRuntime's namespace
key: token # a key of the Connection's Secret

auth.type still says how the credential is sent. The operator gives it to the handler only when all of these hold:

  • the Connection exists and its credential kind is secret;
  • the Secret it names has the key;
  • every URL the handler sends the credential to is https on port 443, at a host the Connection lists. Those URLs are httpConfig.endpoint and urlTemplate; mcpConfig.endpoint; openAPIConfig.specURL and baseURL (an openapi handler must set baseURL); and grpcConfig.endpoint (a grpc handler must set tls).

The HandlerConnectionsValid condition reports the result. When a check fails, for example because the Connection or its Secret was deleted, the operator removes the handler’s key from <agentruntime>-tool-secrets. It does not keep the last value. The runtime then refuses each call to that handler instead of calling unauthenticated. A Connection is looked up in the AgentRuntime’s namespace, the same as a secretRef. HandlerConnectionsValid checks it in the registry’s namespace.

The endpoint (host:port) lives in grpcConfig.endpoint, required unless the handler addresses a sandbox:

- name: grpc-tools
type: grpc
grpcConfig:
endpoint: tool-service.tools.svc.cluster.local:50051
tls: false
tlsInsecureSkipVerify: false
tool:
name: process_data
description: "Process data via gRPC"
inputSchema:
type: object
properties:
data:
type: string
required: [data]
FieldTypeDefaultDescription
endpointstring—gRPC server address (host:port). Required unless the handler sets sandbox, and left unset when it does
tlsboolfalseEnable TLS
tlsCertPathstring—Path (in the runtime container) to a client certificate for mutual TLS; used only together with tlsKeyPath
tlsKeyPathstring—Path to the client certificate’s private key
tlsCAPathstring—Path to a CA bundle for verifying the server
tlsInsecureSkipVerifyboolfalseSkip TLS verification
retryPolicyobject—Retry behaviour (per gRPC status code)

The backend behind grpcConfig.endpoint must implement the Omnia Tool protocol defined by api/proto/tools/v1/tools.proto (ToolService), not an arbitrary gRPC API. See Tool backend protocol for the full contract. The controller requires a tool definition on a grpc handler, so the backend needs Execute only.

Model Context Protocol handlers automatically discover tools from the MCP server. No tool definition is required.

SSE Transport (connect to MCP server via Server-Sent Events):

- name: mcp-server
type: mcp
mcpConfig:
transport: sse
endpoint: http://mcp-server.tools.svc.cluster.local:8080/sse

Streamable HTTP Transport:

- name: mcp-http
type: mcp
mcpConfig:
transport: streamable-http
endpoint: http://mcp-server.tools.svc.cluster.local:8080/mcp

There is no stdio transport. An MCP server reaches the runtime over the network or not at all — the runtime image is distroless/static, so there is no subprocess to spawn, and stdio carried no caller identity. To give an agent local tooling including a shell, use a codegen sandbox.

FieldTypeRequiredDescription
transportstringYessse or streamable-http
endpointstringYesServer URL, or a path on the sandbox port when the handler sets sandbox
toolFilterobjectNoallowlist / blocklist of tool names to expose
retryPolicyobjectNoRetry behaviour for CallTool failures

auth is supported on both transports: the credential is attached as a transport header.

Reconcile rejects an endpoint whose host is a link-local or cloud-metadata address (a literal address or a well-known metadata hostname) and sets HandlersValid=False. In-cluster Service names and private addresses are allowed.

A non-sandbox MCP handler holds one session for the life of the pod. When that session is lost, the runtime re-dials, re-lists the handler’s tools and retries the call once, whether or not retryPolicy is set. A session that ends is re-dialled in the background with capped backoff.

OpenAPI handlers automatically discover tools from an OpenAPI/Swagger specification. Each operation becomes a tool.

- name: petstore
type: openapi
openAPIConfig:
specURL: https://petstore.swagger.io/v2/swagger.json
baseURL: https://petstore.swagger.io/v2
operationFilter:
- getPetById
- findPetsByStatus
FieldTypeRequiredDescription
specURLstringYesURL to an OpenAPI v2 or v3 spec in JSON. A YAML spec fails to parse
baseURLstringNoOverride the base URL from spec. A path on the sandbox port when the handler sets sandbox
operationFilter[]stringNoLimit to specific operation IDs
headersmapNoAdditional headers for requests
retryPolicyobjectNoRetry behaviour (uses the HTTP retry policy shape)
authTypestringNoDeprecated. Use the handler-level auth stanza instead.
authSecretRefobjectNoDeprecated. Use the handler-level auth stanza instead.

client handlers are executed by the connected browser client over the WebSocket facade, not by the runtime. They take an explicit tool definition like HTTP/gRPC handlers. See Client-side tools.

- name: geolocation
type: client
tool:
name: get_location
description: "Read the user's current location from the browser"
inputSchema:
type: object
clientConfig:
consentMessage: "Share your location with the agent?"
categories: [location]

clientConfig.consentMessage and clientConfig.categories are sent on each tool_call as consent_message and categories. Each one that is set replaces the value the PromptPack tool declares.

platform handlers have no backend to dial: the operator serves their tools itself, over its own REST API. The handler names which platform service it exposes: discovery, the workspace catalog, or arena, real-provider runs of the project mounted in the caller’s sandbox.

- name: catalog
type: platform
platform:
service: discovery
FieldTypeRequiredDescription
servicestringYesThe platform service this handler exposes: discovery or arena.

The CRD rejects a platform handler that also sets tool, httpConfig, mcpConfig, grpcConfig, openAPIConfig, clientConfig, sandbox, auth or delegation. A platform handler has no address, no declared tool, and its delegation token is implied rather than author-configured.

A service: discovery handler renders three tools, self-described like an MCP or OpenAPI handler. Discovery is search-first: an agent calls catalog_overview to see what exists, catalog_search to find candidates by what it needs, and catalog_get for full detail on the ones it picked.

ToolArgumentsReturns
catalog_overviewnoneA count per kind (including skill), and the groups (tool registries, skill sources) an agent can narrow a search by, each with a count and readiness
catalog_searchquery?, kinds? (comma-separated, e.g. tool,skill), group? (a group id from catalog_overview, e.g. toolRegistry:billing), readyOnly? ("true"/"false"), limit? (default 10, max 50), cursor?A ranked page of compact results (id, kind, name, group, a description truncated to 200 characters, ready) and a nextCursor when more remain. No query browses every entry in id order
catalog_getids (required, comma-separated, at most 20, exactly as catalog_search returned them)Full detail per found id (tool schema, A2A card, provider pricing, a skill’s allowed tools, and so on); unknown or malformed ids come back in missing with a reason, never as a failure of the whole call

Every argument is a string or a number, never an array: the platform handler’s rendered HTTP tools send GET arguments as query parameters formatted with %v, which would render an array as [a b].

Every result and entry carries a stable id: tool:<registry>/<name> and skill:<source>/<name> for the two per-group kinds, <kind>:<name> for every other kind (provider, agent, skillSource, connection, sandboxClass, promptPack, memorySchema). A group id is toolRegistry:<name> or skillSource:<name>. Skills are a searchable kind: catalog_search reads them from a per-SkillSource index the SkillSource reconciler writes after each sync, so a skill is findable by name or description without a body fetch — catalog_get’s skill detail is {source, name, description, allowedTools, relPath}, never the SKILL.md text. allowedTools is returned detail only; the ranker does not score against it.

readyOnly is a string ("true"/"false"), not a JSON boolean: over MCP the tool’s declared argument type is string, so a bare JSON true/false is refused rather than coerced.

An agent lists these three tool names in its PromptPack exactly like any other ToolRegistry tool — a platform handler carries no implicit exemption from the pack’s tool allowlist. Each call reaches the operator’s own catalog over GET /api/v1/workspaces/{workspace}/catalog/{overview,search,get}, authenticated per call by a delegation token whose audience is the agent’s workspace rather than this handler, so it cannot be replayed against another workspace. Errors follow the same shape whichever way the catalog is reached: an unknown kind in kinds is 400 naming the valid kinds, a group that is not a toolRegistry:<name> or skillSource:<name> id is 400, a cursor reused with a different query or filter set is 400, and more than 20 ids to catalog_get is 400.

Every caller admitted through this handler sees the same catalog — an ordinary conversation, a scheduled function fire, and a session with an externally verified end user all get the same viewer-level view. The handler being present in an agent’s own ToolRegistry is the opt-in: no separate grant object names it.

- name: arena
type: platform
platform:
service: arena

A service: arena handler renders three tools that let an agent run the arena project mounted in its own sandbox on the workspace’s real Providers. No provider credential enters the sandbox: the run is an ordinary ArenaJob, executed by the arena controller’s workers.

ToolArgumentsReturns
arena_check_projectnone{project, problems}: file validation, deploy.config binding and tool-wiring problems (a declared tool no prompt allows, or a scenario asserting a call its prompt cannot make), each {source, severity, file?, line?, column?, message, remediation?}. It checks the declaring user’s working clone as it stands, uncommitted changes included
arena_start_runkind (required; only evaluation), scenarios? and providers? (comma-separated){runId, job, message?}. The run executes the commit the declaring user’s working clone has checked out, and only once it is pushed (see below). Without providers, providers resolve from that commit’s deploy.config bindings, and any binding error refuses the run (422, with the problems). providers names workspace Providers that must exist and be Ready
arena_get_runrunId (required), waitSeconds? (at most 60)The run’s phase and progress, and once it finishes a compact result: pass rate (a percentage, 0–100), item counts, up to 50 scenario × provider cells, 10 failed assertions and 5 distinct errors

arena_start_run is a POST with a JSON body; the other two are GETs. Each call reaches /api/v1/workspaces/{workspace}/platform/arena/{check|start|get} with a delegation token minted for that one tool, and a token minted for one arena tool is refused by the others.

The project is never an argument. It is the one mounted in the calling session’s SandboxClaim — a claim the operator itself wrote, as recorded by its ProvenanceVerified condition. A session with no project mounted gets 409 “no project is mounted in this session”, and arena_get_run on a run of another project gets 403. runId may be the job name arena_start_run returned before its run existed; arena_get_run follows it to the run. Every error body is {error, problems?}.

The working clone is the one the claim’s spec.declaredBy names: the verified user whose session declared the mount, never anything in the request. arena_start_run builds only from pushed work, because the agent never pushes and the person’s push is the approval a real-provider run waits for. Before it starts anything, it fetches a hosted project’s repository into the clone and refuses with 409:

errorWhen
commit and push first; main has 2 uncommitted changesThe clone has uncommitted or untracked changes
commit and push first; main is 1 aheadThe branch has commits the repository does not
commit and push first; main has no commitsThe branch has no commits yet
commit and push first; HEAD is not on a branch; switch back to oneHEAD is detached
the workspace is busy: ...Another git operation holds the clone’s index.lock

A clone that is behind but not ahead runs its own HEAD, which is a pushed commit. The job’s spec.ref is that commit’s full SHA.

People reach the same operations through user routes on the deploy API, authorised by their workspace role rather than a session: GET .../arena/projects/{project}/problems (viewer), POST .../arena/projects/{project}/runs (editor) and GET .../arena/runs/{run}?wait= (viewer).

spec.sandboxes declares the per-session codegen sandboxes this registry’s handlers are served from, at most 16, keyed by name. A sandbox is a backend: its class says which ports it has, and the handlers that name it say what it serves. The operator claims one for each session that calls a handler on it. The AgentRuntime that uses the registry must set spec.sandbox.enabled.

spec:
sandboxes:
- name: workspace
class:
name: codegen-default
kind: ClusterSandboxClass
allocation: lazy
maxInstances: 1
targetPath: projects/checkout-service
connections:
- role: vcs
connection: github-shared
handlers:
- name: workspace
type: mcp
sandbox:
name: workspace # a sandbox in spec.sandboxes
port: mcp # a port its class declares
mcpConfig:
transport: sse
endpoint: /sse # a path on that port
toolFilter:
allowlist: [Read, Write, Edit, Glob, Grep, Bash]
FieldTypeRequiredDescription
namestringYesUnique sandbox name (^[a-z0-9]([-a-z0-9]*[a-z0-9])?$, max 63 chars). Handlers name it in sandbox.name, and it is the first part of each claim’s handle, <sandbox>/<instance>
class.namestringYesThe SandboxClass or ClusterSandboxClass each sandbox is built from
class.kindstringNoSandboxClass (default, looked up in the claim’s namespace) or ClusterSandboxClass
maxInstancesint32NoInstances of this sandbox one session may hold. Default 1, minimum 1. Above 1, every tool of every handler on the sandbox gains an optional sandbox argument that names which one to use; see Instances. AgentRuntime.spec.sandbox.maxPerSession caps the session’s total across sandboxes. Must be 1 when targetPath is set
allocationstringNolazy (default) claims on the first tool call of a handler on the sandbox; eager claims when the session starts. The runtime prepares each eager sandbox once per session, in the background and best-effort, on any facade (WebSocket, A2A, REST or MCP): a refusal or failure is logged, does not fail the session’s open or delay its first message, and the first tool call of a handler on the sandbox then acquires it as a lazy one would
workspacestringNoWhere each sandbox’s /workspace lives. claim (default) gives every sandbox its own scratch directory, lost with the sandbox. session gives each of the session’s handles one directory that outlives its sandboxes: a sandbox reaped when idle, or replaced after a failure, comes back to the same files. session needs a class with a workspace mount. See Session workspaces. A mount the session declares overrides it
targetPathstringNoProject the sandboxes sync their work back into, relative to the workspace content volume (for example projects/checkout-service). Max 512 chars. The operator copies it onto each claim. Empty makes the sandboxes scratch space, which cannot be synced back
connections[]{role, connection}NoThe sandbox’s default Connection for each role the class declares, at most 16. An AgentRuntime’s spec.sandbox.connections overrides a role for that agent

The CRD rejects a sandbox whose targetPath is set and whose maxInstances is above 1. Every instance takes the sandbox’s targetPath, so several instances would each promote into the same project, and the last to sync would replace the others’ work. Declare a separate sandbox, with its own targetPath, for each project.

The operator reads the class, maxInstances and targetPath from the sandbox each time it creates a claim, and resolves the Connection bindings: the AgentRuntime’s binding for each role, else the sandbox’s default. It copies the result onto the claim’s spec.connections. The runtime’s request names only the sandbox’s handle, <sandbox>/<instance>.

A handler’s sandbox block names the sandbox it is served from and the class port it dials:

FieldTypeRequiredDescription
sandbox.namestringYesA sandbox in spec.sandboxes. The SandboxesValid condition reports a name the registry does not declare
sandbox.portstringYesThe class port this handler dials. The SandboxesValid condition reports a port the class does not declare

The handler has no address of its own. The runtime asks the operator’s Sandbox API for the session’s sandbox, takes the address of the named port from the SandboxClaim it returns, and joins the handler’s endpoint path to it. http, openapi, grpc and mcp handlers can address a sandbox. The CRD rejects a sandbox block on a client or platform handler, and for the other types requires:

TypeOn a sandbox
mcpmcpConfig.endpoint is a path starting with /. mcpConfig.toolFilter.allowlist is required, because the sandbox’s tools are not listed until a session claims one
httphttpConfig.endpoint, and urlTemplate if set, are paths starting with /, joined to http:// and the address of the named port
openapiopenAPIConfig.baseURL, if set, is a path starting with /; it defaults to the port’s root, and the spec’s servers are ignored. specURL is read when the runtime starts, before any sandbox exists, so it must be reachable without one
grpcgrpcConfig.endpoint is left unset, and the runtime dials the port’s host:port. tool is required, because ListTools cannot be called before a sandbox exists

For mcp, the allowlist is the whole of what the model is offered: the class is a backend, and says nothing about its image’s tools, so another registry may offer different tools of the same class.

Several handlers can name one sandbox, each on its own port. They share its claims: a session holds one sandbox pod per sandbox and instance, whichever of its handlers is called first. Here an MCP server and an HTTP API served from the same pod reach the same /workspace:

spec:
sandboxes:
- name: build
class:
name: build-box
kind: SandboxClass
handlers:
- name: build-shell
type: mcp
sandbox: {name: build, port: mcp}
mcpConfig:
transport: sse
endpoint: /sse
toolFilter:
allowlist: [Read, Bash]
- name: build-status
type: http
sandbox: {name: build, port: api}
httpConfig:
endpoint: /status
method: GET
tool:
name: build_status
description: "Report the state of the current build"
inputSchema:
type: object

With workspace: session, the operator keeps each handle’s files for as long as the session is kept:

  • The directory is sessions/<runtime>/<key>/handles/<sandbox>/<instance> on the workspace content volume, where <key> is a hash of the session id. The operator derives it from the claim’s own runtime, session and handle. Nothing the client, the model or the sandbox says can name another one.
  • Every sandbox of that session and handle mounts it read-write, so the class needs only a scratch mount.
  • The operator removes the directory once the session is gone from its session-api, because retention dropped it or an erasure request deleted it, and once no claim of the session remains. It asks at least three times over at least an hour before removing anything. A workspace whose service group runs no session-api keeps its session directories. The directories of a deleted AgentRuntime are removed on the same schedule.

A session workspace claim is privileged, like a project mount: only the operator may create one, and it is honoured only while the chart’s sandbox-claim admission policy is in force (Kubernetes 1.30 or later). Where the policy is not in force, the operator refuses the sandbox’s claims (ClaimPolicyNotInForce) rather than falling back to scratch, because a claim anyone could write would reach any session’s files. A class with no filesystem.workspace (such as the chart’s browser class) has no workspace, so a sandbox of it must keep workspace: claim; the registry reports SandboxesValid=False with reason WorkspaceNotSupported otherwise.

A session workspace is the session’s own, not a project. With a targetPath, sync-back promotes from it exactly as it does from a scratch directory.

With maxInstances: 1, a session has one instance of the sandbox, under the handle <sandbox>/default, and the tools’ schemas are the pack’s.

With maxInstances above 1, the runtime adds one optional string property, sandbox, to the schema of every tool of every handler on the sandbox. The model names an instance with it, and each name is a separate sandbox of the same class for the rest of the session:

  • The property’s description tells the model the cap: Which sandbox to use. Omit for "default". Use a new name for a separate sandbox, up to <maxInstances>. Names are lowercase letters, digits and '-'.
  • An omitted sandbox means default.
  • A name must be a DNS label: lowercase letters, digits and -, starting and ending with a letter or digit.
  • The runtime removes sandbox from the arguments before it calls the tool, so the sandbox image never receives it. ToolPolicy rules and the recorded tool call still see it.
  • A call that would open a sandbox beyond maxInstances fails with MaxInstances, and the error lists the names the session already holds.
  • A sandbox image whose own tool already declares a sandbox argument is refused when the runtime connects to it, naming the tool.

Unused instances are freed by the idle reaper, like any other sandbox. See Use several sandboxes in one session.

Current phase of the ToolRegistry:

ValueDescription
PendingInitial state before the first reconcile completes
ReadyEvery discovered tool is Available
DegradedSome tools Available, some Unavailable (only when probing is enabled)
FailedA handler failed validation, no tools were discovered, or (with probing) no tool is reachable

By default a tool’s status reflects configuration validity only. Set spec.probe.enabled: true to have the controller periodically TCP-dial each tool’s resolved endpoint and mark it Available or Unavailable, which drives the registry phase (Ready when all reachable, Degraded when some are not, Failed when none are). It is a reachability check (a TCP connect), not a tool invocation, so it has no side effects on the backend.

spec:
probe:
enabled: true
interval: "60s" # how often to re-probe (default 60s)
timeout: "5s" # per-endpoint dial timeout (default 5s)
handlers:
- name: order-lookup
# …
FieldTypeDefaultDescription
enabledboolnone (required within spec.probe)Turn on reachability probing. Including the probe: stanza without enabled set is rejected by the API server; omitting probe entirely behaves as disabled.
intervalstring60sHow often endpoints are re-probed
timeoutstring5sPer-endpoint TCP dial timeout

Client tools (client://browser) have no network address and are never probed (they stay Available). Probing checks that the port accepts a TCP connection — it does not verify the tool actually works; Test a tool exercises the real call.

Total number of tools discovered across all handlers.

How many ToolPolicies name this registry in spec.selector.registry.

Zero is the value to look for. The policy broker is default-deny: a tool call is permitted only by a matching permit, so a registry that no policy selects refuses every call with deniedBy=no-matching-permit — while the registry itself reports Ready with its full tool count, because it is valid and its tools do exist. The POLICIES column and the PolicyBound condition are what tell the two apart.

The field is absent, rather than 0, on an install with no policy engine: without the ToolPolicy CRD the question does not apply, which is different from a registry that has no policy.

sha256:<hex> over the resolved tool set — tool names, descriptions, schemas and endpoints. It changes when what an agent can reach changes, and not when a probe flips a tool’s health.

When the controller last processed the handlers.

List of discovered tools with their status:

status:
discoveredTools:
- handlerName: calculator
name: calculate
status: Available
endpoint: https://api.example.com/calculate
- handlerName: petstore
name: petstore
status: Available
endpoint: https://petstore.swagger.io/v2/swagger.json
TypeDescription
ReadyTrue (reason ToolsDiscovered) when handlers are valid and at least one tool is available; False with reason ValidationFailed or NoToolsAvailable otherwise
DegradedTrue (reason SomeToolsUnavailable) when the phase is Degraded; False (reason AllToolsAvailable) otherwise
HandlersValidTrue when every handler passed validation; False (with the errors) otherwise
ToolsDiscoveredReports how many tools were discovered from how many handlers
PolicyBoundTrue when at least one ToolPolicy selects this registry, naming them; False when none does, which means the default-deny broker refuses every call. Absent when the ToolPolicy CRD is not installed
SandboxesValidTrue (reason SandboxesValid) when every sandbox’s class exists, and every handler on a sandbox names one in spec.sandboxes and a port its class declares. False with the reason of the first problem found, and a message naming each failing sandbox or handler: ClassNotFound or ClassResolveFailed for a sandbox’s class, WorkspaceNotSupported for workspace: session on a class with no workspace mount, SandboxNotDeclared for a handler naming a sandbox the registry does not declare, PortNotDeclared for a port the class does not declare, RuntimeClassNotFound for a class whose runtimeClassName names no RuntimeClass, UnsupportedOnRuntime for a class using a feature its runtime family cannot serve (on Kata-Firecracker, filesystem.workspace and filesystem.scratch). Re-evaluated when a referenced SandboxClass, ClusterSandboxClass or RuntimeClass changes. Absent when the registry has no sandbox and no handler on one
SandboxConnectionsValidTrue (reason ConnectionsValid) when every sandbox’s default connections check against its class: each role is declared, each Connection exists, a browserSession Connection is bound only to a role that declares its session delivery, and no two place the same environment variable or file. False with the check’s reason and a message naming the sandbox. A required role left unbound is not reported here, since an AgentRuntime may still bind it. Re-evaluated when a bound Connection changes. Absent when no sandbox has default bindings
HandlerConnectionsValidTrue (reason HandlerConnectionsValid) when every handler with auth.connectionRef can read its credential: the Connection exists and is kind secret, lists every host the handler sends the credential to (https on 443), and its Secret has the key. False with the check’s reason (ConnectionNotFound, ConnectionKindNotAccepted, ConnectionHostNotCovered, ConnectionSecretNotFound, ConnectionSecretKeyMissing) and a message naming the handler and, for a host, the host. Re-evaluated when the Connection or its Secret changes. Absent when no handler has auth.connectionRef

ToolRegistry with multiple handler types:

apiVersion: omnia.altairalabs.ai/v1alpha1
kind: ToolRegistry
metadata:
name: agent-tools
namespace: agents
spec:
handlers:
# Explicit HTTP tool with schema
- name: calculator
type: http
httpConfig:
endpoint: https://api.example.com/calculate
method: POST
tool:
name: calculate
description: "Perform mathematical calculations"
inputSchema:
type: object
properties:
expression:
type: string
description: "Mathematical expression to evaluate"
required: [expression]
timeout: "10s"
# gRPC tool service
- name: user-service
type: grpc
grpcConfig:
endpoint: user-grpc.internal.svc.cluster.local:50051
tool:
name: get_user
description: "Retrieve user information"
inputSchema:
type: object
properties:
user_id:
type: string
required: [user_id]
# Self-describing MCP server
- name: code-tools
type: mcp
mcpConfig:
transport: sse
endpoint: http://mcp-code.tools.svc.cluster.local:8080/sse
# Self-describing OpenAPI service
- name: external-api
type: openapi
openAPIConfig:
specURL: https://api.example.com/openapi.json
operationFilter:
- searchProducts
- getProductDetails

Status after reconcile:

status:
phase: Ready
discoveredToolsCount: 4
policyCount: 1
discoveredTools:
- handlerName: calculator
name: calculate
status: Available
- handlerName: user-service
name: get_user
status: Available
- handlerName: code-tools
name: code-tools
status: Available
- handlerName: external-api
name: external-api
status: Available
conditions:
- type: HandlersValid
status: "True"
- type: ToolsDiscovered
status: "True"
- type: PolicyBound
status: "True"
reason: PolicySelectsRegistry
message: "selected by 1 ToolPolicy(s): [calculator-policy]"