Skip to content

Authoring a custom facade

Run your own image as an agent’s facade so the agent speaks a protocol Omnia does not ship, while the runtime sidecar, policy broker, sessions and exposure stay as they are. Your container terminates the client protocol, authenticates those clients, and sends each turn to the runtime sidecar over gRPC with the caller’s identity attached as metadata.

examples/custom-facade/ in the Omnia repository is a minimal conformant facade. Start from it.

  • A PromptPack, the Providers the agent uses, and an image the cluster can pull.
  • An AgentRuntime in mode: agent. The CRD rejects a custom facade in mode: function.
  • If the Enterprise webhook is installed: a licence that includes custom facades, or dev mode (Enterprise enabled, no licence), which admits everything.
  1. Add one type: custom entry to spec.facades[] with image set. The CRD rejects a custom facade without image.

    apiVersion: omnia.altairalabs.ai/v1alpha1
    kind: AgentRuntime
    metadata:
    name: my-agent
    spec:
    mode: agent
    promptPackRef:
    name: my-pack
    facades:
    - type: custom
    image: ghcr.io/acme/my-facade:v1.0.0
    port: 8080
    managementPlane: true
    expose:
    enabled: true

    port defaults to 8080, managementPlane defaults to true, and expose.enabled defaults to false.

  2. Make the custom facade the only facade. The API server rejects a custom entry beside any other: the operator takes the image, port, extraEnv, managementPlane, drainTimeout and expose from the primary facade, and a websocket, rest or a2a entry would outrank custom.

drainTimeout sets the pod’s termination grace period (the drain timeout plus 30 seconds). The operator passes neither drainTimeout nor clientToolTimeout to your container. See the AgentRuntime reference for every facade field.

Your container reads environment variables and binds ports. The diagram shows the ports in the agent pod.

flowchart LR
Client -->|"your protocol :8080"| Facade
Dashboard -->|"management JWT :18080"| Facade
Kubelet -->|"/healthz /readyz :8081"| Facade
subgraph Pod["Agent pod"]
Facade["facade container (your image)"]
Runtime["runtime container"]
Facade -->|"gRPC localhost:9000"| Runtime
end
  1. Read these environment variables.

    VariableValue
    OMNIA_AGENT_NAMEAgentRuntime name, from the pod’s app.kubernetes.io/instance label
    OMNIA_NAMESPACEPod namespace
    OMNIA_FACADE_PORTExternal listen port (facades[].port, default 8080)
    OMNIA_HEALTH_PORT8081
    OMNIA_HANDLER_MODEfacades[].handler, default runtime
    OMNIA_RUNTIME_ADDRESSlocalhost:9000. Set only when the handler mode is runtime.
    OMNIA_MGMT_PLANE_JWKS_URLDashboard JWKS URL. Set only when the dashboard is enabled.
    OMNIA_VARIANTstable or candidate
    POD_IPPod IP
    OMNIA_ROUTE_REDIS_URLRedis URL. Set only with a Redis context store.
    OMNIA_TRACING_ENABLED, _ENDPOINT, _INSECURESet only when tracing is enabled on the operator

    The operator appends facades[].extraEnv to the environment.

  2. Bind these ports.

    PortPurposeRequired
    OMNIA_FACADE_PORTYour external protocol listenerYes
    8081/healthz and /readyzYes
    18080Management-plane twinOnly when managementPlane is true

Your facade is the gRPC client. Dial OMNIA_RUNTIME_ADDRESS and call omnia.runtime.v1.RuntimeService, defined in api/proto/runtime/v1/runtime.proto. In mode: agent use the streaming Converse RPC. Invoke serves mode: function.

  1. Open a Converse stream and send a ClientMessage with session_id and either content or parts.
  2. Handle each ServerMessage variant:
    • chunk: a partial text fragment. Forward it to your client.
    • tool_call: a client-side tool call when execution is TOOL_EXECUTION_CLIENT. Forward it, collect the result, and send it back on the same stream as a ClientToolResult in a ClientMessage. Server-side tool calls never appear on the stream.
    • done: the turn is complete, with final_content, parts and usage.
    • error: a machine-readable code and a message.
    • media_chunk: raw binary media (no base64).
    • interruption: a barge-in on duplex audio. Clear buffered audio.
    • runtime_hello and heartbeat: the runtime’s capabilities and a keepalive. Do not forward either.
  3. For duplex audio, set duplex_start on the first ClientMessage of the stream and send inbound frames as audio_input.

The facade and runtime protocol reference lists every message and field.

Omnia does not sit in front of your external port. Validate your protocol’s credential, then attach the caller’s identity to every Converse call as lowercase x-omnia-* gRPC metadata. ToolPolicy rules that read identity.* see empty values without it.

  1. Emit a key only when its value is non-empty. The keys are x-omnia-agent-name, x-omnia-namespace, x-omnia-session-id, x-omnia-request-id, x-omnia-user-id, x-omnia-user-email, x-omnia-origin, x-omnia-workspace and one x-omnia-claim-<name> per identity claim. The flat metadata keys table defines each one, including x-omnia-subject-id.
  2. In Go, build the metadata with PropagationFields and ToGRPCMetadata from pkg/policy, as the example does.
  3. Do not emit x-omnia-provider or x-omnia-model. The runtime sets both and overwrites any value you send.
  4. Do not forward the caller’s Authorization token to the runtime. The propagation map excludes it so that tool upstreams never receive the caller’s credential. Keep the raw token inside your process.

See Configure tool policies for writing rules against the identity you send.

The operator probes your container on port 8081:

  • Readiness: GET /readyz
  • Liveness: GET /healthz
  1. Serve both endpoints and return a 2xx status when healthy. Without them the pod never becomes Ready and never joins the Service endpoints.
  2. Serve /readyz only once your runtime connection and external listener are up.
  3. Serve GET /prestop on the same port. The operator’s pre-stop hook calls it before SIGTERM. The built-in facade holds the request for 5 seconds so endpoint removal can propagate.

Set managementPlane: false to skip this step. The operator then allocates no facade-mgmt Service port and no status.managementEndpoints entry, and the dashboard’s “Try this agent” view does not work against the agent.

With managementPlane: true, the operator adds a ClusterIP-only facade-mgmt Service port for 18080 and publishes it as status.managementEndpoints.ws. Your container must:

  1. Bind a second listener on port 18080.
  2. Accept only RS256 JWTs management-api mints for the dashboard, verified against the keys at OMNIA_MGMT_PLANE_JWKS_URL. Fetch keys on demand and refetch when a key ID is unknown. The built-in facade also requires the aud claim omnia-facade and rejects a token whose agent claim names another agent.
  3. Reject every request without a valid token.
  4. Serve the WebSocket protocol at /ws for the dashboard’s debug view, which management-api relays as a WebSocket client.

expose.enabled: true creates a host-based HTTPRoute to the agent’s Service on the facade port when the platform has a default-exposure Gateway. Exposure adds no authentication.

  1. Apply the AgentRuntime and check its phase.

    Terminal window
    kubectl get agentruntime my-agent -o jsonpath='{.status.phase}{"\n"}'

    The phase is Running.

  2. Check the pod and the facade log.

    Terminal window
    kubectl get pods -l app.kubernetes.io/instance=my-agent
    kubectl logs deploy/my-agent -c facade

    Every container in the pod is Ready.

  3. With the management plane on, read the published port.

    Terminal window
    kubectl get agentruntime my-agent -o jsonpath='{.status.managementEndpoints}{"\n"}'

    The output contains "ws":18080.

  4. Send a request through your protocol and confirm the reply.

The Enterprise webhook found no licence entitlement. Install a licence, or run Enterprise without a licence for dev mode. See Install a licence.

The container does not return 2xx on :8081/readyz. Read the pod events with kubectl describe pod.