* fix(desktop): launch spaced REA archives and Electron apps Link the unpacked REA entrypoint to a fixed path so a top folder with spaces is not split by the generated rea launcher. When a build request marks the app as Electron, rewrite its installed desktop command with the export path's launch policy before gtk-launch. * fix(desktop): escape rewritten Exec values for desktop files Command quoting alone left literal backslashes one escape short of the desktop-entry value layer, so gio rejected the rewritten entry. Escape the command with the same rules parseDesktopEntry undoes before writing the Exec line. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
166 lines
9.2 KiB
Markdown
166 lines
9.2 KiB
Markdown
# Desktop build computer
|
|
|
|
A build computer is a local agentenv computer that an agent drives while it
|
|
builds a desktop app environment. Claude Code cannot attach a new MCP server
|
|
in the middle of a session, so REA keeps the computer up and forwards the
|
|
agent's computer-use actions to it through three MCP tools:
|
|
|
|
| Tool | What it does |
|
|
| ----------------------- | ------------------------------------------------------------------------------------------ |
|
|
| `start_build_computer` | Starts a computer from a request or a bundle folder; returns its id and a first screenshot |
|
|
| `act_on_build_computer` | Sends one action to the computer's driver and returns the driver's answer, images included |
|
|
| `close_build_computer` | Tears the computer down, by agentenv's teardown or by removing its sandbox's containers |
|
|
|
|
The tools are MCP-only, like the debugger sessions: the computer lives as long
|
|
as the REA MCP server that started it, or until it is closed.
|
|
|
|
## Requirements
|
|
|
|
- Docker, and an agentenv-framework install whose `agent-env` program is named
|
|
by the absolute `REA_AGENT_ENV_COMMAND`. Bundles target agentenv
|
|
0.9.1431. REA runs its helper, `bridge/agentenv/computer.py`, with that
|
|
runner's Python: the interpreter on the program's `#!` line, or the
|
|
`python3` beside it in its virtual environment.
|
|
- `REA_DOCKER_COMMAND`, the absolute path of the Docker CLI, lets REA remove a
|
|
computer whose agentenv teardown was not confirmed. Without it, REA names the
|
|
command to run instead.
|
|
- For a request, `REA_BUILD_COMPUTER_REA_ARCHIVE`: the absolute path of a tar
|
|
archive of a runnable REA, with its production dependencies, whose one top
|
|
folder holds `scripts/rea.mjs`. The computer runs REA from it.
|
|
|
|
Set these where the MCP server starts; a shell export does not reach a server
|
|
that is already running.
|
|
|
|
## Start a computer
|
|
|
|
From a request, REA renders a bundle in a private temporary folder: a computer
|
|
app on the `ubuntu-24.04` base with the `reference` driver that installs the
|
|
app, opens it on demand with `gtk-launch <desktop entry>` and keeps no seed and
|
|
no data hooks. The app is not open when the computer starts: agentenv lists it
|
|
in the desktop's Applications menu under Other by the environment's name, such
|
|
as `mousepad`, and that entry launches it through the app's server.
|
|
|
|
When the app is Electron, set `electron` to true. The install rewrites that
|
|
desktop entry's command with the same policy an export uses: `--no-sandbox`
|
|
immediately after the program, unless the command already has it. The
|
|
computer's container does not grant the user namespaces Chromium's sandbox
|
|
needs, and Electron exits without the flag. With the rewritten command, the
|
|
Applications menu entry can open the app.
|
|
|
|
```json
|
|
{
|
|
"source": {
|
|
"kind": "request",
|
|
"desktop_entry": "org.xfce.mousepad",
|
|
"window_class": "Mousepad",
|
|
"install": { "kind": "archive", "package": "mousepad" }
|
|
}
|
|
}
|
|
```
|
|
|
|
`install` takes one of:
|
|
|
|
| `kind` | Installs |
|
|
| ---------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
| `archive` | `package` from the Ubuntu 24.04 archive, on amd64 and arm64 |
|
|
| `apt_repository` | `package` from the repository at `uri` with `suite` and `components`, trusting the key file `key_file` |
|
|
| `package_file` | The `.deb` at `url`, checked against `sha256`, for one `architecture`, after its apt `dependencies` are in |
|
|
|
|
Build steps run with the network off, so a `.deb`'s dependencies must be
|
|
listed. The bundle adds REA in a build-only layer: Node.js
|
|
24.22.1, downloaded per architecture and checked against its release checksum,
|
|
and REA from `REA_BUILD_COMPUTER_REA_ARCHIVE`, both unpacked into `/opt/rea` by
|
|
a root build step, with `node` and `rea` on `PATH`. An exported bundle carries
|
|
none of it.
|
|
|
|
From a bundle folder, such as one `export-desktop-application` wrote, REA
|
|
starts that bundle's `tasks/smoke.json` as it is:
|
|
|
|
```json
|
|
{ "source": { "kind": "bundle", "path": "/home/ada/mousepad-bundle" } }
|
|
```
|
|
|
|
REA runs the deploy with `run_bundle(keep=True, emulate=False)` on the local
|
|
sandbox provider, so only the host's own architecture is built. The first start
|
|
of a bundle builds its image and can take minutes; later starts reuse it. REA
|
|
then waits until the computer's readiness checks pass, opens one MCP session
|
|
to its driver and takes a screenshot, with the `region`, `format` and `quality`
|
|
given in `screenshot`. The result holds `computer_id`, agentenv's `sandbox_id`,
|
|
the environment name or bundle folder, and the screenshot's frame as structured
|
|
content beside its image.
|
|
|
|
A failed start returns the last 16 KiB of the helper's stderr, where a failing
|
|
build prints its error, and its exit status; a cancelled one stops the helper,
|
|
which tears down whatever agentenv brought up. When that teardown is not
|
|
confirmed, the error's `computer_id` names what REA keeps for
|
|
`close_build_computer` to retry. Closing the server cancels starts still in
|
|
progress and waits for them before closing every computer.
|
|
|
|
## Act
|
|
|
|
`act_on_build_computer` takes `computer_id`, an `action` and the driver's own
|
|
arguments for `computer_<action>`, which REA passes through:
|
|
|
|
| `action` | Arguments |
|
|
| ------------ | --------------------------------------------------------------- |
|
|
| `screenshot` | `region`, `format` (`png` or `jpeg`), `quality` |
|
|
| `click` | `x`, `y`, `button`, `count` (2 double-clicks), `modifiers` |
|
|
| `drag` | `path` of `[x, y]` points, `button`, `duration_ms`, `modifiers` |
|
|
| `scroll` | `direction`, `x`, `y`, `amount`, `modifiers` |
|
|
| `type` | `text`, where `\n` presses Enter and `\t` Tab |
|
|
| `key` | `keys`, one chord pressed in order, and `repeat` |
|
|
| `wait` | `ms` |
|
|
|
|
Pointer actions also take `frame_id`, `space` and `norm_scale`, and every
|
|
action but `screenshot` takes `screenshot: true` for an image afterwards. That
|
|
image is taken at once, so after an action that opens a menu or a window, ask
|
|
for it with a `wait` of a few hundred milliseconds instead.
|
|
Points are pixels of the latest frame, so take a screenshot first; with
|
|
`space: "norm"` they are on a `0..norm_scale` grid over it. Keys are W3C names
|
|
or one character; REA translates model spellings such as `ctrl+s`, `Return`,
|
|
`esc` and `cmd` and leaves other names for the driver to judge. An argument the
|
|
action does not take is refused before anything is sent.
|
|
|
|
The result carries the driver's content unchanged, its text and images, after
|
|
REA's own JSON: `action_index` and the driver's structured result. A refusal,
|
|
such as a stale frame, is an error carrying the computer's message. A response
|
|
larger than the MCP budget is refused with how to ask for a smaller image: a
|
|
`region`, or `jpeg` with a lower `quality`. That error keeps REA's own result,
|
|
`computer_id` included, as `details.result`, since the action or start has
|
|
happened.
|
|
|
|
Every action is logged on the computer in order with the arguments sent, its
|
|
start and end and whether it was applied, refused or failed. The log keeps the
|
|
latest 256 actions within 256 KiB of JSON, dropping the oldest first. Each
|
|
string argument, such as `type`'s `text`, is kept to its first 1 KiB and each
|
|
list, such as `drag`'s `path`, to its first 64 items; an entry names the
|
|
arguments it cut in `truncated_arguments`, and the driver gets them whole. The
|
|
log counts every action, so it says how many it dropped, and keeps the index of
|
|
the last applied action of each kind for rules over the whole session. The log
|
|
and the computer's recording hooks (`computer-recording/v1` start and end under
|
|
REA's own request id, and the segments fetched through the driver's `data/get`
|
|
one at a time, each read within its manifest size and checked against it) are
|
|
kept for later build stages; they are not tools. A recording larger than a
|
|
quarter of the V8 heap is refused before any segment is read.
|
|
|
|
## Close
|
|
|
|
`close_build_computer` ends REA's MCP session and closes the helper's stdin,
|
|
on which the helper runs agentenv's teardown. When agentenv confirms nothing is
|
|
left, the result says `teardown: "agentenv"`. Otherwise REA removes the
|
|
containers of that exact sandbox: agentenv runs a computer as
|
|
`agent-<sandbox id>` without a label and labels its other containers
|
|
`agentenv.sandbox=<sandbox id>`, so REA lists both with `docker ps -aq
|
|
--filter`, removes them with `docker rm -f`, and says
|
|
`teardown: "container_label"`. When neither confirms it, the error names what
|
|
is left and the computer stays listed, so closing it again retries. Closing the
|
|
MCP server closes every computer the same way, and a helper whose REA server
|
|
exits sees its stdin close and tears down.
|
|
|
|
The control token that guards the computer's framework-only routes stays in
|
|
REA: no result, error or log carries it. Code running in the computer as its
|
|
user can still read it, which is acceptable for a local build.
|
|
|
|
See the [specification](specs/desktop-app-environments/build-computer/spec.md)
|
|
for scope and verification.
|