## Related issue Follow-up to #4673. ## Summary - Add a typed, default-off release-feature registry driven by one comma-separated `OMNIGENT_FEATURES` environment variable, with strict validation and lifecycle metadata. - Gate the web Usage route/navigation and page-only report enrichment while preserving the existing `GET /v1/usage` CLI API. - Migrate web-driven harness installation to the same immutable startup snapshot and wire rollout configuration across Docker, Kubernetes, Render, Railway, and Databricks. ELI5: the server reads one list of enabled features when it starts, enforces that same list on backend routes, and tells the web app which controls and pages to show. ```text OMNIGENT_FEATURES | v FeatureFlags snapshot / \ backend gates GET /v1/info | v frontend gates ``` ## Test Plan - `uv run pytest tests/server/test_feature_flags.py tests/host/test_local_server.py tests/server/integration/test_utility_endpoints.py tests/server/integration/test_hosts_install_harness.py tests/server/integration/test_hosts_store_credential.py tests/server/routes/test_usage_report.py tests/server/test_openapi_drift.py -q` - `cd web && pnpm vitest run src/lib/capabilities.test.ts src/lib/harnessSetup.test.ts src/App.test.tsx src/shell/Sidebar.test.tsx` - `uv run pytest tests/e2e_ui/sessions/test_usage_page_feature.py -q` - `uv run python scripts/dump_openapi.py --check` - `pre-commit run --files <changed files>` - Verified default-off and enabled Usage route/sidebar behavior, strict unknown-feature rejection, legacy CLI usage compatibility, and harness route enforcement. ## Demo - Default off: the updated visual baselines show the original sidebar without the Usage row. - Enabled Usage page: https://github.com/user-attachments/assets/8385d4f0-47ad-430f-bf2c-06c35af6c499 ## Type of change - [ ] Bug fix - [x] Feature - [x] UI / frontend change - [ ] Refactor / chore - [x] Docs - [ ] Test / CI - [ ] Breaking change ## Test coverage - [x] Unit tests added / updated - [x] Integration tests added / updated - [x] E2E tests added / updated - [x] Manual verification completed - [x] Existing tests cover this change - [ ] Not applicable ## Coverage notes Manually reviewed the default-off visual output and verified that the Usage route is absent while the capability is disabled. Targeted backend and frontend tests cover both flag states, capability parsing, startup snapshots, and harness enforcement. ## Changelog Usage and web-driven harness setup can now be enabled per deployment with `OMNIGENT_FEATURES`. Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
14 KiB
Omnigent on Kubernetes
Deploy Omnigent to any Kubernetes cluster using Kustomize. The manifests pull
the prebuilt image and set up a persistent volume and health checks. They also
include an Ingress so you can serve the app over HTTPS at a public web address,
but that part is optional — it only matters when people need to reach the server
over the internet, and it pulls in two extra add-ons (ingress-nginx and
cert-manager). For local or dev use, ignore it and connect with kubectl port-forward (see Verify the deployment).
What gets provisioned
- Deployment — single-replica pod running
ghcr.io/omnigent-ai/omnigent-server, served on port 8000. - Service — ClusterIP on port 80 → 8000.
- Ingress (optional) — serves the app over HTTPS at a public web address, using cert-manager for the certificate. Skip it if the server isn't going on the internet.
- PVC — 10 Gi volume at
/data/artifactsfor the artifact store, minted cookie secret, and admin credentials. - ConfigMap + Secret — environment config and database credentials.
Prerequisites
- A Kubernetes cluster (1.25+)
kubectlwith Kustomize support (kubectl kustomizeor standalonekustomize)- A PostgreSQL database (managed or in-cluster — see below)
- Only if you're putting the server on a public web address: an ingress controller (e.g. ingress-nginx) and cert-manager
Install cluster add-ons for ingress and cert management (optional)
Skip this unless you're putting the server on a public web address. (For local
or dev use you'll reach it with kubectl port-forward, or you can let your own
load balancer or proxy handle HTTPS instead.) Otherwise, if your cluster doesn't
already have an ingress controller and cert-manager, install them (pin the
versions to taste):
# ingress-nginx — use the provider manifest that matches your cluster
# (this is the kind one; for EKS/GKE/AKS use that provider's manifest or Helm chart):
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
# cert-manager:
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml
# wait until both are ready:
kubectl wait -n ingress-nginx --for=condition=Ready pod \
-l app.kubernetes.io/component=controller --timeout=180s
kubectl wait -n cert-manager --for=condition=Available deployment --all --timeout=180s
Create a cert-manager issuer (optional)
Skip this unless you're using the Ingress. cert-manager fetches the HTTPS
certificate for the Ingress from a ClusterIssuer named letsencrypt-prod
(the cert-manager.io/cluster-issuer annotation in base/ingress.yaml). That
issuer is not shipped here — create one before deploying, or change the
annotation to match an issuer you already have. Two common choices:
# Production — real certificates from Let's Encrypt
# (needs a public domain and an Ingress reachable from the internet):
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: you@example.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
ingressClassName: nginx
# Local / dev — self-signed (no public DNS needed; browsers will warn):
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
selfSigned: {}
Apply your chosen issuer with kubectl apply -f <file>. Without it, cert-manager
logs IssuerNotFound and no certificate is issued (the server still runs — only
TLS is affected).
Release features
Release features are deployment-wide and off by default. Set the
comma-separated OMNIGENT_FEATURES value in base/configmap.yaml, apply your
Kustomize target, and restart the Deployment so every pod receives one fresh
startup snapshot:
kubectl kustomize deploy/kubernetes/base/ | kubectl apply -f -
kubectl rollout restart deployment/omnigent
kubectl rollout status deployment/omnigent
Use the same restart after removing a feature for rollback. See
designs/FEATURE_FLAGS.md for known keys.
Deploy with an external database
Use this path when you have a managed Postgres (RDS, Cloud SQL, Neon, etc.).
-
Edit the secret — set your real
DATABASE_URLand generate a cookie secret:# deploy/kubernetes/base/secret.yaml DATABASE_URL: "postgresql+psycopg://user:pass@your-db-host:5432/omnigent" OMNIGENT_ACCOUNTS_COOKIE_SECRET: "$(openssl rand -hex 32)" -
Set your domain (skip if you're not using the Ingress) — replace
omnigent.example.cominbase/ingress.yamlwith your domain, and make sure theletsencrypt-prodClusterIssuer exists (see Create a cert-manager issuer). -
Apply:
kubectl kustomize deploy/kubernetes/base/ | kubectl apply -f - -
Create the first admin. Open the app (via your Ingress host, or port-forward for a quick check — see Verify the deployment). With the default
accountsprovider the first visitor claims the instance: the Setup screen prompts for a username + password, and whoever finishes it first becomes the admin.
Deploy with in-cluster Postgres
The overlays/postgres/ overlay adds a single-replica Postgres 16 StatefulSet
with its own 10 Gi PVC. Good for dev/testing clusters.
-
Edit secrets — in
overlays/postgres/secret-patch.yaml, replacechangemewith real passwords:POSTGRES_PASSWORD: "<strong-password>" DATABASE_URL: "postgresql+psycopg://omnigent:<strong-password>@postgres:5432/omnigent" OMNIGENT_ACCOUNTS_COOKIE_SECRET: "$(openssl rand -hex 32)" -
Set your domain (skip if you're not using the Ingress) — edit the hostname in
base/ingress.yaml, and make sure theletsencrypt-prodClusterIssuer exists (see Create a cert-manager issuer). -
Apply:
kubectl kustomize deploy/kubernetes/overlays/postgres/ | kubectl apply -f -
Deploy with OpenShell sandboxes
The overlays/openshell/ overlay configures the server to provision
NVIDIA OpenShell sandboxes for managed
sessions, and includes RBAC for the
kubernetes-sigs/agent-sandbox
CRD when the gateway uses a Kubernetes compute driver.
-
Edit the configmap patch — set
OMNIGENT_SANDBOX_SERVER_URLto the public URL sandboxes will dial back to, and optionally setOPENSHELL_GATEWAYto a specific gateway name:# deploy/kubernetes/overlays/openshell/configmap-patch.yaml OMNIGENT_SANDBOX_SERVER_URL: "https://omnigent.example.com" OPENSHELL_GATEWAY: "my-gateway" -
Edit secrets — in
overlays/openshell/secret-patch.yaml, set the database URL, cookie secret, and the LLM API keys your harness needs:DATABASE_URL: "postgresql+psycopg://omnigent:<password>@your-db-host:5432/omnigent" OMNIGENT_ACCOUNTS_COOKIE_SECRET: "$(openssl rand -hex 32)" ANTHROPIC_API_KEY: "sk-ant-..." -
Gateway access — the server pod needs to reach the OpenShell gateway's gRPC endpoint. If the gateway runs in-cluster, make sure the NetworkPolicy allows it (the included policy allows all egress on 443 — tighten to taste). If the gateway stores its config/TLS material in a Secret, create
openshell-gateway-configin theomnigentnamespace and the deployment mounts it at~/.config/openshell. -
Install the agent-sandbox CRD (optional) — if the OpenShell gateway delegates to the kubernetes-sigs/agent-sandbox controller:
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/agent-sandbox/main/config/crd/bases/sandbox.agent.k8s.io_agentsandboxes.yamlThe overlay's RBAC already grants the server's ServiceAccount permission to manage
AgentSandboxresources. -
Apply:
kubectl kustomize deploy/kubernetes/overlays/openshell/ | kubectl apply -f -
For OpenShell + in-cluster Postgres, layer the postgres overlay on top (compose both bases in a new kustomization, or apply the postgres StatefulSet separately). See Network egress policy for the sandbox-side egress allow-list (server URL + LLM provider hosts).
Building a UBI image (Red Hat / OpenShift)
For RHEL and OpenShift environments that require UBI-compliant containers, use
the UBI variant of the Dockerfile. It uses Red Hat Universal Base Image 9
(ubi9/python-312, ubi9/nodejs-20) and runs the server as non-root (UID 1001)
by default — compatible with OpenShift's restricted-v2 SCC out of the box.
# from the repo root
docker build -t omnigent-server:ubi -f deploy/docker/Dockerfile.ubi .
Then reference the image in the OpenShift overlay by patching the Deployment or pointing your image stream at it.
Deploy on Red Hat OpenShift
The overlays/openshift/ overlay replaces the Ingress with an OpenShift Route
(edge TLS, managed by the platform) and adds a restricted-v2-compatible
SecurityContext. No ingress controller or cert-manager add-ons needed.
-
Edit the secret in
base/secret.yaml(same as the external-database path above). -
Set your route hostname — replace
omnigent.apps.example.cominoverlays/openshift/route.yamlwith your cluster's apps domain. -
Apply:
kubectl kustomize deploy/kubernetes/overlays/openshift/ | oc apply -f -
For in-cluster Postgres on OpenShift, use overlays/openshift-postgres/
instead — it combines the Postgres StatefulSet, OpenShift Route, and restricted
security contexts:
# edit overlays/openshift-postgres/secret-patch.yaml with real passwords first
kubectl kustomize deploy/kubernetes/overlays/openshift-postgres/ | oc apply -f -
On-demand sandbox runners
The overlays/sandbox-runners/ overlay turns on the kubernetes managed
sandbox provider: a host_type: managed session spawns one runner Pod that runs
omnigent host as its entrypoint and dials back over the launch-token tunnel. It
adds a dedicated runner namespace, a least-privilege server SA (scoped Pod +
Secret rights, no pods/exec), and the sandbox: server config. The
overlay swaps in the official omnigent-server-kubernetes image variant, which
adds the kubernetes client extra the provider imports (the base server image
omits it). See overlays/sandbox-runners/README.md for the full guide.
kubectl apply -k deploy/kubernetes/overlays/sandbox-runners
# then create the omnigent-creds harness Secret (see the overlay README)
Credentials & auth — two separate concerns, don't conflate:
- Server auth. Front the server with
header/oidcauth or run single-user; the built-inaccountsmode refuses the per-session runner dial-back (403), a framework-level limit shared by all sandbox providers — see Auth. - Model keys (
ANTHROPIC_API_KEY/CLAUDE_CODE_OAUTH_TOKEN/OPENAI_API_KEY/GIT_TOKEN/ …) ride theomnigent-credsSecret projected into every runner Pod.
Both are detailed in
overlays/sandbox-runners/README.md.
Verify the deployment
Check the rollout and reach the server without a public domain:
kubectl get pods -n omnigent # omnigent (and, with the overlay, postgres) → Running
kubectl rollout status deploy/omnigent -n omnigent
kubectl logs -n omnigent deploy/omnigent # server logs
# Port-forward the Service and open the app locally:
kubectl port-forward -n omnigent svc/omnigent 8000:80
# → http://localhost:8000 (health check: curl localhost:8000/health → {"status":"ok"})
The first boot runs database migrations before the server starts listening; the pod may restart once if the liveness probe fires during that window (see Resource sizing).
To test the Ingress itself instead of port-forwarding, point its hostname at a
domain that already resolves to localhost — omnigent.localtest.me or
<node-ip>.sslip.io — use the self-signed issuer above, and reach it through the
ingress controller's published port.
Next steps: connect a host
The server is the control plane — agents run on hosts that register with it. A brand-new deployment has none, so connect at least one machine:
omnigent login https://omnigent.example.com # authenticate the CLI
omnigent host --server https://omnigent.example.com # register this machine
The host then appears in the web UI when you start a new chat. See the main README for the full host/auth reference.
Use your own IdP instead (OIDC) — optional
Optional. The default accounts provider (username + password) works out of the
box; use this only to delegate authentication to an external OIDC provider. Add
OIDC env vars to the secret:
kubectl create secret generic omnigent-oidc -n omnigent \
--from-literal=OMNIGENT_AUTH_PROVIDER=oidc \
--from-literal=OMNIGENT_OIDC_ISSUER=https://github.com \
--from-literal=OMNIGENT_OIDC_CLIENT_ID=<client-id> \
--from-literal=OMNIGENT_OIDC_CLIENT_SECRET=<client-secret> \
--from-literal=OMNIGENT_OIDC_REDIRECT_URI=https://omnigent.example.com/auth/callback \
--from-literal=OMNIGENT_OIDC_COOKIE_SECRET=$(openssl rand -hex 32)
Then add envFrom: [{secretRef: {name: omnigent-oidc}}] to the Deployment
container spec (or merge the values into omnigent-secrets).
Resource sizing
The server idles around ~275 MB RSS. The manifests request 512 Mi and limit at
1 Gi — adjust to taste. The first boot against a remote Postgres runs
migrations and takes ~1 minute; bump the liveness initialDelaySeconds to
~90s if you see the pod get killed during the first deploy.
Scaling
The server uses an in-memory runner registry, so only one replica is
supported. Do not increase replicas unless the architecture is changed to
use a shared registry (e.g. Redis).