# Deploying Omnigent Omnigent ships several ways to deploy the server, organized by target platform. Pick the one that matches your environment. Deploying buys you a stable URL: sessions become reachable from any device, including your phone (the web UI is built for mobile), and teammates can join. The server is the coordination point; your code and model keys stay on the machines that register as hosts (see [Execution model](#execution-model)). ## Deploy in one click No local tooling needed. Pick a platform, click the button, and your Omnigent server is live with HTTPS in a few minutes. | Platform | Button | Docs | |---|---|---| | **Render** | [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/omnigent-ai/omnigent) | [`render/README.md`](render/README.md) | | **Railway** | *(button pending; see below)* | [`railway/README.md`](railway/README.md) | Both provision a managed Postgres database automatically and default to the built-in `accounts` auth provider, so a fresh deploy is multi-user with no external IdP. First boot auto-creates an admin (password in the service logs); invite teammates from the web UI. Prefer your own IdP? Switch to OIDC after deploy by setting the `OMNIGENT_OIDC_*` vars (auth stays enabled; the issuer is what flips the mode); see the platform README for both walkthroughs. **Three more platforms** are supported with a little more setup (not a single button): **Fly.io** (`fly deploy`, or its web-UI Launch), **Hugging Face Spaces** (a demo-grade Docker Space), and **Modal** (`modal deploy`, an always-on web server with a durable artifact Volume). See the menu below. Fly and HF Spaces can run on the **SQLite lite tier** with no database to provision (see [Database: Postgres or SQLite](#database-postgres-or-sqlite)); Modal needs a bring-your-own Postgres. --- ``` deploy/ ├── README.md ← (this file) the menu │ ├── render/ ← Render 1-click deploy │ └── README.md │ ├── railway/ ← Railway 1-click deploy │ └── README.md │ ├── fly/ ← Fly.io (CLI `fly deploy`, or web-UI Launch) │ ├── fly.toml │ └── README.md │ ├── hf-spaces/ ← Hugging Face Spaces (demo-grade Docker Space) │ ├── Dockerfile │ └── README.md │ ├── modal/ ← Modal (`modal deploy`, always-on, durable Volume) │ ├── modal_app.py │ └── README.md │ ├── cloudflare/ ← Cloudflare Containers + D1 + R2 (serverless, scale-to-zero) │ ├── Dockerfile server image + D1 dialect │ ├── src/index.js the Worker that fronts the container │ ├── wrangler.jsonc │ └── README.md │ ├── trycloudflare/ ← Cloudflare quick tunnel (public URL for a LOCAL server) │ └── README.md │ ├── tailscale/ ← Tailscale (private access from phone/tablet/laptop │ └── README.md via tailnet; Funnel for cloud sandbox dial-back) │ ├── daytona/ ← Daytona sandbox-provider guide + the Cloudflare │ ├── wrangler.toml Worker egress relay for its free tier; NOT a │ ├── src/index.js server deploy target. See its README.md. │ └── README.md │ ├── blaxel/ ← Blaxel sandbox-provider guide + managed-host config; │ └── README.md NOT a server deploy target. │ ├── islo/ ← Islo sandbox-provider guide (gateway credential │ └── README.md injection); NOT a server deploy target. │ ├── e2b/ ← E2B sandbox-provider guide (boots from a pre-built │ └── README.md E2B template); NOT a server deploy target. │ ├── gensee/ ← Gensee managed sandbox-provider guide (ephemeral │ └── README.md provider-managed runtimes); NOT a server deploy target. │ ├── openshell/ ← NVIDIA OpenShell sandbox-provider guide (self-hosted │ └── README.md gRPC gateway, on-prem/air-gapped); NOT a server target. │ ├── microsandbox/ ← microsandbox sandbox-provider guide (local libkrun │ └── README.md microVMs, embedded SDK, no daemon); NOT a server target. │ ├── databricks/ ← Databricks Apps (Lakebase + UC Volumes) │ ├── databricks.yml bundle declarative config │ ├── deploy.py build + `bundle deploy`/`run` orchestrator │ ├── src/app.py app entrypoint (Lakebase + UC Volumes) │ └── README.md │ └── docker/ ← common Docker image + compose stack ├── Dockerfile multi-stage slim image (node web build → python builder → runtime) ├── docker-compose.yaml omnigent + postgres for any Docker host ├── entrypoint.py ├── .env.example ├── README.md └── SKILL.md ``` ## Pick your target | If you want to … | Use | Where to look | |---|---|---| | **Deploy from a browser (no local tools)** | **Render or Railway** | Buttons above: [Render](render/README.md) · [Railway](railway/README.md) | | Try the server on your laptop | Docker compose | [`docker/README.md`](docker/README.md): `./bootstrap.sh` to mint the `.env` secrets, then `docker compose up -d` | | Run on any host you already have (VPS, home server, on-prem) | Docker compose | [`docker/README.md`](docker/README.md): copy the compose stack, `./bootstrap.sh`, then `docker compose up -d` | | Deploy to Fly.io | Fly | [`fly/README.md`](fly/README.md): `fly deploy`, SQLite on a volume | | Deploy to Modal (durable artifact Volume) | Modal | [`modal/README.md`](modal/README.md): `modal deploy`, BYO Neon Postgres | | Deploy serverless (scale-to-zero, no VM/Postgres to manage) | Cloudflare Containers + D1 + R2 | [`cloudflare/README.md`](cloudflare/README.md): `wrangler deploy` | | Stand up a quick demo (no DB to provision) | HF Spaces | [`hf-spaces/README.md`](hf-spaces/README.md): Docker Space, SQLite | | Share a server running on your **laptop**: demo it to teammates, or let remote runners & cloud sandboxes connect back to it (nothing to deploy) | Cloudflare quick tunnel | `cloudflared tunnel --url http://localhost:6767` | | Access your server privately from **your phone, tablet, or other personal devices** without exposing it to the internet | Tailscale | [`tailscale/README.md`](tailscale/README.md): `tailscale serve https / http://localhost:8000` | | Cloud Run / Kubernetes / other | Docker image | [`docker/README.md`](docker/README.md), then point your platform at the image | | Deploy on a Databricks workspace (Lakebase + UC Volumes), self-managed | Databricks Apps | [`databricks/README.md`](databricks/README.md): uses Asset Bundles | > **On Databricks?** The fully managed > [Omnigent on Databricks](https://docs.databricks.com/aws/en/omnigent/) > (Beta) is the recommended path: Databricks operates the server for > you, wired to workspace identity, Foundation Models, Unity Gateway, and > MLflow Tracing. Enable the **Omnigent** preview in your workspace > settings. The self-managed Databricks Apps bundle above is for when > you need control the managed service does not expose yet. All non-Databricks deploy paths share the same image (`docker/Dockerfile`): a slim Python container running the FastAPI / WebSocket coordinator, with Postgres or SQLite as the datastore. The Databricks Apps path uses a separate entrypoint (`databricks/src/app.py`) that swaps Postgres for Lakebase (managed PostgreSQL) and the artifact store for UC Volumes. ## Database: Postgres or SQLite The server supports two database backends, both first-class (same schema, same migrations; pick per `DATABASE_URL`): - **Postgres**: the default and the production answer. Required for more than one server instance. **Managed and auto-provisioned on deploy** on Render and Railway. On platforms without a managed database (HF Spaces, Modal, or Fly if you want Postgres over volume-SQLite), bring your own. The quickest is **Neon**: create one at [pg.new](https://pg.new) and set the connection string as `DATABASE_URL`. Any `postgres://` / `postgresql://` URL works (pooled or direct); the entrypoint normalizes it to the psycopg3 dialect automatically. - **SQLite**: a zero-dependency "lite tier" for demos and single-instance deploys, with no database to provision. The `.db` file lives on the platform's persistent disk/volume (Render disk, Fly volume, Railway volume) and survives restarts there; on Hugging Face free Spaces the disk is ephemeral, so SQLite data resets on restart, and on Modal the Volume's eventual-consistency semantics don't suit a live `.db` file, so skip the SQLite tier there. Set `DATABASE_URL=sqlite:////data/artifacts/chat.db`. Tradeoff: single instance only, no managed backups. **Who provisions the database.** Render and Railway create the Postgres *as part of the deploy* (one step; it's owned by your platform account). Platforms without a managed DB don't: there you either run on SQLite (zero setup, ephemeral on HF) or bring an owned Postgres like Neon (a one-time signup, then persistent). A deploy can't auto-provision a *persistent* database for you; persistence requires an owned account, and that's the one step that can't be automated away. **First boot against a remote Postgres is slow.** Migrations run over the network on the first boot (~1 minute on Neon, vs near-instant for local SQLite); subsequent boots are fast. Make sure the platform's healthcheck grace tolerates it: Render and Railway do by default; on Fly, raise `grace_period` if you use a remote DB. **Memory floor:** the server's working set is ~512 MB–1 GB. Render Starter (512 MB), Railway (usage-scaled), and HF Spaces clear it automatically; Fly's 256 MB default does not, so the Fly config pins a 1 GB machine, and the Modal app pins `memory=1024` for the same reason. ## Serving: put an HTTP/2 proxy in front for many concurrent views Each open session in the web UI holds a long-lived streaming HTTP response (`GET /v1/sessions/{id}/stream`, `text/event-stream`) for as long as the view is on screen. Over **HTTP/1.1 browsers cap concurrent connections at ~6 per origin**, and every open stream occupies one of those slots. Open a handful of windows or tabs against the same server and the pool fills with held-open streams — then every *other* request the UI makes (sending a message, the session list, `/health`, auth) queues behind the cap and never fires. The symptom is the whole UI appearing to freeze across all windows while the server itself is idle; in DevTools → Network the stuck requests sit in **Stalled/Queued**, not "waiting for server". **Fix: serve over HTTP/2.** HTTP/2 multiplexes every stream over one connection, so the per-origin cap stops mattering. `uvicorn` (the server's ASGI server) speaks HTTP/1.1 only, so HTTP/2 comes from a reverse proxy that terminates TLS in front of it — which most real deploys already have: - **The bundled Caddy overlay** (`docker/docker-compose.https.yaml`) gives you this for free — Caddy negotiates HTTP/2 (and HTTP/3) over TLS via ALPN with no extra config. See [`docker/README.md`](docker/README.md#multi-user-mode-oidc). - **The one-click platforms** (Render, Railway, Fly, Cloudflare) and managed **Databricks** terminate TLS with HTTP/2 at their edge, so they're already covered. The gap is only when you expose the raw `:8000` HTTP/1.1 port directly to browsers (e.g. `docker compose up` with no proxy, reached over plain HTTP). That's fine for a single window; put a proxy in front once you or your team routinely open several. ## Execution model Omnigent runs in two pieces that talk to each other over a WebSocket tunnel: - **Server**: the FastAPI app you deploy here. Handles HTTP / SSE routes, terminal-attach WebSockets, persistence, web UI. - **Runner (host)**: a Python subprocess that runs on the **user's machine** (laptop, dev container, etc.). Dials in to the server via `WS /v1/runner/tunnel`, executes the LLM loop + tools locally, streams events back. The deploy options here are all about the server. Runners aren't deployed; every user launches one on their own machine with `omnigent run … --server ` or `omnigent claude --server `. This separation is why the server image is small (no `tmux`, no harness SDKs, no LLM API keys in the image) and why no agent code runs inside it. ## Connect your laptop Once the server is up, sign in from your machine. The token is reused by `run`, `attach`, and `host`: ```bash omnigent login https://your-host ``` `login` detects the server's auth mode automatically. Built-in accounts, OIDC, header-auth proxies, and Databricks-hosted servers (a Databricks App or a workspace API path) all work with the same command; for Databricks it runs `databricks auth login` against the right workspace for you (requires the `databricks` extra). ### OIDC login client upgrades OIDC ticket login requires PKCE and explicit browser approval. This is a breaking change for older CLIs, mobile apps, Slack integrations, and custom clients: their next login cannot complete until they are updated. Existing sessions and refresh grants keep their normal expiry and revocation behavior. Browser-only login and the other authentication modes are unchanged. An old client's empty login request opens an **Update Omnigent to sign in** page. CLI users should run `omni upgrade` and retry their login; mobile users should update the app through their normal distribution channel; Slack users need their administrator to update the integration. The terminal or Slack message may still say the login was rejected because installed old clients do not display the server's detailed error text. Update the centrally managed Slack bot before the server: its old error modal can replace the upgrade link before the user opens it. The server returns HTTP 200 only to let these clients open the instructions. Its `ticket` field is a fixed, non-redeemable notice marker: no login ticket or credentials are created, and polling returns terminal HTTP 410. Requests containing invalid PKCE data still return HTTP 400. There is no legacy login fallback. Before deploying the server, make client builds containing the PKCE update available through your CLI, mobile, and Slack distribution channels, and announce that older clients must upgrade to sign in again. Test both an old and an updated client against a staging server with your actual identity provider. The old client must show the upgrade page without signing in; the updated client must complete fresh sign-in and browser approval. Preserve PKCE and consent enforcement if rolling back an unrelated deployment issue. Updated clients send proof in `X-Omnigent-Code-Verifier`; proxies must forward this header without logging it. Query-based proof remains supported for API compatibility, so do not retain `/auth/cli-poll` query strings either: a ticket and verifier together can redeem an approved login. Approval requires a browser session authenticated for that specific ticket; ordinary browser sessions, runner tokens, and refresh-issued tokens cannot approve it. Existing sessions remain valid elsewhere. Generic OIDC ticket flows require fresh IdP authentication and a signed ID token with `auth_time` at or after the reauthentication request. Verify that your IdP honors `prompt=login` and `max_age=0` and supplies `auth_time`; missing or stale claims fail closed. GitHub OAuth may reuse its provider session, but still requires ticket-specific browser sign-in and approval. For an IdP that cannot supply a fresh `auth_time` (for example, Cloudflare Access), set `OMNIGENT_OIDC_REQUIRE_REAUTH=0`. The server still sends `prompt=login` and requires PKCE, ticket-specific sign-in, and approval, but it no longer verifies that the IdP asked for credentials again. Polling responses forbid caching, including successful credential responses. CLI and Slack display a comparison code; native mobile apps currently rely on explicit browser approval without displaying that code in the app. Include the actual native apps in staging checks before publishing a mobile build. ### Register the host Then register the machine as a host, so sessions created in the web UI can run on it: ```bash omnigent host https://your-host ``` Or point a one-off run at the server directly: ```bash omnigent run path/to/agent.yaml --server https://your-host ``` ## Run hosts in cloud sandboxes Don't want a laptop to be the host? Run the host in a cloud sandbox instead. **From the CLI (Modal, Daytona, Blaxel, Islo, or E2B).** Install the provider extra when needed (`pip install 'omnigent[modal]'`, `'omnigent[daytona]'`, `'omnigent[blaxel]'`, or `'omnigent[e2b]'`; Islo uses the built-in HTTP client). Authenticate with `modal token new`, `DAYTONA_API_KEY`, Blaxel's `BL_WORKSPACE` and `BL_API_KEY`, `ISLO_API_KEY`, or `E2B_API_KEY`. Then run: ```bash omnigent sandbox create --provider modal # or --provider daytona / blaxel / islo / e2b omnigent sandbox connect --provider modal --sandbox-id --server https://your-host ``` > [!NOTE] > Modal caps sandbox lifetime at 24 hours. Re-run `create` + `connect` to > roll the host onto a fresh sandbox. Daytona and Islo have no Omnigent-imposed > lifetime cap; Daytona free-tier orgs restrict egress to an allowlist; see > [`daytona/README.md`](daytona/README.md) for the relay workaround. E2B > shares Modal's 24-hour cap **and** boots from a pre-built E2B *template* > rather than a registry image — build it once first; see > [`e2b/README.md`](e2b/README.md). **Server-managed (Modal, Daytona, Blaxel, Islo, E2B, or Gensee).** With *managed hosts*, creating a session with `"host_type": "managed"` (e.g. `POST /v1/sessions {"agent_id": ..., "host_type": "managed"}`) makes the server provision a sandbox, start a host in it, and run the session there. No laptop, no CLI steps per session; the sandbox is terminated when the session is deleted. Configuration is a `sandbox:` section in the server config (`omnigent server -c config.yaml`, or `/config.yaml`): ```yaml sandbox: provider: modal server_url: https://your-host # public URL sandboxes dial back to reaper: # optional, disabled by default enabled: true terminate_after_offline_days: 30 # default: 30 days sweep_interval_s: 86400 # default: 1 day ``` Modal credentials come from the server's environment (`MODAL_TOKEN_ID` / `MODAL_TOKEN_SECRET`, or a mounted `~/.modal.toml`), not the config file. Daytona reads `DAYTONA_API_KEY`. Blaxel reads `BL_WORKSPACE` and `BL_API_KEY`. Islo reads `ISLO_API_KEY` and optional `ISLO_BASE_URL`. E2B reads `E2B_API_KEY`. Gensee reads `GENSEE_CONTROLLER_API_TOKEN`; see [`gensee/README.md`](gensee/README.md) for its managed-only configuration. All credentials come from the server environment. Each sandbox authenticates back with a server-minted, per-launch token, so no user credentials ever enter the sandbox. The optional reaper runs once for the whole `sandbox:` deployment, including multi-provider configurations. It queries managed host rows directly and detaches a sandbox generation only after its host has stayed offline past `terminate_after_offline_days`. Provider termination failures remain recorded for a later sweep, while the session history and durable host binding stay available for a fresh sandbox generation. Both `terminate_after_offline_days` and `sweep_interval_s` must be positive integers. **The host image.** Most sandboxes boot from the official prebaked host image (`ghcr.io/omnigent-ai/omnigent-host:latest`, published by CI from the `host` target of [`docker/Dockerfile`](docker/Dockerfile)), so the host starts in seconds instead of installing Omnigent at boot. The image ships the coding-harness CLIs (`claude`, `codex`, `pi`, `kiro-cli`). Blaxel uses `blaxel/omnigent-host:latest`, which combines this host runtime with Blaxel's required `sandbox-api`. E2B uses its provider template. Gensee uses a provider-managed runtime rather than this registry-image setting. To use a custom image with a provider that accepts one, build the same `host` target and point the provider config at it: ```bash docker build -f docker/Dockerfile --target host \ -t docker.io//omnigent-host:latest . docker push docker.io//omnigent-host:latest ``` ```yaml sandbox: provider: modal server_url: https://your-host modal: image: docker.io//omnigent-host:latest ``` For private registries, set `OMNIGENT_MODAL_REGISTRY_SECRET` on the server to the name of a Modal secret holding `REGISTRY_USERNAME` and `REGISTRY_PASSWORD`. For CLI-launched sandboxes, `OMNIGENT_MODAL_HOST_IMAGE`, `OMNIGENT_DAYTONA_HOST_IMAGE`, `OMNIGENT_BLAXEL_HOST_IMAGE`, or `OMNIGENT_ISLO_HOST_IMAGE` overrides the image. **LLM credentials for managed sessions.** A fresh sandbox has no API keys. Park your provider credentials in a [Modal secret](https://modal.com/secrets) and list it in the config. Its env vars are injected into every managed sandbox, and the in-sandbox host forwards the standard harness credential vars (`ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_BASE_URL`, `CLAUDE_CODE_OAUTH_TOKEN`, `CODEX_ACCESS_TOKEN`, `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `GEMINI_API_KEY`, plus their `OMNIGENT_`-prefixed aliases) to its runners: ```bash modal secret create omnigent-llm \ OMNIGENT_ANTHROPIC_API_KEY=sk-ant-… OPENAI_API_KEY=sk-… ``` Prefer `OMNIGENT_ANTHROPIC_API_KEY` for Claude Code API-key auth. Omnigent resolves it into Claude Code's `apiKeyHelper`, avoiding a raw `ANTHROPIC_API_KEY` in the Claude CLI process. ```yaml sandbox: provider: modal server_url: https://your-host modal: secrets: [omnigent-llm] ``` For Daytona, Blaxel, and Islo, list server environment variable names under `sandbox.daytona.env`, `sandbox.blaxel.env`, or `sandbox.islo.env`. The launcher copies the current server values into each sandbox: ```yaml sandbox: provider: islo server_url: https://your-host islo: env: [OPENAI_API_KEY, GIT_TOKEN] ``` Using a **Claude subscription** instead of an API key? Run `claude setup-token` on your own machine and store the resulting long-lived token as `CLAUDE_CODE_OAUTH_TOKEN` in the secret. A **ChatGPT Business/Enterprise plan** works the same way via a [Codex access token](https://developers.openai.com/codex/enterprise/access-tokens) stored as `CODEX_ACCESS_TOKEN`. For gateway setups or other env vars beyond the standard set, add `OMNIGENT_RUNNER_ENV_PASSTHROUGH=NAME1,NAME2` to the secret to name the extra vars the host should forward to runners. **Private repositories.** Managed sessions can clone a repository as the session workspace; for private ones, store an HTTPS token as `GIT_TOKEN` in a Modal secret (GitLab: add `GIT_USERNAME=oauth2`). The host image's git credential helper picks it up for the clone and for the agent's later fetch/push. See the [`modal`](modal/README.md), [`daytona`](daytona/README.md), [`blaxel`](blaxel/README.md), and [`islo`](islo/README.md) guides for provider setup and troubleshooting. ## Auth Auth is driven by a single switch, `OMNIGENT_AUTH_ENABLED`. The framework default (a bare local `omnigent server`) leaves it off: single-user `header` mode, no login. The containerized deploys here (Docker / HF / Render / Railway / Modal / Fly) set `OMNIGENT_AUTH_ENABLED=1` by default in their entrypoints, since a network-exposed instance should be authenticated. With the switch on, the mode is chosen by your config: supply the `OMNIGENT_OIDC_*` vars and you get `oidc`, otherwise you get the built-in `accounts` flow. `OMNIGENT_AUTH_PROVIDER` is an explicit escape hatch that pins the mode and overrides this auto-selection. | Mode | When to use | What's needed | |---|---|---| | `accounts` (deploy default) | Standalone deploy, no external IdP: built-in username/password with first-user-is-admin bootstrap and UI-based invites. Opt in with `OMNIGENT_AUTH_ENABLED=1` (and no OIDC vars). | Set `OMNIGENT_ACCOUNTS_COOKIE_SECRET` (or let `bootstrap.sh` mint it) and `OMNIGENT_ACCOUNTS_BASE_URL` (public URL). On first boot, set the admin password via the web Create-admin form, the terminal prompt, or `--admin-password` / `OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD`. | | `oidc` | Standalone deploy with your own IdP: server handles the full login flow | Set `OMNIGENT_AUTH_ENABLED=1` and the `OMNIGENT_OIDC_*` env vars; the presence of `OMNIGENT_OIDC_ISSUER` selects OIDC (or pin `OMNIGENT_AUTH_PROVIDER=oidc`). Requires HTTPS (the session cookie uses the `__Host-` prefix). | | `header` | Behind an existing SSO proxy (oauth2-proxy, AWS ALB OIDC, Cloudflare Access, Tailscale Funnel, …) that injects an identity header | The default when `OMNIGENT_AUTH_ENABLED` is off; or pin `OMNIGENT_AUTH_PROVIDER=header`. Reads `X-Forwarded-Email` by default; set `OMNIGENT_AUTH_HEADER` for proxies that use another name (e.g. `Cf-Access-Authenticated-User-Email`), and `OMNIGENT_AUTH_HEADER_STRIP_PREFIX=accounts.google.com:` for Google IAP. Proxy MUST strip any inbound copy of the header from clients. Missing headers are always rejected. | > [!NOTE] > **Managed sandboxes need `header`/`oidc` or single-user auth.** Each session's > runner dials back with the *user's* identity, which the built-in `accounts` mode > (the deploy default above) can't supply over the runner WebSocket — it returns > `403` even though the host connects. Framework-level; applies to every sandbox > provider (Modal / Daytona / Islo / Kubernetes / …). ### Ending accounts sessions An `accounts` session lasts `OMNIGENT_ACCOUNTS_SESSION_TTL_HOURS` (default 8). To end one user's sessions sooner, change the password (Account menu → Change password) or have an admin reset it (Members → Reset). Either one invalidates the browser sessions, CLI logins, refresh tokens, and unused sign-in links issued to that user so far. A password change keeps the browser that made it signed in. The user's scheduled tasks and host registrations are kept, but that does not mean they keep running. A host that is already connected keeps its connection, and still looks online, but launches on it fail, including scheduled runs, until it reconnects: - A host signed in with the user's credentials (`omnigent host` after `omnigent login`) cannot reconnect with them. Run `omnigent login`, then restart the host. - A managed host, which the server starts in a sandbox, connects with its own launch token rather than the user's credentials. It needs no login: once it reconnects with that token, before the token expires, launches work again. CLIs need `omnigent login` too. Servers running a release without this behaviour keep the user's sessions when they change or reset a password. During a rolling upgrade, repeat a change made while such instances were still serving. Rotating `OMNIGENT_ACCOUNTS_COOKIE_SECRET` signs out every user at once: it invalidates all session tokens, CLI logins, and refresh tokens. Unused sign-in and invite links are looked up by value, not signed with the secret, so they keep working until they expire (sign-in links after `OMNIGENT_ACCOUNTS_MAGIC_TTL_MINUTES`, default 10; invites after `OMNIGENT_ACCOUNTS_INVITE_TTL_HOURS`, default 72). ### Browser origin allowlist Independent of the auth mode above, the server also checks the browser `Origin` header on WebSocket handshakes and file-upload POSTs — the only signal that tells the user's own web UI apart from some other page open in the same browser. Set `OMNIGENT_WS_ALLOWED_ORIGINS` (comma-separated) to trust origins beyond the server's own host, e.g. when the web UI is reached through a proxy, tunnel, or a domain the server doesn't know about itself. An entry's host may start with `*.` to trust every subdomain of a domain in one entry, instead of listing each hostname individually — useful when many or rotating subdomains all serve the same deploy (per-workspace subdomains, preview environments, a tunnel provider's generated hostnames, …): ```dotenv OMNIGENT_WS_ALLOWED_ORIGINS=https://*.example.com ``` This matches `https://foo.example.com` and `https://a.b.example.com` (any subdomain depth), but not the bare `https://example.com` itself. See [`tailscale/README.md`](tailscale/README.md) for a worked example using Tailscale's per-device hostnames. ### Single sign-on (OIDC) The built-in `accounts` flow needs no setup beyond the deploy itself. To let your team sign in with the accounts they already have (Google, GitHub, Okta, Microsoft), point the server at your identity provider. In `docker/.env` (or your platform's env settings): ```dotenv # Auth is already on (OMNIGENT_AUTH_ENABLED=1) by default in the deploys here. # Adding an OIDC issuer flips the mode to single sign-on. No extra flag. OMNIGENT_OIDC_ISSUER=https://accounts.google.com # or https://github.com / your Okta / Entra URL OMNIGENT_DOMAIN=agents.yourcompany.com # your server's domain OMNIGENT_OIDC_CLIENT_ID=… OMNIGENT_OIDC_CLIENT_SECRET=… ``` ```bash docker compose up -d # restart to apply ``` Your team signs in with their existing accounts, and there are no passwords for you to manage. Nothing else about the app changes. Generic OIDC also supports public PKCE clients without a client secret, PS256-signed identity tokens, and operator-configured authorization/token/JWKS endpoints. See [public clients and explicit endpoints](docker/README.md#public-pkce-clients-and-explicit-endpoints) for configuration and security considerations. Existing client-secret login remains the default. > [!TIP] > The only outside step is creating an app with your provider (e.g. Google > Cloud Console, or GitHub → Settings → Developer settings) to get the client > ID and secret. Set its **callback URL** to `https:///auth/callback`. **Decide who's allowed in**, in your server config (`/data/config.yaml`): ```yaml allowed_domains: [yourcompany.com] # only your company's emails can sign in admins: [you@yourcompany.com] # who can manage members ``` > [!TIP] > Need to let in one outsider, say a contractor on a personal account? Set > `OMNIGENT_OIDC_ALLOW_INVITES=1` and send them a one-time invite link, > instead of opening up the whole allowlist. **Already have a team on built-in accounts?** One command brings everyone across when you switch, so they keep their sessions and admin rights: ```bash omnigent debug migrate-accounts-to-oidc --domain yourcompany.com ``` For the provider-specific walkthroughs (GitHub OAuth, Google Workspace, generic OIDC), see [`docker/README.md#multi-user-mode-oidc`](docker/README.md#multi-user-mode-oidc). ### Header mode (X-Forwarded-Email) > [!WARNING] > Don't deploy a shared server in header-auth mode unless you run a trusted > reverse proxy. `header` mode (`OMNIGENT_AUTH_PROVIDER=header`) takes the caller's identity from a trusted request header — `X-Forwarded-Email` by default. It exists for deployments that sit behind an SSO proxy (oauth2-proxy, Cloudflare Access, an ALB/OIDC listener, Databricks Apps) that authenticates the user and injects that header on every request. Proxies that authenticate with a different header name set `OMNIGENT_AUTH_HEADER` to that name instead of standing up an extra hop to rename it. For example, behind **Cloudflare Access** (which provides the authenticated email in `Cf-Access-Authenticated-User-Email`): ```dotenv OMNIGENT_AUTH_PROVIDER=header OMNIGENT_AUTH_HEADER=Cf-Access-Authenticated-User-Email ``` Some proxies namespace the identity they inject. **Google IAP** forwards the email in `X-Goog-Authenticated-User-Email` prefixed with `accounts.google.com:` (e.g. `accounts.google.com:user@example.com`). Set `OMNIGENT_AUTH_HEADER_STRIP_PREFIX` to drop that prefix and recover the bare email: ```dotenv OMNIGENT_AUTH_PROVIDER=header OMNIGENT_AUTH_HEADER=X-Goog-Authenticated-User-Email OMNIGENT_AUTH_HEADER_STRIP_PREFIX=accounts.google.com: ``` In header mode **the server trusts whatever that header says**. If no proxy sets it, requests are rejected (`401`) rather than silently sharing one identity. But a *misconfigured* proxy is still dangerous: if the proxy doesn't **strip** any client-supplied copy of the identity header before forwarding, anyone can impersonate anyone by sending the header themselves. Getting this wrong exposes every user's sessions, conversation history, tool output, and files to every other caller. **For almost everyone, use built-in `accounts` (the default in these deploys) or `oidc`**; both authenticate users at the server with no proxy to get right. Only choose `header` when you already operate a proxy you trust to set and sanitize the identity header, and read [`docker/README.md#header-proxy-mode-for-deploys-behind-an-existing-sso-proxy`](docker/README.md#header-proxy-mode-for-deploys-behind-an-existing-sso-proxy) first. ## Branding (white-labeling) Customize the app name, landing heading, and logos with a `branding:` block in the server config (`omnigent server -c config.yaml`, or `/config.yaml` — `/data/config.yaml` in the Docker stack). Takes effect on the next server start. ```yaml branding: app_name: "Acme Agent" # tab title, sidebar wordmark, login screen server_name: "Acme Engineering" # this server's name in the desktop app's server list heading: "How can I help?" # landing hero; "" hides it, omit to keep the default logo: # a bare string sets `main`; or per-variant: main: logo.png # branding-assets/logo.png loading: loading.webp # working indicator (falls back to main) favicon: favicon.png # browser-tab icon powered_by: true # "Powered by Omnigent" credit; false to hide ``` Logo files must live under a dedicated `branding-assets/` directory beside the config file (for example, `/data/branding-assets/logo.png`). PNG, JPEG, GIF, WebP, and ICO files up to 5 MiB are accepted only after full decoder validation. ICO files must contain only PNG-backed entries; every directory entry is bounded and decoded independently, while DIB/BMP-backed entries are rejected. Malformed, truncated, oversized, overlapping, trailing-payload, SVG, symlinked, escaped, and non-image files are ignored. Images are also bounded to 4096 pixels per side, 128 frames, 16 megapixels per frame, and 64 megapixels across all decoded frames. The values are served over the unauthenticated `GET /v1/info` and `GET /v1/branding/logo/` endpoints so the login screen is branded before sign-in. Any unset field keeps its built-in default, so a partial block is fine. The small "Powered by Omnigent" credit under the landing composer appears only once you set custom branding; `powered_by: false` hides it even then. It always shows the Omnigent mascot, never your logo. `server_name` names this particular server, so people who connect the desktop app to several servers can tell them apart. It is published in the unauthenticated `GET /.well-known/omnigent.json` manifest, not in `/v1/info`. Desktop builds that read it always show the host next to it, and an MDM-provided name (`omnigentServerName`) takes precedence. When unset, the desktop shows the host; it does not fall back to `app_name`. ## Adding a new deploy target Drop a new subdirectory under `deploy//` with a `README.md` and `SKILL.md`. If the new target uses the existing Docker image, your work is mostly platform-specific glue (a `fly.toml`, a Cloud Run service.yaml, a Helm chart, an HF Spaces config) plus a README that explains how to point that platform at `docker/Dockerfile`. Update this top-level README with a row in the table above.