# The PromptPack Deploy Program

Omnia is a Kubernetes-native runtime, but pack authors work in
[PromptArena](https://promptkit.altairalabs.ai/) and do not use `kubectl`. The
**deploy program** bridges the two. It lets a PromptPack author push a pack into
an Omnia workspace without hand-writing CRDs, using the `promptarena-deploy-omnia`
deploy adapter.

This page explains the moving parts: the deploy-profile export, the
browser-login flow, and how a deployed pack becomes a running
[AgentRuntime](/reference/core/agentruntime/). For the step-by-step walkthrough, see
[Deploy a PromptPack to Omnia](/tutorials/deploy-to-omnia/).

## The problem it solves

A pack on its own is inert. To run it in Omnia you need:

- an **API endpoint** and **workspace** to deploy into,
- the target workspace configured with a ready **[service group](/how-to/workspaces/configure-service-groups/)** (the session-api / memory-api backend agents persist to),
- an **API credential** scoped to that workspace,
- one or more **Providers** to back the LLM calls, and
- optionally **SkillSources** for retrieval and tools.

Getting each of these names right by hand is error-prone. A Provider name that
matches nothing in the workspace is reported by the deploy plan as
`provider_not_found`, and a Provider that is not `Ready` leaves the AgentRuntime
`Pending` with `ProviderReady` false. The deploy-profile export gives the author
a pre-filled config block built from what the workspace has.

:::caution[The workspace needs a service group, or sessions silently vanish]
The deploy program creates a PromptPack and an AgentRuntime, but it does **not**
configure the workspace's session/memory backend. If the target workspace has no
ready service group, the deployed agent starts and chats but **silently falls back
to an in-memory session store**. Conversations, token usage, and cost are **not
persisted**, dashboard session views stay empty, and memory is unavailable.
Configure at least one service group (conventionally `default`) on the workspace
**before** deploying; see
[Configure workspace service groups](/how-to/workspaces/configure-service-groups/).
:::

## How it fits together

The adapter talks only to the dashboard's address, where management-api serves
every `/api` path. management-api answers discovery requests itself and forwards
deploys to the operator, which owns the translation into CRDs.

```mermaid
flowchart LR
  CLI["promptarena CLI + promptarena-deploy-omnia"]
  subgraph MA["management-api, at the dashboard's address"]
    PROFILE["GET /api/workspaces/{name}/deploy-profile"]
    DEPLOY["POST /api/workspaces/{name}/deployments"]
    LOGIN["/api/cli/authorize, grant, token"]
  end
  OP["Operator deploy-intent API (:8085)"]
  K8S["Kubernetes API"]
  CLI --> LOGIN
  CLI --> PROFILE
  CLI --> DEPLOY
  PROFILE -->|"list Provider, SkillSource"| K8S
  DEPLOY -->|"DeployIntent + identity JWT"| OP
  OP -->|"ConfigMap, PromptPack, AgentRuntime"| K8S
  K8S --> REC["AgentRuntimeReconciler"]
  REC -->|"Deployment + Service"| K8S
```

## The deploy profile

The **deploy profile** is a discovery payload for a single workspace. The
management-api serves it at `GET /api/workspaces/{name}/deploy-profile`. It is
discovery only, and the endpoint never returns a secret. It contains:

- `api_endpoint`: the dashboard's external ingress URL, derived from the
  forwarded host (falling back to `OMNIA_DASHBOARD_EXTERNAL_URL`).
- `workspace`: the target workspace name.
- `providers`: the discovery menu of Providers.
- `skills`: the discovery menu of SkillSources.

Two filters shape that menu.

The first filter lists only Providers and SkillSources whose `status.phase` is `Ready`
are listed. A Provider in phase `Error` or `Unavailable`, a SkillSource in phase
`Error`, and a resource that has no phase yet are all left out, because a
deployment that references one does not come up.

The second filter exports only `llm`-role Providers. The embedding,
tts, stt, image and inference roles are workspace-level services, consumed by
memory-api and similar subsystems, and are not per-agent extras. The rerank
role is reserved: no component consumes it, and memory retrieval does not
rerank. Bundling any of them into an agent's `spec.providers` breaks the pack
at its first request, so the export drops them. A Provider with no explicit `spec.role`
defaults to `llm`.

### Default-LLM picker and the token

The raw discovery payload lists real Provider names. Two things happen on top of
it when an author exports:

- The default-LLM picker: the export UI asks the author to pick one Provider
  and marks it `name: default` in the adapter config, with `ref` pointing at the
  real Provider CRD. The runtime loads the binding named `default` as its
  primary LLM. With no binding of that name, it uses the first binding by sorted
  name.
- The workspace-scoped token: the profile itself carries no secret, so the
  export mints an `omnia_sk_` API token scoped to this workspace. The token
  deploys only to that workspace, and it acts with the author's own role there.
  On re-export the dashboard offers to **reuse** the saved token or **regenerate**
  it (revoking the old key), so repeated exports do not pile up duplicate keys.
  If the deployment uses a read-only API-key store, the token is left as a
  placeholder for the author to mint by hand.

### Browser-login autoconfigure

When the dashboard runs in OAuth mode, the CLI can mint the whole config through
a browser-login flow (the same shape as `gcloud auth login`) instead of
copy-paste. The Omnia side is three endpoints.

```mermaid
sequenceDiagram
  participant CLI as promptarena CLI
  participant B as Browser
  participant M as management-api
  CLI->>B: open /api/cli/authorize?callback=loopback&state=nonce
  B->>M: GET /api/cli/authorize
  M-->>B: redirect to OIDC login, then the dashboard's /cli/select
  B->>M: POST /api/cli/grant (workspace)
  M-->>B: 303 to loopback callback with one-time code
  B->>CLI: GET callback?code=...&state=...
  CLI->>M: POST /api/cli/token (code)
  M-->>CLI: token + deploy profile
```

`GET /api/cli/authorize` stashes the CLI's loopback `callback` and `state`, and routes the browser through OIDC login to a workspace picker at `/cli/select`. `POST /api/cli/grant` re-checks the author's editor access, mints a one-time exchange code and 303-redirects the browser to the loopback callback, so no token crosses the browser. `POST /api/cli/token` is the only place a token is issued: a workspace-scoped token with a default TTL of 1 hour (`OMNIA_AUTH_CLI_TOKEN_TTL_SECONDS`), returned with the deploy profile.

`authorize` and `token` are public so an unauthenticated CLI can start and finish
the flow. `grant` needs the browser session and is same-origin only. Token
creation must be enabled for the exchange to succeed. The profile that comes
back is already in the adapter's config shape, because the CLI merges it into
`spec.deploy.config` unchanged: the endpoint, the workspace, and every Ready
`llm` Provider as a `{name, ref, role}` binding with one named `default`. It
carries no token, which the CLI stores separately, and no skills, which stay
opt-in. The precise CLI
command that drives the flow lives in the `promptarena` deploy tooling. See
[Set up the deploy config without the export](/how-to/agents/configure-deployment-setup/)
for the steps.

## The adapter config

The export produces the contents of `spec.deploy.config`, the mapping the
`promptarena-deploy-omnia` adapter consumes, with no `config:` key of its own,
because `promptarena deploy config import` deep-merges the file into that
mapping. It holds the endpoint, workspace and token, plus `providers` bindings (alias,
Provider `ref`, role) and `skills` entries that each name a SkillSource as
`source`. For the shape and a worked example, see
[Set up the deploy config without the export](/how-to/agents/configure-deployment-setup/).
The deploy-intent API that the config feeds is documented in the
[Deploy-intent contract](/reference/platform/deploy-intent/).

## From profile to AgentRuntime

The adapter does not write CRDs. It sends one versioned **DeployIntent** to
`POST /api/workspaces/{name}/deployments`, authenticated with the workspace-scoped
token. management-api forwards the document opaquely to the operator's deploy-intent
API with an identity token carrying the caller. The operator validates the intent
and creates the Kubernetes objects:

- a **PromptPack** and its content ConfigMap, holding the compiled pack (see
  [the PromptPack reference](/reference/core/promptpack/)). Skills attach here:
  the intent's `pack.skills` becomes the PromptPack's `spec.skills`, each entry
  naming a SkillSource.
- an **AgentRuntime** whose `promptPackRef` points at the pack and whose
  `spec.providers` carries the bindings, with the `default` binding as the
  primary LLM.

The AgentRuntime exposes the runtime through
[`spec.facades[]`](/reference/core/agentruntime/), a list of facade entries that
each serve one protocol. A pack deployed for browser chat lands as a single
`websocket` facade on port 8080 (the default). An agent can expose more surfaces,
for example an additional `a2a` entry, by listing more facade entries. The operator
then reconciles the AgentRuntime into a Deployment and Service, and the pack is
live.

The adapter config has no field for a cloud-specific ServiceAccount name, so an
AgentRuntime created this way carries none of its own. It falls back to the
workspace's [`spec.runtime.serviceAccountName`](/reference/core/workspace/#runtime)
default, so a keyless provider (`auth.type: workloadIdentity`) still
authenticates as long as the workspace configured that default.

## Why the intent, not the CRD

The alternative is for each client to author AgentRuntime bodies itself. The
`/agents` and `/promptpacks` routes management-api serves work that way: they
apply the caller's spec to the Kubernetes API unchanged, and the only validation
is the CRD's own OpenAPI and CEL schema. The dashboard's deploy wizard uses them.
That keeps management-api free of schema knowledge, but every client then owns a copy of the
AgentRuntime schema, and a client that falls behind the CRD is rejected by the
apiserver (for example `422 spec.facades: Required value`).

The DeployIntent moves that ownership to the operator. The adapter sends a
description of the deploy, the operator maps it onto whatever the installed CRDs
look like, and an adapter built against an older cluster keeps working. The
contract is versioned by `apiVersion`: the operator serves
`deploy.omnia.altairalabs.ai/v1` and answers anything else with `400`. The deploy
profile lists the accepted versions in `supportedDeployIntentVersions`, so an
adapter can check before it posts. The cost is a second schema to maintain, in
the operator's `internal/api/deploy` package, which has to grow with the CRDs.

A breaking AgentRuntime change still has to ship with the in-app deploy wizard,
because the wizard authors AgentRuntime bodies directly.

## Upgrading the CRDs

Upgrade the CRDs first, as their own release. They ship in a separate chart,
`omnia-crds`, so `helm upgrade omnia-crds` can apply schema changes. The app
chart's own `helm upgrade` never touches them:

```bash
helm upgrade omnia-crds oci://ghcr.io/altairalabs/charts/omnia-crds \
  --version <version> -n omnia-system
```

Skipping this leaves a new operator binary reconciling objects against an old CRD
schema. The app chart's preflight refuses to install or upgrade against older
CRDs, so a normal `helm upgrade omnia` cannot skip the step unless you set
`crdPreflight.enabled=false`.

## Related

- [Deploy a PromptPack to Omnia](/tutorials/deploy-to-omnia/): the walkthrough.
- [Set up the deploy config without the export](/how-to/agents/configure-deployment-setup/): browser login and manual setup.
- [Deploy-intent contract](/reference/platform/deploy-intent/): endpoints, result semantics and what a redeploy preserves.
- [PromptPack reference](/reference/core/promptpack/): the resource the deploy creates.
- [AgentRuntime reference](/reference/core/agentruntime/): `spec.facades[]` and provider binding.