Run OpenBot as one container, and give a Bot a shell (#62)
* Give each Bot a computer on Daytona when an API key is configured Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu> * Keep Daytona reset monotonic across delayed sandbox indexes Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu> * Retry Daytona resets after transient deletion failures Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu> * Make Daytona computer lifecycle reads non-provisioning and bounded Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu> * Keep computer status reads renderable during supervisor failures Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu> * Check the address a supervisor hands back before calling it `resolveBaseUrl` returns a URL and the next line puts it into `fetch` with this deployment's computer token on it. That was fine while the only supervisor was ours, answering on loopback. It stops being fine with a hosted provider, where the address arrives from a third party's API and we call whatever it says. Deliberately not the navigation check. That one refuses private hosts, which is correct for a Bot browsing and exactly wrong here: our own supervisor answers `http://127.0.0.1:<port>` for a container on this machine, so reusing it would refuse the ordinary case. What survives is what holds however the address was produced. The scheme has to be one a computer speaks, and the cloud metadata addresses are refused whatever anything says, because that is how a container's credentials leave it and no supervisor has a reason to name one. Groundwork for A10, where the supervisor becomes somebody else's API. * Tell OpenBot traffic apart in the analytics that already go The CopilotKit runtime reports a handful of events about itself and every deployment built on it looks the same in them, so there is no way to tell which of that traffic came through OpenBot. One field on those events answers it by filtering. No new events and no new pipeline. Carried on `global_properties`, which the sink treats as the pass-through bag for `oss.runtime.*` and spreads into the analytics event. `OPENBOT_ACCESSIBILITY_DISABLED=true` switches it off. The SDK's own `COPILOTKIT_TELEMETRY_DISABLED` and `DO_NOT_TRACK` still stop the events entirely, this field with them. Anything other than `true` or `1` leaves it on. A deployment that typed something else has not opted out, and reading a typo as consent to stop would be a setting that appears to work and does not. * Unify computer backends behind a deep provider and single gateway module Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu> * Make computer providers honest and race-safe Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu> * Give a Bot a shell, and let Chromium sandbox itself where the host allows A browser alone cannot install a tool, read a file it just downloaded, or run the thing it was asked to run. `computer_run_command` runs a command in the Bot's workspace through the same gate as every other action: the target is resolved, the CEL policy decides, an audit row is written, and only then does anything happen. The command is recorded in full; its output never is, because output is the one part that can carry a page's contents back into the audit log. Bounded rather than trusted. Two minutes by default and ten at most, 64KB of output kept from the end, and the abort signal kills the child rather than detaching from it. The sandbox flag is the other half. `--no-sandbox` was unconditional, which is what a default container seccomp profile forces, but Playwright re-adds the flag on its own unless `chromiumSandbox` says otherwise, so a deployment that could have had the sandbox silently did not. Now COMPUTER_SANDBOX=on means it, and either way the choice is printed at start-up. * Run the whole thing as one container, with Postgres as a choice A laptop runs the app, the API, a browser, and a database. Deploying that shouldn't mean learning Kubernetes first, so the image carries all four and s6 supervises them: the app is served by the API process from a built directory, the browser runs beside it, and Postgres starts only when no external one was given. Point DATABASE_URL at a managed Postgres and the embedded one never starts; leave it unset and the container is self-contained. s6 rather than a shell script or supervisord because a browser that dies should take the container down and let the platform restart it, not leave an API answering requests it cannot serve. Runs as pwuser, not root. Chromium rendering the open internet as root inside a container that also holds the database is the pairing to avoid, and the earlier version had it. One replica, and the deployment doc says so out loud: this image holds state a load balancer would split. The fleet shape is A6. Minimum sizes in the doc are measured, not guessed, and /dev/shm gets its own note because the platforms that cap it at 64MB make Chromium fail in a way that reads like a bug in us. * Keep a changelog, and ask a PR to add to it There is no way today for somebody running OpenBot to find out what changed between two commits other than reading them, and the commits are written for whoever touches the code next. So one file, newest first, written for the operator: a line lands when a deployment behaves differently afterwards, and does not when only the code moved. The PR template asks for the line, or for a sentence saying why there isn't one. Backfilled with what is on this branch and not yet released. * Say what a Bot's shell can do, and show the command it ran Three things found by driving the built container rather than reading it. The first attempt to install a package failed. The shell runs as an unprivileged user, `sudo` is there and needs no password, and nothing told the model any of that, so it ran `apt-get install` bare, got permission denied, and offered to explain how to do it somewhere else. The tool description now says so, and the same request installs on the first try. The audit row for a command read "not in the current snapshot" under what it acted on. A command has no page element and never will, the same as a file action, and the row now carries the command instead. The deployment doc still said PostgreSQL was not in the image, and the README had no deployment section at all. * Say that a rule can match on the command The Boundaries page lists what a CEL rule may ask about, and the shell added a field the list did not mention. An operator reading that page would conclude there was no way to write a rule against a command, when `contains(command, "...")` works and refuses before anything runs. * Stop the chat dying silently on a deployment without TLS `crypto.randomUUID` is only defined in a secure context. A laptop never sees this, because `http://localhost` counts as one; a deployment reached at `http://<address>` does not, so the function is simply absent. The chat surface called it directly in five places, all of them on the submit path. The throw landed inside a React event handler, was swallowed, and the surface did nothing: no message, no error, no clue. The channel was created, so it looked like the Bot had nothing to say. Found by deploying the container to a real address and pressing send, which is the only place this is visible. Ids now come from `crypto.getRandomValues`, which has no such restriction, and the fallback is the case under test rather than the one nobody runs. TLS is still what a deployment should have. This is about failing where an operator can see it. * Use a neutral host in the computer address test --------- Signed-off-by: Muhammad Hashmi <mhashmi@berkeley.edu> Co-authored-by: Muhammad Hashmi <mhashmi@berkeley.edu>
D
David McKay committed
c647fa4ff2882857aff244367d62736089e32aa4
Parent: a5450a7
Committed by GitHub <noreply@github.com>
on 8/20/2026, 10:36:27 PM