Files
omnigent-ai--omnigent/deploy/docker/README.md
T
Zeyi (Rice) Fan b9d53f0a96 feat(server): add deployment-wide release feature flags (#4775)
## 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>
2026-08-14 10:57:20 -07:00

13 KiB

Omnigent — docker-compose stack

Run the server as a self-contained Docker stack on any host: your laptop, a VPS, an EC2 instance, a home server, anywhere docker compose runs.

The stack:

  • postgres — persistent DB on a Docker volume
  • omnigent — the server image (built from ../Dockerfile)

Auth is in-process — the server has both header-proxy and native OIDC modes built in (see Multi-user mode below). There is no separate auth-proxy container.

Quickstart (single-user)

cd deploy/docker
./bootstrap.sh                          # mints POSTGRES_PASSWORD + cookie secret into .env
docker compose up -d
docker compose logs -f omnigent       # ctrl-c when boot is clean

bootstrap.sh is idempotent — re-running it leaves already-set secrets alone. If you prefer to manage .env yourself, just cp .env.example .env and edit POSTGRES_PASSWORD (and OMNIGENT_OIDC_COOKIE_SECRET if you're enabling OIDC) by hand.

Server is on http://localhost:8000. The web UI prints the CLI command to launch a local runner against it. From your laptop:

omnigent run path/to/agent.yaml --server http://localhost:8000

Reset everything (drops the DB and the artifact store):

docker compose down -v

Release features

Release features are deployment-wide and off by default. Enable one or more with the comma-separated OMNIGENT_FEATURES variable in .env, then recreate the server container:

OMNIGENT_FEATURES=usage_page
docker compose up -d
curl -s http://localhost:8000/v1/info | jq '.features'

Known keys and their lifecycle are documented in designs/FEATURE_FLAGS.md. Unknown keys fail server startup so a typo cannot silently produce the wrong rollout. To roll back, remove the key (or empty the variable), run docker compose up -d again, and reload the web app.

Multi-user mode (accounts — default)

Built-in accounts auth: no IdP to register, no proxy to host. This is the default — docker compose up -d brings it up with no extra env wiring. No credentials are auto-generated. On first boot, when no admin exists yet and none was pre-seeded, the server creates nothing and prints:

→ No admin yet. Open <base_url> to create the first admin account (choose a username + password).

You then open the web UI's Create admin form (it appears while no admin exists) and pick your own username + password.

For any deploy reachable through a public domain, also set the external URL so the printed link and invite links resolve correctly:

# Add to .env (bootstrap.sh already minted the cookie secret for you):
OMNIGENT_ACCOUNTS_BASE_URL=https://omnigent.example.com

docker compose up -d
docker compose logs omnigent      # shows the "No admin yet" line with your base URL

Once you've created the admin and signed in:

  • Click your username in the top-right → MembersInvite member.
  • Share the single-use URL with the teammate; they pick their own username and password when they redeem it.
  • Sign-out lives in the same account menu.

Headless deploy (CI, Cloud Run, etc.) where you can't reach the Create-admin form? Pre-seed the admin password so first boot creates the admin directly:

OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD=<your-strong-password>

OMNIGENT_ADMIN_CREDENTIALS_PATH (set to /data/admin-credentials in docker-compose.yaml) anchors the persistent state directory on the artifact-data volume — it survives docker compose restart and is deleted by docker compose down -v.

Multi-user mode (OIDC)

Single-user mode trusts everyone who reaches the port and uses the identity "local" for all requests. For a shared deploy, the server has native OIDC support — it handles the full login flow itself (/auth/login, /auth/callback, /auth/logout) with a signed session cookie. No extra container, no Caddy basic-auth shim, no oauth2-proxy.

Walkthrough: GitHub OAuth (easiest to register)

  1. Register the OAuth app. Go to https://github.com/settings/developers → New OAuth App. Set the callback to https://<your-host>/auth/callback (HTTPS is strongly recommended; GitHub permits HTTP for testing but warns).

  2. Mint a cookie secret. ./bootstrap.sh already did this on the quickstart path — OMNIGENT_OIDC_COOKIE_SECRET is set in your .env. If you skipped it, run openssl rand -hex 32 and paste the value yourself.

  3. Edit .env:

    OMNIGENT_AUTH_PROVIDER=oidc
    OMNIGENT_OIDC_ISSUER=https://github.com
    OMNIGENT_OIDC_CLIENT_ID=Iv1.abc123…
    OMNIGENT_OIDC_CLIENT_SECRET=OMNIGENT_OIDC_REDIRECT_URI=https://omnigent.example.com/auth/callback
    # OMNIGENT_OIDC_COOKIE_SECRET is already set by bootstrap.sh — leave it alone.
    
  4. Bring it up.

    docker compose up -d
    

    The server will fail loud at startup if any required OIDC env var is missing — check docker compose logs omnigent if it doesn't come up.

  5. Visit the URL → you should be redirected to GitHub to log in, then back to the web UI with a __Host-ap_session cookie set.

Walkthrough: Google Workspace (with domain allowlist)

OMNIGENT_AUTH_PROVIDER=oidc
OMNIGENT_OIDC_ISSUER=https://accounts.google.com
OMNIGENT_OIDC_CLIENT_ID=…apps.googleusercontent.com
OMNIGENT_OIDC_CLIENT_SECRET=OMNIGENT_OIDC_REDIRECT_URI=https://omnigent.example.com/auth/callback
OMNIGENT_OIDC_COOKIE_SECRET=<64-hex-chars>
OMNIGENT_OIDC_ALLOWED_DOMAINS=example.com,subsidiary.example.com

ALLOWED_DOMAINS is critical when the OAuth consent screen is "External" — without it, any Google account on the planet can log in.

Generic OIDC (Okta, Auth0, Keycloak, Entra ID)

Any IdP that publishes /.well-known/openid-configuration works. Set OMNIGENT_OIDC_ISSUER to the base URL; the server fetches discovery at startup.

OMNIGENT_AUTH_PROVIDER=oidc
OMNIGENT_OIDC_ISSUER=https://your-tenant.okta.com
OMNIGENT_OIDC_CLIENT_ID=OMNIGENT_OIDC_CLIENT_SECRET=OMNIGENT_OIDC_REDIRECT_URI=https://omnigent.example.com/auth/callback
OMNIGENT_OIDC_COOKIE_SECRET=<64-hex-chars>

HTTPS for the callback URL

Most IdPs require HTTPS for non-localhost redirect URIs, and the session cookie uses the __Host- prefix which browsers only accept over HTTPS. Three options:

  1. Use the bundled Caddy overlay (easiest — any VPS / EC2 / home server with a public domain):

    # In .env:
    OMNIGENT_DOMAIN=omnigent.example.com
    OMNIGENT_ACME_EMAIL=you@example.com      # optional, for Let's Encrypt notices
    
    # Point DNS A/AAAA records at the host, then:
    docker compose -f docker-compose.yaml -f docker-compose.https.yaml up -d
    

    Caddy auto-provisions and renews a Let's Encrypt cert; the omnigent container stops being directly exposed and only :80 + :443 are published. Requires Docker Compose 2.24+ for the overlay's !reset directive. See Caddyfile for the (3-line) config.

  2. Behind an existing reverse proxy — point your proxy at omnigent:8000 over the docker network (or 127.0.0.1:8000 from the host). Examples: AWS ALB with ACM cert, Cloudflare in "Full" SSL mode, Fly.io / Cloud Run / Render platform certs.

Header-proxy mode (for deploys behind an existing SSO proxy)

If you already have oauth2-proxy, Databricks Apps, AWS ALB OIDC, Cloudflare Access, Tailscale Funnel, or any other proxy that injects an identity header, set OMNIGENT_AUTH_PROVIDER=header. The server will reject requests without the header.

OMNIGENT_AUTH_PROVIDER=header

The header read is X-Forwarded-Email by default. Proxies that use a different header name set OMNIGENT_AUTH_HEADER to point the server at it — for example, Cloudflare Access supplies the authenticated email in Cf-Access-Authenticated-User-Email:

OMNIGENT_AUTH_PROVIDER=header
OMNIGENT_AUTH_HEADER=Cf-Access-Authenticated-User-Email

Some proxies namespace the value they inject. Google IAP forwards the email in X-Goog-Authenticated-User-Email prefixed with accounts.google.com:; set OMNIGENT_AUTH_HEADER_STRIP_PREFIX to drop it and recover the bare email:

OMNIGENT_AUTH_PROVIDER=header
OMNIGENT_AUTH_HEADER=X-Goog-Authenticated-User-Email
OMNIGENT_AUTH_HEADER_STRIP_PREFIX=accounts.google.com:

Security note: in this mode the proxy is responsible for stripping any inbound copy of the identity header from the client request — otherwise any visitor can spoof an identity. The server trusts whatever value reaches it.

Environment variables

Variable Default Purpose
POSTGRES_PASSWORD required DB password for the bundled Postgres container.
POSTGRES_USER / POSTGRES_DB omnigent DB user + database name.
OMNIGENT_PORT 8000 Host port the server is published on.
OMNIGENT_AUTH_ENABLED 1 (in compose) Master auth switch. 1 → accounts (or oidc if OMNIGENT_OIDC_ISSUER is set); 0 → single-user local mode (every request is the shared local user — local dev only, never shared deploys).
OMNIGENT_AUTH_PROVIDER unset Escape hatch to pin a mode explicitly: header / accounts / oidc. Overrides the AUTH_ENABLED auto-selection.
OMNIGENT_AUTH_HEADER X-Forwarded-Email Header-mode only: name of the trusted identity header. Set for proxies that use another name, e.g. Cf-Access-Authenticated-User-Email (Cloudflare Access).
OMNIGENT_AUTH_HEADER_STRIP_PREFIX unset (strip nothing) Header-mode only: prefix removed from the identity header value. Set to accounts.google.com: for Google IAP's X-Goog-Authenticated-User-Email.
OMNIGENT_OIDC_* unset OIDC config — required in oidc mode (issuer set, or AUTH_PROVIDER=oidc). See .env.example.
PYPI_INDEX_URL https://pypi.org/simple Build-time PyPI index — override only behind a corporate proxy.

DATABASE_URL and ARTIFACT_DIR are computed by compose and injected into the container.

Host image (--target host)

The same Dockerfile publishes a second image: the official Omnigent host image, which remote sandboxes boot from so they start in seconds instead of paying an in-sandbox dependency install. It bakes the full omnigent install (all three packages + deps, python and pip on PATH), git (workspaces / worktrees), tmux (terminal sessions spawned by native harnesses), and the coding-harness CLIs — claude, codex, pi, and kiro-cli, with the runtime they need — so claude-sdk / claude-native / codex / pi / kiro-native agents run in sandboxes without an in-sandbox install. None of the server-only bits are included (no SPA bundle, no psycopg, no uvicorn entrypoint).

CI publishes it next to the server image, with the same tag scheme:

  • ghcr.io/omnigent-ai/omnigent-host:latest — tracks main HEAD (the default for omnigent sandbox create --provider modal)
  • ghcr.io/omnigent-ai/omnigent-host:sha-<short> — immutable per-commit pin
  • ghcr.io/omnigent-ai/omnigent-host:vX.Y.Z — release tags

Build it locally from the repo root:

docker build -t omnigent-host:latest --target host \
             -f deploy/docker/Dockerfile .

Using it with the Modal sandbox provider

omnigent sandbox create --provider modal boots sandboxes from ghcr.io/omnigent-ai/omnigent-host:latest by default. Your local checkout's wheels are still built and overlaid on top at create time (pip install --force-reinstall --no-deps), so the sandbox runs exactly your code — the baked image just supplies the dependency tree. A checkout that adds a brand-new dependency needs that package installed manually in the sandbox until the official image rebuilds with it.

Two environment variables tune the pull:

Variable Purpose
OMNIGENT_MODAL_HOST_IMAGE Override the image ref, e.g. an org-internal copy (ghcr.io/<your-org>/omnigent-host:latest) or a :sha-<short> pin.
OMNIGENT_MODAL_REGISTRY_SECRET Name of a Modal secret holding registry credentials for private pulls. Create it with keys REGISTRY_USERNAME (your registry username) and REGISTRY_PASSWORD (for GHCR: a personal access token with read:packages). Unset = anonymous pull.

Using it with the Daytona sandbox provider

The same host image backs Daytona-managed sessions (server config sandbox.provider: daytona; Daytona is managed-only — there is no omnigent sandbox create --provider daytona CLI flow). Daytona ingests the registry image into an internal snapshot on first use (the first launch from a given image takes minutes; later launches reuse the snapshot and take seconds). Override the ref with OMNIGENT_DAYTONA_HOST_IMAGE or the server config's sandbox.daytona.image. See deploy/daytona/README.md for the full provider guide (credentials, the free-tier egress relay, and security considerations).

  • designs/OIDC_AUTH.md — full native OIDC design
  • designs/SESSIONS_AUTH.mdAuthProvider contract + permission system