# Browser-ready desktop packages Desktop installers include the pinned `agent-browser` engine and Chromium Headless Shell. A fresh installation needs no separate browser download. This is an automation browser, not a headed Google Chrome application. Browser access is on by default, both for the workspace (Settings) and for each bot (its computer panel); switching either off withholds the browser, and bundling executables never overrides that switch. ## Package layout and lookup `pnpm package:prepare` runs `pnpm build:browser`. It downloads verified vendor archives and stages `dist-native/browser/PLATFORM-ARCH`, copied as `Resources/browser-engine` on macOS or `resources/browser-engine` elsewhere. Each package includes only its target architecture: macOS ARM64/x64, Windows x64, or Linux x64. The server receives `OMB_RESOURCES_PATH` from Electron. It resolves an explicit `OMB_AGENT_BROWSER_PATH` override first, then the complete bundled engine and browser, then a separately installed engine or PATH. An incomplete bundle fails closed with a reinstall/update message. An explicit `AGENT_BROWSER_EXECUTABLE_PATH` still overrides the browser executable. Profiles, cookies and downloads are user data, never packaged resources. The npm/self-hosted distribution stays small and retains its explicit browser installation flow. Desktop packaging does not silently enlarge the npm tarball. ## Which Chrome the bots' browser runs agent-browser still drives every bot browser, but the browser it starts is a real Chrome whenever this machine has one, chosen once per machine (`server/browser/chrome/resolve.ts`): 1. The person's installed **Google Chrome**, unless an organization manages it (`/Library/Managed Preferences/com.google.Chrome.plist`, the `HKLM\SOFTWARE\Policies\Google\Chrome` key, or a JSON file in `/etc/opt/chrome/policies/managed`). Bots get their own profiles; the person's Chrome profile is never touched. 2. Otherwise **Chrome for Testing** 153.0.8010.47, downloaded once into `tools/chrome/` in the data directory at first launch, checked against its pinned size and SHA-256 before it is unpacked, and moved into place in one rename. Only platforms with a pin download (Apple silicon Macs and Linux servers on x64 or arm64 today; `scripts/pin-chrome-for-testing.ts` measures more). A browser call made during the download waits up to 45 seconds, then says how far it got. 3. A Linux desktop without Google Chrome, and a platform without a pin, keep the bundled headless shell. That stays the machine's choice; it never changes while the server runs. Servers and the Docker image name their browser with `AGENT_BROWSER_EXECUTABLE_PATH` (the image's Chrome for Testing), or use the Chrome for Testing `agent-browser install` put in place. A Linux server where agent-browser already finds a browser (Chrome, Chromium or Brave on `PATH`, or a complete Puppeteer or Playwright Chromium in their caches) keeps it; with none, it downloads its own as in step 2. `OMB_CHROME_PATH` names one explicitly. An `OMB_AGENT_BROWSER_PATH` engine keeps its own choice unless a Chrome is named too. **Identity.** The chosen Chrome reports exactly what it reports headed, read from the binary itself once per version: its own user agent without "HeadlessChrome", and its own client hints (brands, full version list, platform version, architecture). agent-browser runs a generated launcher (`browser/chrome-launcher.sh`, `.cmd` on Windows) that adds `--disable-blink-features=AutomationControlled`, that user agent, the primary display's size and, when OMB knows it, the language; on a server it also passes the server's `TZ`. A bot's Chrome never touches the person's keychain or keyring (`--use-mock-keychain` on macOS, `--password-store=basic` on Linux), and on macOS it always runs with the person's own home, so no "keychain cannot be found" dialog can appear. A second DevTools connection gives every page, cross-site frame and worker the matching client hints before it runs. Nothing is invented: Chrome for Testing reports Chromium brands, Google Chrome its own. `OMB_BROWSER_SCREEN`, `OMB_BROWSER_LANG` and `OMB_BROWSER_TIMEZONE` override the display, language and zone. `openmausbot browser status`, the server's startup log and the `browserEngine.chrome` field of its configuration status name the choice, for example `Google Chrome 155 (installed)`. **Rollback:** start the server with `OMB_BROWSER_LEGACY_SHELL=1` to run exactly the previous engine. `node --experimental-strip-types scripts/verify-browser-identity.ts --public` checks what sites see, in a throwaway home: the identity on a loopback probe page, then the four public bot-test pages, read-only. ## Signed-in sites and My Chrome Each built-in browser lists its signed-in sites (Settings → Browser profiles, or a bot's Browser panel) with Sign out per site and Sign out of all. It works the same on desktop, a server and Cloud, for an open or a closed browser. A shared profile can instead use **My Chrome**, the person's own running Chrome, for sites that refuse the automated one, such as Google sign-in. That needs Chrome 144+ with remote debugging on at `chrome://inspect/#remote-debugging`, and either the desktop app or My Cloud with a Mac or PC that lends its Chrome. That computer lends Chrome only by its own **Your Chrome** switch, off by default and dropped when lending is turned off; Chrome then travels over the device link, to the DevTools port the computer finds itself, and only the person's own conversations and routines use it. My Chrome works only on a personal Cloud home: on a Team Cloud and on self-hosted servers profiles stay built-in. Details: [the browser engine plan, step 5](plans/browser-engine.md) and [MausBot Cloud](cloud-pro.md#your-chrome). ## What a bot's browser can reach Every Chrome OpenMausBot launches for a bot sends all of its connections through an egress gate: a SOCKS5 server on `127.0.0.1`, one per browser session, run by the OpenMausBot server (`server/browser/egress/`). The gate resolves names itself and checks every address a name resolves to before it connects, so redirects and DNS rebinding reach nothing it would refuse. It carries bytes only, so TLS, HTTP/2 and WebSockets are unchanged. Chrome sends loopback traffic through it too (`--proxy-bypass-list=<-loopback>`), and WebRTC sends no UDP around it. If the gate is not running, the browser connects nowhere. | Destination | Desktop | Self-hosted server | MausBot Cloud | |---|---|---|---| | Public internet | yes | yes | yes | | OpenMausBot's own ports (loopback and this machine's addresses) | no | no | no | | Other ports on this computer | yes, by `localhost` or an address, not by a public name | only servers a bot's own processes opened (Linux), or allowed below; with "Open sites on this server" on, yes, by `localhost`, an address or a name that points at this server's own addresses, not a public name that points at loopback | only servers a bot's own processes opened | | Local network: 10/8, 172.16/12, 192.168/16, 100.64/10, fc00::/7 | yes, unless `browserEngine.egress.localNetwork` is `false` | only what `browserEngine.egress.allow` lists, or all of it with "Open sites on this server" on | no | | Link-local and cloud metadata (169.254/16, fe80::/10, `fd00:ec2::254`, 100.100.100.200) | no | no | no | | 0/8, 198.18/15, multicast | no | no | no | OpenMausBot's own ports are the server's and its webhook receiver's, the desktop client relay, the companion and its control page and gateway, the Vite dev server, the Local VM viewer, every port the server process has listened on, the ports the desktop app reports, each gate, and agent-browser's stream servers and Chrome DevTools ports. Servers add their HTTPS edge (8080), Caddy's admin API (2019) and the ports of fleet workspaces. "A bot's own processes" means a process in the session of an engine running a bot's turns, so a dev server the bot started keeps counting after the shell that started it exits. Other platforms than Linux cannot tell, so there only allowed destinations are reachable. A self-hosted server opens more with `config.json`: ```json { "browserEngine": { "egress": { "allow": ["192.168.1.0/24", "10.0.0.5:8080", "127.0.0.1:9000"] } } } ``` Each entry is an address or CIDR range with an optional `:port` (IPv6 in brackets for a port: `[fd12::1]:443`). `localhost` resolves to both loopback addresses, so `127.0.0.1:9000` also allows `[::1]:9000` and the other way round, on that port only. OpenMausBot's own ports, metadata and link-local addresses stay closed whatever it lists, and MausBot Cloud and shared company workspaces ignore it. Settings > Computer > "Open sites on this server" (`browserEngine.egress.localNetwork: true`, off by default and shown only to the server's owner or an admin; not on a shared company workspace, whose network is the operator's) opens this server's own sites and its local network without listing them. OpenMausBot's own ports, metadata and link-local addresses stay closed, also through a private address such as a Docker host's gateway, and a public name that resolves to loopback is still refused. It applies to new connections; a page already open keeps the ones it has until it closes them. Refused connections show up in the bot's next browser result: the first three by name and the rest as a count (up to 20 per result, each destination and reason once): `The page tried to reach "127.0.0.1:8799" (blocked: OpenMausBot's own service)`. `agent_browser_read` reads the page open in the browser. Given an address, agent-browser 0.37 would fetch it with its own HTTP client, around the browser and the gate, so OpenMausBot does not offer that parameter and refuses a call that passes one; the bot opens the address first. My Chrome is the person's own browser and does not go through the gate; the server never treats a browser as its owner (see [self-hosting](self-hosting.md#a-browser-signs-in)), and the companion's control page needs its per-launch token. ## Process hygiene Each built-in browser is an agent-browser daemon and the Chrome it starts. agent-browser starts the daemon in a session of its own and Chrome in a process group of its own, so neither stops with OpenMausBot, and a daemon nothing closes keeps its Chrome for the engine's one-hour idle timeout. The same rules hold on the desktop, a server and Cloud: - **Closing waits for the processes.** `close` answers before the daemon has exited (about 150 ms later on a Mac). A browser counts as closed once the engine no longer lists it and nothing of the process tree read before the close still runs. Past the deadline the daemon gets TERM first, alone, so it can still save the sign-ins from its Chrome; whatever is left after a short grace is killed by process group, and a process outside those groups by its pid (Windows: `taskkill /T`), and Chrome's temporary profile folder is removed as the daemon would have. Only a pid file that still names this session's daemon is acted on: the same agent-browser binary, with `AGENT_BROWSER_DAEMON=1` and the session's name in its environment (on Windows, which shows neither, the same image, checked again before `taskkill` and while waiting). Each process is matched by its start time, so a pid the system gave to another program, another bot's daemon or an engine command included, is never signalled. A browser that is not running is never sent `close` at all: the CLI would start a daemon only to stop it again (and leave its `.config` file behind). On Linux the process table is read from `/proc`, so an image without `ps` works the same. - **Unused browsers close.** A browser nothing has used for `browserEngine.idleTimeoutMinutes` in `config.json` (1 to 60; 10 when unset, 3 on a MausBot Cloud home, whose 4 GB fits few browsers) is closed: no bot call (a tool listing counts, and so do a call waiting behind another, its page-health reads and recorded step, and fill_login's typing; a preview frame only holds off a close while it is taken), no turn that has used it and is still running, and no person holding it or using it through a Browser panel (opening the panel, a click, a key, an address, taking or handing back control). A panel that only watches does not keep it open: the panel says the browser closed after it sat unused and offers to open it again, and on the new engine it shows the browser again by itself once the bot's next call starts it. It never reconnects on its own, which would start the browser again. Saved sign-ins stay, and the bot's next browser call opens it again and is told its pages are gone. A temporary browser saves nothing, so it starts fresh, and the bot is told its sign-ins went too. A profile set to My Chrome is the person's own Chrome and is never closed. - **Leftovers are swept.** A crash or a kill closes nothing, so the server sweeps when it starts, every minute, and at shutdown. It closes only this data dir's browsers: each bot's own, each built-in profile's, and its temporary browsers, whose names carry a tag of the data dir. On a MausBot Cloud home, whose HOME is the server's alone, it also closes the browser of any bot that no longer exists (a `bot-` session), within a minute. Deleting a bot or profile closes its browser and erases its saved sign-ins with the agent-browser OpenMausBot installed, or on a Cloud home the image's own; an agent-browser found only on PATH elsewhere just has the running browser closed, since a wrapper there could fail the erase for good. A Chrome whose daemon was killed under it (by the OOM killer, by hand) can never be used again: it is found by the session its environment names and stopped, and its profile folder removed. Anything else under the same HOME, such as a person's own agent-browser sessions, is left alone. Browsers are told apart by session name, and agent-browser keeps one daemon per name under a HOME: a person's own agent-browser session named like one of this data dir's profiles (a profile from before profile ids were random, such as `work`), or a copy of this data dir run under the same HOME, shares this data dir's browsers, and the sweep closes them as its own. Windows lists no environments, so there a Chrome whose daemon was killed is not found. **Containers.** Chrome's helper processes need something to collect them once their parent exits, and Chrome needs shared memory. Both Docker compose files and the Podman one set `init: true` and `shm_size: 512m`, as OpenMausBot's desktop containers do (`--shm-size 512m`); a plain `docker run` wants `--init --shm-size=512m`. agent-browser 0.37.0 itself adds `--no-sandbox` and `--disable-dev-shm-usage` when it sees `/.dockerenv` or `/run/.containerenv`, runs as root or under CI, or finds docker, kubepods or lxc in `/proc/1/cgroup`. A runtime that shows none of these (a Kubernetes pod on cgroup v2, for one) needs `/dev/shm` room of its own (on Kubernetes, an `emptyDir` with `medium: Memory`). The image does not set `AGENT_BROWSER_ARGS`: OpenMausBot hands its browsers a curated environment that would not carry it. On Fly Machines (the Cloud home) Fly's own init runs as PID 1. ## Video recording The macOS and Linux engine is pinned to agent-browser 0.37.0. Its [`record start` and `record restart`](https://agent-browser.dev/recording) commands record the active page at 30 fps by default; `--fps 60` selects 60 fps (valid range: 1–60). This saves a video file; it does not enable a live view. Recording requires `ffmpeg` on the server's PATH, with `libvpx` for WebM or `libx264` for MP4. `agent-browser doctor` reports these optional dependencies. They are not bundled or required for normal browsing. Higher frame rates use more CPU and disk space; the actual distinct frames depend on page repaints. There is no recording control in OMB's browser panel. ## Linux sandbox Use the `.deb` on Ubuntu 24.04. Its package hooks install a narrowly scoped AppArmor policy for the root-owned browser executable under `/opt/OpenMausBot`. They do not disable the browser sandbox or change the global user-namespace restriction. See [Linux desktop](linux-desktop.md). The AppImage contains the same binaries, but cannot install a privileged sandbox policy. Restricted hosts may need the `.deb` rather than the AppImage. ## Release verification and updates Versions, URLs, sizes and SHA-256 digests live in `server/browser-bundle-release.ts`; engine pins are shared with `server/browser-engine-release.ts`. Every cached download is rechecked. Preparation inventories the entire vendor tree, including licenses. The `afterPack` gate rejects missing resources, changed bytes or wrong executable architectures before signing. macOS signing changes native bytes, so signed packages are subsequently checked using code signatures and runtime tests, not the original upstream executable hashes. Run the real browser check against an unpacked app: ```sh node scripts/smoke-browser-bundle.mjs --resources /absolute/app/resources ``` On macOS, use `/absolute/OpenMausBot.app/Contents/Resources`. On Linux, run as an unprivileged user against the installed `.deb` at `/opt/OpenMausBot/resources`. This check creates its own empty home and local web page, checks automatic discovery, navigation, typing/clicking, the loopback command relay the server reads, pictures and types through (`server/browser-relay.ts`), screenshot delivery and two-bot cookie/storage isolation, then removes only its fixture. No model account or user browser profile is used. Cross-target `--check-only` checks layout only and is not evidence of successful browser execution. The release workflows run this check on native macOS, Windows and Linux runners before publishing. macOS also verifies the browser binaries are signed with the app's team identity. The browser is updated through a reviewed app release: review the new vendor target/license contents, update all platform pins, run preparation/tests and the native package gates, and ship promptly when browser security updates are needed. There is no independent automatic browser updater in the desktop bundle. ### Temporary Windows engine backport Windows uses an explicitly identified OpenMausBot build of agent-browser 0.36.0, with the handle-inheritance fix from [upstream PR #1781](https://github.com/vercel-labs/agent-browser/pull/1781). The official 0.37.0 release does not contain this fix, so Windows stays on the native-verified `0.36.0-omb.1` revision and does not yet get the new recording options. Do not replace it with the unpatched 0.37.0 Windows binary. The original binary can hang when a newly started background browser holds the tool call's output connection open. The backport changes that Windows startup behavior; it does not include the PR's broader output-reader rewrite or unrelated changes from upstream main. The artifact-only **Windows browser vendor build** workflow builds the pinned source and checked-in patch, retains the licenses and build provenance, and tests cold start plus close/reopen through the same MCP connection on Windows. Its candidate must be reviewed and its exact size and SHA-256 pinned before the normal application packaging workflow consumes it. Candidate testing does not replace the packaged-app tests. Published vendor bytes must not be overwritten; a changed build needs a new revision and reviewed pins. The Windows revision has a separate managed installation directory so an old 0.36.0 download is not mistaken for the patched engine. Desktop packages use their bundled engine. Explicit executable overrides remain user-managed. Remove the backport when an official release includes the fix and passes the same native cold-start and restart tests. Complete upstream notices and provenance are in [`third_party/browser`](../third_party/browser/README.md). The full Google Chrome distribution and its proprietary Widevine component are not bundled.