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
API version
Section titled “API version”apiVersion: omnia.altairalabs.ai/v1alpha1kind: ToolRegistryShort name: tr (e.g. kubectl get tr).
Spec fields
Section titled “Spec fields”handlers
Section titled “handlers”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]Handler types
Section titled “Handler types”| Type | Category | Description |
|---|---|---|
http | Explicit | HTTP REST endpoint |
grpc | Explicit | gRPC service using the Omnia Tool protocol |
mcp | Self-describing | Model Context Protocol server |
openapi | Self-describing | OpenAPI/Swagger-documented service |
client | Explicit | Browser-executed tool (runs in the connected client) |
platform | Self-describing | Served by the operator itself, no backend endpoint. See Platform handler |
Handler definition
Section titled “Handler definition”Common fields for all handler types:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique handler name (^[a-z0-9]([-a-z0-9]*[a-z0-9])?$, max 63 chars) |
type | string | Yes | Handler type (http, grpc, mcp, openapi, client, platform) |
tool | object | Conditional | Tool definition — required for http, grpc, and client. Not permitted on platform handlers |
httpConfig | object | Conditional | Required when type: http. Not permitted on platform handlers |
grpcConfig | object | Conditional | Required when type: grpc. Not permitted on platform handlers |
mcpConfig | object | Conditional | Required when type: mcp. Not permitted on platform handlers |
sandbox | object | No | {name, port}: serves the handler from one of the registry’s sandboxes. Not permitted on client or platform handlers. See Handlers on a sandbox |
openAPIConfig | object | Conditional | Required when type: openapi. Not permitted on platform handlers |
clientConfig | object | No | {consentMessage, categories} for a client handler. See Client handler |
platform | object | Conditional | Required when type: platform, and not permitted for any other type. See Platform handler |
auth | object | No | How the runtime authenticates to the backend (see Authenticating tools). Not permitted on platform handlers |
delegation.enabled | bool | No | Default 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 |
timeout | string | No | Per-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 |
Tool definition (for explicit handlers)
Section titled “Tool definition (for explicit handlers)”http, grpc, and client handlers require a tool definition:
| Field | Type | Required | Description |
|---|---|---|---|
tool.name | string | Yes | Tool name exposed to the LLM (^[a-z][a-z0-9_]*$, max 64) |
tool.description | string | Yes | Human-readable description |
tool.inputSchema | object | Yes | JSON Schema for input parameters |
tool.outputSchema | object | No | JSON Schema for output (optional) |
HTTP handler
Section titled “HTTP handler”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"HTTP configuration options
Section titled “HTTP configuration options”| Field | Type | Default | Description |
|---|---|---|---|
endpoint | string | — | Required. HTTP endpoint URL, or a path on the sandbox port when the handler sets sandbox |
method | string | POST | HTTP method |
headers | map | — | Additional HTTP headers |
headersFromSecret | map | — | 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. |
contentType | string | application/json | Content-Type header |
retryPolicy | object | — | Retry behaviour. See HTTP retry policy |
authType | string | — | Deprecated. Use the handler-level auth stanza instead. Auth type (bearer or basic). |
authSecretRef | object | — | 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.
HTTP retry policy
Section titled “HTTP retry policy”httpConfig.retryPolicy and openAPIConfig.retryPolicy share this shape. With
no retryPolicy, a call is attempted once.
| Field | Type | Default | Description |
|---|---|---|---|
maxAttempts | int | — | Required. Total attempts including the first, 1 to 10. 1 disables retries |
initialBackoff | duration | 100ms | Delay before the first retry |
backoffMultiplier | string | "2.0" | Decimal multiplier between retries, at least 1.0 |
maxBackoff | duration | 30s | Upper 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 |
retryOnNetworkError | bool | true | Retry connection, DNS and timeout errors, which occur before a status code arrives |
respectRetryAfter | bool | true | On 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.
Authenticating tools
Section titled “Authenticating tools”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| Field | Type | Default | Description |
|---|---|---|---|
auth.type | string | none | Authentication mechanism: none, bearer, basic, serviceAccount, or workloadIdentity. |
auth.secretRef | object | - | Secret holding the credential. bearer/basic need this or auth.connectionRef. |
auth.connectionRef | object | - | {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.audience | string | - | Audience the projected ServiceAccount token binds to (required for serviceAccount). |
auth.workloadIdentity | object | - | 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 resolvessecretRefinto an operator-managed<agentruntime>-tool-secretsSecret, 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 asAuthorization: Bearer <token>.workloadIdentity— resolved by the runtime under the pod’s ambient Azure identity (core);cloudmust beazure. The runtime acquires a token foraudienceand sets it onheader(defaultAuthorization). Supported on http, grpc, and mcp (sse / streamable-http) and openapi handlers; any other handler type withworkloadIdentityfails 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).
Reading the credential from a Connection
Section titled “Reading the credential from a Connection”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 Secretauth.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
httpson port 443, at a host the Connection lists. Those URLs arehttpConfig.endpointandurlTemplate;mcpConfig.endpoint;openAPIConfig.specURLandbaseURL(an openapi handler must setbaseURL); andgrpcConfig.endpoint(a grpc handler must settls).
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.
GRPC handler
Section titled “GRPC handler”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]GRPC configuration options
Section titled “GRPC configuration options”| Field | Type | Default | Description |
|---|---|---|---|
endpoint | string | — | gRPC server address (host:port). Required unless the handler sets sandbox, and left unset when it does |
tls | bool | false | Enable TLS |
tlsCertPath | string | — | Path (in the runtime container) to a client certificate for mutual TLS; used only together with tlsKeyPath |
tlsKeyPath | string | — | Path to the client certificate’s private key |
tlsCAPath | string | — | Path to a CA bundle for verifying the server |
tlsInsecureSkipVerify | bool | false | Skip TLS verification |
retryPolicy | object | — | 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.
MCP handler (self-describing)
Section titled “MCP handler (self-describing)”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/sseStreamable HTTP Transport:
- name: mcp-http type: mcp mcpConfig: transport: streamable-http endpoint: http://mcp-server.tools.svc.cluster.local:8080/mcpThere 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.
MCP configuration options
Section titled “MCP configuration options”| Field | Type | Required | Description |
|---|---|---|---|
transport | string | Yes | sse or streamable-http |
endpoint | string | Yes | Server URL, or a path on the sandbox port when the handler sets sandbox |
toolFilter | object | No | allowlist / blocklist of tool names to expose |
retryPolicy | object | No | Retry 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 handler (self-describing)
Section titled “OpenAPI handler (self-describing)”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 - findPetsByStatusOpenAPI configuration options
Section titled “OpenAPI configuration options”| Field | Type | Required | Description |
|---|---|---|---|
specURL | string | Yes | URL to an OpenAPI v2 or v3 spec in JSON. A YAML spec fails to parse |
baseURL | string | No | Override the base URL from spec. A path on the sandbox port when the handler sets sandbox |
operationFilter | []string | No | Limit to specific operation IDs |
headers | map | No | Additional headers for requests |
retryPolicy | object | No | Retry behaviour (uses the HTTP retry policy shape) |
authType | string | No | Deprecated. Use the handler-level auth stanza instead. |
authSecretRef | object | No | Deprecated. Use the handler-level auth stanza instead. |
Client handler (browser-executed)
Section titled “Client handler (browser-executed)”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 handler (self-describing)
Section titled “Platform handler (self-describing)”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: discoveryPlatform configuration options
Section titled “Platform configuration options”| Field | Type | Required | Description |
|---|---|---|---|
service | string | Yes | The 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.
| Tool | Arguments | Returns |
|---|---|---|
catalog_overview | none | A 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_search | query?, 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_get | ids (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.
service: arena
Section titled “service: arena”- name: arena type: platform platform: service: arenaA 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.
| Tool | Arguments | Returns |
|---|---|---|
arena_check_project | none | {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_run | kind (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_run | runId (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:
error | When |
|---|---|
commit and push first; main has 2 uncommitted changes | The clone has uncommitted or untracked changes |
commit and push first; main is 1 ahead | The branch has commits the repository does not |
commit and push first; main has no commits | The branch has no commits yet |
commit and push first; HEAD is not on a branch; switch back to one | HEAD 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).
Sandboxes
Section titled “Sandboxes”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]Sandbox fields
Section titled “Sandbox fields”| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique 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.name | string | Yes | The SandboxClass or ClusterSandboxClass each sandbox is built from |
class.kind | string | No | SandboxClass (default, looked up in the claim’s namespace) or ClusterSandboxClass |
maxInstances | int32 | No | Instances 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 |
allocation | string | No | lazy (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 |
workspace | string | No | Where 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 |
targetPath | string | No | Project 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} | No | The 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>.
Handlers on a sandbox
Section titled “Handlers on a sandbox”A handler’s sandbox block names the sandbox it is served from and the class
port it dials:
| Field | Type | Required | Description |
|---|---|---|---|
sandbox.name | string | Yes | A sandbox in spec.sandboxes. The SandboxesValid condition reports a name the registry does not declare |
sandbox.port | string | Yes | The 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:
| Type | On a sandbox |
|---|---|
mcp | mcpConfig.endpoint is a path starting with /. mcpConfig.toolFilter.allowlist is required, because the sandbox’s tools are not listed until a session claims one |
http | httpConfig.endpoint, and urlTemplate if set, are paths starting with /, joined to http:// and the address of the named port |
openapi | openAPIConfig.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 |
grpc | grpcConfig.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: objectSession workspaces
Section titled “Session workspaces”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
scratchmount. - 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.
Sandbox instances
Section titled “Sandbox instances”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
sandboxmeansdefault. - A name must be a DNS label: lowercase letters, digits and
-, starting and ending with a letter or digit. - The runtime removes
sandboxfrom 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
maxInstancesfails withMaxInstances, and the error lists the names the session already holds. - A sandbox image whose own tool already declares a
sandboxargument 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.
Status fields
Section titled “Status fields”Current phase of the ToolRegistry:
| Value | Description |
|---|---|
Pending | Initial state before the first reconcile completes |
Ready | Every discovered tool is Available |
Degraded | Some tools Available, some Unavailable (only when probing is enabled) |
Failed | A handler failed validation, no tools were discovered, or (with probing) no tool is reachable |
Probing
Section titled “Probing”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 # …| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | none (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. |
interval | string | 60s | How often endpoints are re-probed |
timeout | string | 5s | Per-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.
discoveredToolsCount
Section titled “discoveredToolsCount”Total number of tools discovered across all handlers.
policyCount
Section titled “policyCount”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.
contentDigest
Section titled “contentDigest”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.
lastDiscoveryTime
Section titled “lastDiscoveryTime”When the controller last processed the handlers.
discoveredTools
Section titled “discoveredTools”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.jsonconditions
Section titled “conditions”| Type | Description |
|---|---|
Ready | True (reason ToolsDiscovered) when handlers are valid and at least one tool is available; False with reason ValidationFailed or NoToolsAvailable otherwise |
Degraded | True (reason SomeToolsUnavailable) when the phase is Degraded; False (reason AllToolsAvailable) otherwise |
HandlersValid | True when every handler passed validation; False (with the errors) otherwise |
ToolsDiscovered | Reports how many tools were discovered from how many handlers |
PolicyBound | True 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 |
SandboxesValid | True (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 |
SandboxConnectionsValid | True (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 |
HandlerConnectionsValid | True (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 |
Complete example
Section titled “Complete example”ToolRegistry with multiple handler types:
apiVersion: omnia.altairalabs.ai/v1alpha1kind: ToolRegistrymetadata: name: agent-tools namespace: agentsspec: 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 - getProductDetailsStatus 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]"