SIGN IN SIGN UP

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