# Omnigent on Modal [Modal](https://modal.com) plays two distinct roles for Omnigent: 1. **[Server deploy target](#deploying-the-server)** — run the Omnigent server itself on Modal as a single always-on web server (`modal_app.py` in this directory). 2. **[Sandbox provider](#sandboxes-for-runner-hosts)** — disposable cloud machines for running Omnigent *hosts*, so sessions execute in the cloud instead of on your laptop. The two are independent: you can deploy the server anywhere and still use Modal sandboxes for hosts, or vice versa. ## Deploying the server Run the Omnigent server on Modal as a single always-on web server. `modal_app.py` pulls the standard server image and launches the same Docker entrypoint every other platform uses; Modal provides the HTTPS URL, log streaming, and a persistent Volume for the artifact store — uploaded agent bundles survive restarts and redeploys here, unlike on Heroku or Cloudflare. ### Prerequisites - A Modal account and the CLI: `pip install modal && modal setup`. No Docker needed locally — Modal's builders pull the image. - A Postgres database. Modal has no managed Postgres — the fastest is **Neon**: create one at [pg.new](https://pg.new) and copy the connection string. ### Deploy ```bash # 1. One secret bundle with the three required values. The app URL is # deterministic: https://--omnigent-server.modal.run # (your workspace name is shown by `modal profile current`). modal secret create omnigent-deploy \ DATABASE_URL='postgres://…neon.tech/…' \ OMNIGENT_ACCOUNTS_COOKIE_SECRET="$(openssl rand -hex 32)" \ OMNIGENT_ACCOUNTS_BASE_URL='https://--omnigent-server.modal.run' # 2. Ship it. modal deploy deploy/modal/modal_app.py ``` `modal deploy` prints the live URL — if it differs from what you guessed in step 1 (e.g. a non-default Modal environment adds a suffix), update the secret and redeploy. The first boot runs DB migrations over the network (~1 minute on Neon). **Get the admin password:** the first boot prints it to the app log: ```bash modal app logs omnigent ``` ``` ✓ Created initial admin account (accounts auth provider). password: ``` Log in as the admin and invite teammates from **Members** in the web UI. > To set a known admin password instead, add > `OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD=` to the > `omnigent-deploy` secret before the first deploy. ### Modal-specific caveats - **2 MiB WebSocket message cap.** Modal's ingress limits WebSocket messages to 2 MiB each, well below the runner tunnel's own 100 MiB allowance. Normal streaming traffic (events, terminal frames) is far smaller, but a single very large tool payload over the tunnel can fail on this platform. - **Connections reset at the 24 h input timeout.** A proxied WebSocket occupies one Modal function input, and inputs are capped at 24 hours — so a tunnel lives at most a day before being cut. Runners auto-reconnect (0.5–10 s jittered backoff). - **One always-on container by design.** `min_containers=1` / `max_containers=1` in `modal_app.py`: the runner registry is in-memory, so traffic must land on a single container, and scale-to-zero would kill live tunnels. Don't raise `max_containers` expecting horizontal scaling. - **No SQLite tier.** The artifact Volume is durable but is not a place for a SQLite database (eventual-consistency semantics); use Postgres. ### Use your own IdP instead (OIDC) Add the OIDC values to the `omnigent-deploy` secret (Modal secrets are key-value bundles; `modal secret create` with the same name replaces it) and redeploy: ```bash modal secret create omnigent-deploy \ DATABASE_URL='…' \ OMNIGENT_AUTH_PROVIDER=oidc \ OMNIGENT_OIDC_ISSUER='https://github.com' \ OMNIGENT_OIDC_CLIENT_ID='…' \ OMNIGENT_OIDC_CLIENT_SECRET='…' \ OMNIGENT_OIDC_REDIRECT_URI='https://--omnigent-server.modal.run/auth/callback' \ OMNIGENT_OIDC_COOKIE_SECRET="$(openssl rand -hex 32)" ``` The IdP registration steps (GitHub / Google / Okta callback URLs, domain allow-listing) are identical to the other platforms — see [`deploy/render/README.md`](../render/README.md#use-your-own-idp-instead-oidc). ### Custom domain Pass `custom_domains=["omnigent.example.com"]` to `@modal.web_server` in `modal_app.py` (requires a paid Modal plan), point your DNS at Modal per the printed instructions, and update `OMNIGENT_ACCOUNTS_BASE_URL` (or the OIDC redirect URI) to match. ### Upgrading `modal deploy deploy/modal/modal_app.py` again — Modal re-resolves `ghcr.io/omnigent-ai/omnigent-server:latest`, so a redeploy is an upgrade. The rollout replaces the container; runners reconnect. ### Cost Modal bills actual usage: memory at ~$0.008/GiB-hour and CPU by the cycle (so an idle server's CPU line is small). An always-on 1 GiB instance runs roughly **$6–8/month**, which fits inside the Starter plan's **$30/month of free credits** — making this effectively free for a lightly loaded server. Rates: [modal.com/pricing](https://modal.com/pricing). ## Sandboxes for runner hosts Modal sandboxes give you disposable cloud machines for running Omnigent hosts — no laptop tethered to a session, no VM to babysit. There are two ways to use them: 1. **CLI-launched sandboxes** — you provision a sandbox from your terminal and register it as a host with your server. Good for development and for running your local checkout's code in the cloud. 2. **Server-managed sandboxes** — the server provisions a sandbox automatically when a session is created with `"host_type": "managed"`, and terminates it when the session is deleted. Good for production deployments where users shouldn't have to think about hosts at all. Both boot from the official prebaked host image, so startup is seconds, not minutes. ### Sandbox prerequisites ```bash pip install 'omnigent[modal]' # installs the modal SDK extra modal token new # one-time browser auth with Modal ``` `modal token new` writes `~/.modal.toml`. Anywhere Omnigent needs to talk to Modal (your laptop for the CLI flow, the server for the managed flow), Modal credentials must be available — either that file or the `MODAL_TOKEN_ID` / `MODAL_TOKEN_SECRET` environment variables. ### The host image Sandboxes boot from `ghcr.io/omnigent-ai/omnigent-host:latest`, an image published by CI from the `host` target of [`deploy/docker/Dockerfile`](../docker/Dockerfile) with Omnigent and its dependencies preinstalled — including the coding-harness CLIs (`claude`, `codex`, `pi`, `kiro-cli`), so agents on any harness run without an in-sandbox install. To use a different image (a fork, or extra tooling baked in), build the same target and push it anywhere Modal can pull from: ```bash docker build -f deploy/docker/Dockerfile --target host \ -t docker.io//omnigent-host:latest . docker push docker.io//omnigent-host:latest ``` Then point Omnigent at it — `OMNIGENT_MODAL_HOST_IMAGE` for the CLI flow, or `sandbox.modal.image` in the server config for the managed flow (see below). For private registries, set `OMNIGENT_MODAL_REGISTRY_SECRET` to the name of a [Modal secret](https://modal.com/secrets) containing `REGISTRY_USERNAME` / `REGISTRY_PASSWORD`. > [!NOTE] > Building on Apple Silicon? Pass `--platform linux/amd64` — Modal > sandboxes run x86_64. ### CLI-launched sandboxes Provision a sandbox and ship your local checkout into it: ```bash omnigent sandbox create --provider modal ``` This pulls the host image, builds wheels from your local checkout, and overlays them on top — so the sandbox runs *your* code, not whatever the image was built from. Then register it as a host with your server: ```bash omnigent sandbox connect --provider modal \ --sandbox-id \ --server https://your-host ``` `connect` runs `omnigent host` inside the sandbox and holds the connection open in your terminal — Ctrl-C tears it down. New sessions targeting that host now run in the sandbox. Running multiple sandboxes against one server? Pass a unique `--host-name