# Authoring a custom runtime

Run your own container as an agent's runtime by serving `omnia.runtime.v1.RuntimeService` and declaring the image on the AgentRuntime. The facade sidecar dials your container for every turn, and the policy broker, sessions and exposure stay as they are. A custom runtime needs no licence entitlement. A [custom facade](/how-to/security/authoring-a-custom-facade/) gets an admission warning when the Enterprise webhook is on and the licence lacks the custom-facade entitlement, and is admitted.

A minimal conformant runtime lives at `examples/custom-runtime/av-preprocessor/` in the Omnia repository. `examples/custom-runtime/` also shows how to run a runtime offline against a directory of manifests.

## Prerequisites

- A container image in a registry the cluster can pull from.
- A gRPC server for `omnia.runtime.v1.RuntimeService`, in any language. Generate stubs from `api/proto/runtime/v1/runtime.proto` at a pinned git ref and do not hand-copy the file.
- A checkout of the Omnia repository, to build the conformance CLI in step 5.

## Step 1: Declare the custom runtime

Set `spec.framework.type: custom` and `spec.framework.image`. Only `promptkit` has a built-in image. A `custom` or `langchain` AgentRuntime with no image blocks with `FrameworkImageUnavailable`.

```yaml
apiVersion: omnia.altairalabs.ai/v1alpha1
kind: AgentRuntime
metadata:
  name: my-agent
spec:
  promptPackRef:
    name: my-pack
  framework:
    type: custom
    image: ghcr.io/acme/my-runtime:v1.0.0
```

To set the image for every AgentRuntime of a type, pass the repeatable operator flag `--framework-image=custom=ghcr.io/acme/my-runtime:v1.0.0`. `spec.framework.image` takes precedence over the flag.

## Step 2: Read the environment and open the ports

The operator injects the runtime container's wiring. Read these variables instead of the AgentRuntime CRD.

| Variable | Value |
|----------|-------|
| `OMNIA_AGENT_NAME` | AgentRuntime name (from a pod label) |
| `OMNIA_NAMESPACE` | Pod namespace |
| `OMNIA_GRPC_PORT` | `9000`. Serve `RuntimeService` here. |
| `OMNIA_HEALTH_PORT` | `9001`. Serve HTTP `GET /healthz` here. |
| `OMNIA_PROMPTPACK_PATH` | The PromptPack file, `/etc/omnia/pack/pack.json`. Unset for a live pack. |

The readiness and liveness probes issue `GET /healthz` against the health port. A runtime that does not serve it never becomes Ready.

The facade dials `localhost:9000`. To run a runtime outside a cluster, set `OMNIA_CONFIG_DIR` to a directory of YAML manifests. Omnia's Kubernetes client then serves the CRDs from those files instead of an API server. `examples/custom-runtime/devroot/` is a working example.

## Step 3: Serve the contract

Implement the four `RuntimeService` methods below. Two more are optional, and the facade tolerates `Unimplemented` from either:

- `EndConversation`: the facade calls it when a WebSocket conversation ends. Implement it only if you hold per-conversation resources worth releasing.
- `WatchInvocation` (contract 1.8.0): a stream of one function invocation's tool calls and tool results, which the facade relays to a caller that asked for an event stream. Without it, that caller gets the outcome with no progress. If you implement it, send `ready` first, end the stream once the invocation has finished, and serve it to loopback callers only.

| Method | Behaviour |
|--------|-----------|
| `Health` | Return `healthy`, the `contract_version` you built against (semver), and your `capabilities`. |
| `Converse` | A bidirectional stream of `ClientMessage` and `ServerMessage`. `ClientMessage` is not a `oneof`, so several fields can be set at once: handle each one you receive and do not drop a part silently. Your first `ServerMessage` is a `RuntimeHello`. |
| `Invoke` | One-shot function mode (`spec.mode: function`). If you only serve `spec.mode: agent`, return `Unimplemented` and do not advertise `invoke`. |
| `HasConversation` | Return `RESUMABLE`, `NOT_FOUND` or `UNAVAILABLE` for a session's working context. |

Read caller identity from the flat `x-omnia-*` gRPC metadata. The raw bearer token is withheld. Do not forward the caller's credentials to third-party tool upstreams. See [identity and claims metadata](/reference/platform/facade-runtime-protocol/#identity--claims-metadata) for the keys.

### If you write Go

Implement `Handler` from `github.com/altairalabs/omnia/pkg/runtime` and call `runtime.Serve`. The SDK sends the `RuntimeHello` first, answers `Health`, runs client-tool round-trips and registers the standard gRPC health service. If `Converse` returns without calling `Done`, the SDK sends an empty one.

```go
import (
	"context"
	"net"

	rt "github.com/altairalabs/omnia/pkg/runtime"
	"github.com/altairalabs/omnia/pkg/runtime/contract"
)

type myRuntime struct{}

func (myRuntime) Capabilities() []string { return []string{contract.CapabilityClientTools} }

func (myRuntime) Converse(ctx context.Context, turn rt.Turn, emit rt.Emitter) error {
	// Call your model or framework and stream output.
	if err := emit.Chunk("hello"); err != nil {
		return err
	}
	return emit.Done(rt.Done{Final: "hello"})
}

func main() {
	lis, _ := net.Listen("tcp", ":9000")
	_ = rt.Serve(lis, myRuntime{})
}
```

To serve `Invoke`, implement the optional `Invoker` interface and list `contract.CapabilityInvoke` in `Capabilities()`. To answer resume probes, implement `ConversationProber`. A handler without it reports every session as `UNAVAILABLE`. The SDK refuses duplex sessions, so do not advertise `duplex_audio` from it.

To wrap Omnia's own PromptKit runtime, import `github.com/altairalabs/omnia/pkg/runtime/promptkit`.

## Step 4: Advertise capabilities

List the optional surfaces you implement in `HealthResponse.capabilities`, and send the same list in the `RuntimeHello`. The [capabilities table](/reference/platform/facade-runtime-protocol/#capabilities) lists the known names. The set is open, so you can advertise a name outside it.

Advertise only what works. The conformance suite fails a runtime that advertises `invoke` or `duplex_audio` and then returns `Unimplemented`. The operator also requires `duplex_audio` and `interruption` for an AgentRuntime with duplex enabled, and `invoke` for one with a `rest` or `mcp` facade. If the runtime advertises less, the operator scales the agent to zero.

For a duplex session, the `RuntimeHello` can carry a `MediaNegotiation` counter-offer. The facade relays an audio counter-offer to the client as a `session_config` message and fails the session closed on a video one. See [per-session negotiation](/reference/platform/facade-runtime-protocol/#per-session-negotiation).

## Step 5: Run the conformance suite

Build the CLI from the repository root and point it at your running runtime.

```sh
go build -o runtime-conformance ./cmd/runtime-conformance
./runtime-conformance --addr localhost:9000
```

The suite checks, over gRPC only:

- `Health` is healthy and `contract_version` is semver.
- The first `Converse` frame is a `RuntimeHello` whose capabilities match `Health`.
- A text turn ends with `done`, and no `done` arrives before the hello.
- An empty `ClientMessage` gets an on-protocol answer, not an internal stream error.
- Capability honesty: with `invoke` advertised, `Invoke` works. Without it, `Invoke` returns `Unimplemented`. With `duplex_audio` advertised, a duplex session emits a frame. Without it, the duplex check is skipped.

The CLI prints a per-check table. It exits `0` when conformant, `1` when any check fails and `2` when it cannot dial the address.

## Verify

After you apply the AgentRuntime, confirm the operator accepted the runtime's capabilities.

```sh
kubectl get agentruntime my-agent -o jsonpath='{.status.runtimeCapabilities}'
kubectl get agentruntime my-agent \
  -o jsonpath='{.status.conditions[?(@.type=="CapabilitiesSatisfied")].status}'
```

The first command prints the capability list your runtime reported. The second prints `True`.

## Troubleshooting

### The AgentRuntime reports FrameworkImageUnavailable

The `custom` type has no built-in image. Set `spec.framework.image`, or start the operator with `--framework-image=custom=<image>`.

### The runtime pod never becomes Ready

The probes call `GET /healthz` on `OMNIA_HEALTH_PORT`. Serve that route, in addition to the gRPC port.

### CapabilitiesSatisfied is False with reason CapabilitiesMissing

The runtime advertises less than the spec requires. Add the missing capability to your runtime or change the spec. The condition message names the missing and advertised capabilities.

## Related pages

- [Facade to runtime protocol](/reference/platform/facade-runtime-protocol/): the message surface, capability table and identity metadata.
- `examples/custom-runtime/av-preprocessor/`: a minimal conformant runtime.
- [Authoring a custom facade](/how-to/security/authoring-a-custom-facade/): the facade counterpart.