fast-forest-2675
06/09/2026, 6:09 AMcalm-artist-7206
06/09/2026, 10:14 AMlimited-student-10747
06/09/2026, 11:44 AMnutritious-balloon-65098
06/09/2026, 11:45 AMlimited-student-10747
06/09/2026, 11:55 AMnutritious-balloon-65098
06/09/2026, 11:56 AMlimited-student-10747
06/09/2026, 11:59 AMnutritious-balloon-65098
06/09/2026, 11:59 AMlimited-student-10747
06/09/2026, 3:54 PMfierce-account-55183
06/09/2026, 3:55 PMlimited-student-10747
06/10/2026, 5:46 PMnutritious-balloon-65098
06/10/2026, 5:48 PMfast-forest-2675
06/15/2026, 1:35 PMlimited-student-10747
06/24/2026, 2:18 PM## Answer to Guhan's air-gap question (from the Slack thread)
I dug into the actual `agent-canvas` Docker image (`docker/Dockerfile`, `docker/entrypoint.sh`), `config/defaults.json`, the OpenHands SDK runtime code (`software-agent-sdk`), and the agent-server base image Dockerfile. Here's the verdict.
### Short answer
**Not out of the box β it will die without mitigation β but it is achievable with a known set of config changes.** The outbound `:443` connections Guhan saw at conversation start come from a handful of well-defined places, all of which have knobs.
### Will it work or die with no external network?
- **Control plane (agent-server + automation + frontend) starts fine air-gapped.** The entrypoint only talks to `127.0.0.1` (agent-server `:18000`, automation `:18001`, static server `:8000`). No outbound calls on startup. β
- **A conversation will die** on the first turn unless you fix these runtime network dependencies:
### The runtime `:443` calls and how to kill each
**1. LLM inference (the big one).** Every turn calls the LLM provider's HTTPS API. This is essential and *must* be served from inside the air-gap.
- *Mitigation:* Point `llm.base_url` at a self-hosted OpenAI-compatible endpoint inside the network (vLLM / SGLang / Ollama / LiteLLM proxy serving an open model). The repo's own mock-LLM E2E (`tests/e2e/mock-llm/scripts/mock-llm-server.py`) proves any OpenAI-compatible `base_url` works β set it via Settings β LLM (Advanced view) or `LLM_BASE_URL`/`LLM_MODEL`/`LLM_API_KEY`. Note: the default model `openhands/minimax-m2.7` points at the All-Hands LLM proxy (`llm-proxy.app.all-hands.dev`) β that won't resolve air-gapped, so you must override.
**2. MCP servers launched via `npx`/`uvx`.** If an agent profile references an MCP server whose command is `npx -y <pkg>` or `uvx <pkg>`, the first connect fetches the package from npm/PyPI.
- *Mitigation:* The agent-server image already **pre-installs and pins** the ACP CLIs (`claude-agent-acp@0.44.0`, `codex-acp@0.46.0`, `gemini-cli@0.46.0`) at build time and rewrites `npx -y <pkg>` to run the local binary (no fetch) β see `acp_providers.py::_maybe_use_pinned_binary`. For any *other* MCP servers you use, either (a) pre-install the package into the image and point the MCP config at the local binary, or (b) avoid MCP servers that aren't vendored. The `github-mcp-server` Go binary is already baked into the agent-canvas image for the same reason.
**3. Browser tool β Chromium download.** `browser_use/impl.py::_install_chromium()` runs `uvx playwright install chromium --with-deps` on first use if Chromium isn't found β this hits the Playwright CDN.
- *Mitigation:* Either set `VITE_ENABLE_BROWSER_TOOLS=false` to drop `BrowserToolSet` from new conversations, **or** pre-install Chromium into a custom runtime image (`uvx playwright install chromium` at build time) and bind-mount `~/.cache/ms-playwright`.
**4. Telemetry (PostHog / `z.openhands.dev`).** Two systems: frontend PostHog (`<http://us.i.posthog.com|us.i.posthog.com>`, keyed off `VITE_POSTHOG_CLIENT_KEY`) and library telemetry (`z.openhands.dev`).
- *Mitigation:* Omit `VITE_POSTHOG_CLIENT_KEY` (PostHogProvider never mounts), set `VITE_DO_NOT_TRACK=1`, and set `user_consents_to_analytics=false` in settings. The live E2E suite already does exactly this (`playwright.live.config.ts`).
**5. Cloud API key validation (`app.all-hands.dev`).** Without it, automation auth falls back to validating locally-generated session keys against the cloud API β 401.
- *Mitigation:* Already handled by the entrypoint β `AUTOMATION_AGENT_SERVER_URL` defaults to `<http://127.0.0.1>:<agent-server-port>`, which flips automation into **local mode** (validates keys internally, no cloud call). Just don't unset it.
**6. Runtime image pull (only if you use DockerRuntime).** The agent-server base image ships **no Docker** ("No Docker, no VNC, no Desktop, no VSCode Web") and uses a **tmux-based local runtime** (`libtmux`, `TerminalTool`) β so by default there's **no `docker pull <http://ghcr.io/openhands/runtime`**|ghcr.io/openhands/runtime`**> at conversation start. If you instead configure a Docker/remote runtime, you must pre-pull the runtime image into a local registry or `docker load` it.
### Recommended air-gapped deployment recipe
1. **Build** the all-in-one image normally (build needs internet β that's fine; air-gap is a run-time constraint): `node scripts/docker-build.mjs`.
2. **Pre-bake** anything you'll need that normally fetches at runtime into a derived image: `uvx playwright install chromium`, any extra MCP server binaries, your model's tokenizer, etc.
3. **Stand up a local LLM** (vLLM/SGLang/Ollama/LiteLLM) serving an open model on the air-gap network.
4. **Run** the container with egress blocked (e.g. `--network none` if the LLM runs in the same container, or a restricted bridge network + iptables/egress-proxy allow-list to only the LLM host).
5. **Set env:** `LLM_BASE_URL`/`LLM_MODEL`/`LLM_API_KEY` β local LLM; `VITE_DO_NOT_TRACK=1`; omit `VITE_POSTHOG_CLIENT_KEY`; keep `AUTOMATION_AGENT_SERVER_URL` default; `FILE_STORE=local`; `VITE_ENABLE_BROWSER_TOOLS=false` (unless you pre-baked Chromium). In Settings, set `user_consents_to_analytics=false`.
6. **Don't configure MCP servers** that need `npx`/`uvx` fetch at runtime β use the pre-installed ACP/GitHub ones or vendor your own.
### Bottom line for Guhan
Yes, OpenHands/agent-canvas *can* run fully air-gapped, but it requires a self-hosted LLM and a handful of env/config knobs to suppress the runtime fetches (LLM API, optional Chromium, optional MCP `npx`/`uvx`, telemetry). The `:443` traffic he observed at conversation start is almost certainly the **LLM provider call** (default points at the All-Hands proxy) plus, if enabled, **MCP `npx`/`uvx` package fetches** and/or the **Chromium download** β all addressable per above. The control-plane services themselves are air-gap-clean.
---
*Note: I was unable to empirically run the container with `--network none` in this environment β the system `dockerd` is running but my user isn't in the `docker` group and there's no passwordless sudo, and rootless Docker couldn't be set up (`uidmap`/`newuidmap` missing, can't `apt install` without sudo). The analysis above is grounded in the actual source of the Dockerfile, entrypoint, SDK runtime, and agent-server base image rather than a live `--network none` run. Graham, if you want, I can write this up as a `docs/AIR_GAPPED.md` and/or produce a hardened `Dockerfile.airgap` + sample `docker run` invocation in the agent-canvas repo.*