Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS=true
# How long one action waits for its element, in ms. Read by agent-computer, not the server.
# ACTION_TIMEOUT_MS=10000

# Extra environment variables a command on the computer may see, by name, comma-separated. The
# shell already receives PATH, locale, terminal and proxy variables. Proxy userinfo is stripped, so
# a password in HTTP_PROXY is not in `env`. It does not inherit the rest of this process, so
# KEY_ENCRYPTION_KEY and the other deployment secrets are not in `env`. Naming a secret or a
# credentialed proxy here is an operator's decision.
# COMPUTER_SHELL_ENV=JAVA_HOME,GOPATH

# ---------------------------------------------------------------------------
# The computer's profile and where its traffic leaves from
# ---------------------------------------------------------------------------
Expand Down
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,52 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.
Which way it went is printed at start-up either way.

### Fixed
- **A Bot could become root inside its container.** `sudo` was granted as `NOPASSWD: ALL`, and the
comment above it named the two conditions that made that acceptable: the container being one Bot's
alone, and not holding a database. The image meets neither, because the supervisor is deliberately
not in it and `EMBEDDED_POSTGRES=on` is a documented way to run it. So root read another Bot's
workspace, the API's environment, and the audit database recording what it did. The grant now names
the package managers, so `apt-get install` still works and `sudo cat /proc/1/environ` does not. It
is a floor rather than a boundary: code a model wrote needs a computer per Bot with
`COMPUTER_SUPERVISOR_URL` and a sandbox under it with `COMPUTER_RUNTIME=runsc`, both of which this
already supports and neither of which the single-container image can reach.
- **A command could take the computer down, or outlive being stopped.** Output was accumulated in
full and only trimmed at the end, so `cat` of a large file allocated until the process that owns
the browser died; it is now bounded as it arrives, and still reports that it was truncated rather
than quietly ending. A stop signalled bash alone, so `sleep 30 | cat` left its children holding the
pipes and the call never returned; the whole process group is signalled now. A `timeoutMs` of zero
or less killed the command before it started and called it a timeout; it has a floor as well as a
ceiling.
- **Stop did not reach a running command.** The `/exec` route never took the person's abort, so the
plumbing for it was dead code and a stopped run left the command finishing inside the container.
- **The live-screen socket did not check the address it was given.** Every acting path resolved
through the gateway, which refuses a foreign or cloud-metadata address; this one asked the provider
directly and then put `COMPUTER_TOKEN` in the query string of whatever it was told.
- **`COMPUTER_SHELL_ENV` refuses the names that run before a command.** Naming `GITHUB_TOKEN` is an
operator deciding a Bot may use a token. Naming `BASH_ENV`, `ENV`, `LD_PRELOAD` or the shell option
variables is handing a Bot a hook into every later command, which is unlikely to be what was meant,
so those are refused and said out loud rather than passed. A name that is not a variable name is
now reported too, instead of quietly disappearing.
- **A deny rule naming one field refused every action that did not have it.** `deny:
contains(command, "rm -rf")`, the example the documentation gives, refused every click, keypress,
navigation and file read in the deployment. Two correct behaviours combined into a wrong one: the
policy context left out fields an action did not have, cel-js treats a missing field as an unknown
identifier and throws, and a thrown deny counts as a match so that a mistyped deny refuses rather
than quietly permitting. Every field is now bound, with a neutral value where the action has
nothing to put there, so a rule about a shell answers honestly about a click instead of refusing
it. Rules about the action they are for are unchanged. The audit row still omits what did not
happen.
- **A command longer than 45 seconds reported failure while it carried on running.** The transport
gave every call the same deadline, which was shorter than the shell's own 120 second default and
600 second maximum, so `apt-get install` told the person the computer had not responded and then
finished installing inside the container. A command now gets a deadline that outlasts the shell,
which reports a timeout itself and says so.

- **A Bot's shell no longer inherits the deployment's environment.** Commands ran with the computer
process's own environment, so `env` in the one-container image printed `KEY_ENCRYPTION_KEY` and
the rest of `.env`. The shell now receives PATH, locale and terminal names, and the proxy
variables. Userinfo is stripped from a proxy URL, so a password in `HTTP_PROXY` is not in `env`.
Anything else is named in `COMPUTER_SHELL_ENV`.
- **A deployment served over plain HTTP could not start a conversation.** The chat surface minted
identifiers with `crypto.randomUUID`, which browsers withhold outside a secure context. On a
laptop `http://localhost` counts as one, so this never showed up in development; on a real
Expand Down
33 changes: 24 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -126,19 +126,34 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& mkdir -p /var/lib/postgresql/data /var/run/postgresql \
&& chown -R postgres:postgres /var/lib/postgresql /var/run/postgresql

# A Bot can install what a task needs.
# A Bot can install what a task needs, and nothing else as root.
#
# `sudo` for one user, no password, because a package manager that cannot install is not one, and
# "install a tool then use it" is the whole point of giving a Bot a shell.
# `sudo` without a password, because a package manager that cannot install is not one, and "install a
# tool then use it" is the whole point of giving a Bot a shell.
#
# BE CLEAR WHAT THIS COSTS. It means a Bot can become root inside its container. That is acceptable
# when the container is the Bot's alone and is contained from below, which is why per-Bot computers
# and gVisor are not optional extras next to this feature; they are what makes it sane. In a
# container shared between Bots, or one holding a database, a Bot with sudo can reach all of it.
# THE PACKAGE MANAGERS, NOT ALL. This was `NOPASSWD: ALL`, and the comment below it explained what
# that cost: a Bot could become root inside its container. It then named the two conditions that make
# that acceptable — the container being one Bot's alone, and not holding a database — and this image
# meets neither. The supervisor is deliberately not in it, so every Bot shares one computer, and
# `EMBEDDED_POSTGRES=on` is a documented way to run it. So root here read another Bot's workspace, the
# API's environment, and the audit database that records what it did.
#
# Naming the commands keeps the feature and removes that. `apt-get install` still works, which is what
# the tool description tells a model to run. `sudo cat /proc/1/environ` does not.
#
# WHAT THIS IS NOT. It is a floor, not a boundary. Root is one CVE away and a shared container is not
# an isolation story for code a model wrote: that needs a computer per Bot and a sandbox under it,
# which is why per-Bot computers and gVisor are not optional extras next to this feature. Run the
# image with `--security-opt no-new-privileges` where the platform allows, which turns setuid off
# entirely for anything not named here.
RUN apt-get update && apt-get install -y --no-install-recommends sudo \
&& rm -rf /var/lib/apt/lists/* \
&& echo 'pwuser ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/pwuser \
&& chmod 0440 /etc/sudoers.d/pwuser
&& printf '%s\n' \
'pwuser ALL=(root) NOPASSWD: /usr/bin/apt-get, /usr/bin/apt, /usr/bin/dpkg, /usr/bin/apt-key, /usr/bin/apt-cache' \
'Defaults!/usr/bin/apt-get env_keep += "DEBIAN_FRONTEND"' \
> /etc/sudoers.d/pwuser \
&& chmod 0440 /etc/sudoers.d/pwuser \
&& visudo -cf /etc/sudoers.d/pwuser

# THE PACKAGE MANAGER AND THE SHELL STAY. Both were removed here once as hardening, which was
# backwards: a Bot being able to open a shell and install what a task needs is a requested feature,
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ as one replica for now.
## Features

- **A computer per Bot**: the supervisor gives each Bot its own container, its own `/workspace` volume and its own browser profile. Set `COMPUTER_RUNTIME=runsc` to run them under gVisor where the host supports it.
- **A shell, not just a browser**: a Bot can run a command in its workspace, install what it needs, and process a file it saved. Through the same gate as everything else, so a rule can refuse a shell outright or refuse particular commands, and the command is on the record either way.
- **A shell, not just a browser**: a Bot can run a command in its workspace, install what it needs, and process a file it saved. Through the same gate as everything else, so a rule can refuse a shell outright or refuse particular commands, and the command is on the record either way. The command inherits PATH, locale, terminal and proxy variables, not the rest of the deployment's environment.
- **The gateway is the only way in**: it resolves the target from a server-held snapshot, evaluates the policy, writes the audit row, and only then calls the computer. There is no path that acts without the record existing first.
- **CEL policy, fail closed**: rules can inspect `tool.name`, `intent`, `bot.id`, `actor.id`, `page.url`, `page.host`, `element.*`, `key`, `file.*` and `mcp.*`. Deny is evaluated before allow, a missing policy permits nothing, and a broken rule refuses rather than opens.
- **Take the wheel**: a Bot that hits a login wall or a 2FA prompt asks for help. Control is handed over in the same panel and recorded as `computer.help_requested`, `computer.control_taken` and `computer.control_released`. While a person is driving, Bot actions are refused rather than queued.
Expand Down
Loading