* fix(desktop): preserve document URLs and value formats * fix(desktop): bound added checks and omit noisy text guards * fix(desktop): exclude unseeded locks from task checks * fix(desktop): preserve draft bounds and launch URLs * fix(desktop): render draft values using their source format * fix(desktop): preserve launch URLs and JSON draft values * fix(desktop): recognize captured file URL documents * fix(desktop): render captured file URLs for runtime home * fix(desktop): distinguish phone numbers from save metadata * fix(desktop): preserve launch URLs and user text * fix(desktop): preserve captured URLs and user text * fix(desktop): preserve local file URL authority * fix(desktop): preserve local file URL authority * fix(desktop): preserve local file URL authority * fix(desktop): render captured file URLs at launch * fix(desktop): classify numeric timestamps using field context * fix(desktop): classify numeric timestamps using field context * fix(desktop): classify numeric timestamps using field context * fix(desktop): retain context for generated draft values * fix(desktop): retain context for generated draft values * fix(desktop): retain context for generated draft values * test(desktop): cover generated IDs in text guards * test(desktop): cover generated IDs in text guards * test(desktop): cover generated IDs in text guards * fix(desktop): isolate launcher URL encoding * fix(desktop): isolate launcher URL encoding * fix(desktop): isolate launcher URL encoding * fix(desktop): isolate launcher URL encoding * fix(desktop): retain clean text in noisy record fields * fix(desktop): classify added line values in one pass * fix(desktop): apply XML quote rules to text drafts * fix(desktop): distinguish XML attributes from quoted text * fix(desktop): preserve parsed XML line context * Format appTaskChecks.ts with the pinned oxfmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(desktop): retain partial XML syntax facts * feat(desktop): target agentenv-framework 0.9.1464 Desktop bundles, setup's --install-agentenv and the doctor's exact-version check now target 0.9.1464, which keeps an exported app's data/get call, holds the computer fixture above boot dialogs, leaves an unresolved setting's build unverified rather than failed, and mounts lock packages instead of copying them into the image. The web environment lane stays on 0.9.1431. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(desktop): recognize markdown autolinks in drafts * fix(desktop): reuse export exclusions in task drafts * fix(desktop): reuse export exclusions in task drafts * fix(desktop): report unchecked client build paths * docs(desktop): clarify gzip seed scrubbing * fix(desktop): report unchecked client build paths * docs(desktop): clarify gzip scrub limits * fix(desktop): reuse export exclusions in task drafts * docs(desktop): clarify gzip scrub limits * fix(setup): own desktop build process cleanup * fix(setup): own desktop build process cleanup * fix(doctor): compare desktop settings with resolved defaults * fix(doctor): compare desktop settings with resolved defaults * style(desktop): format the noisy-record draft files with the pinned oxfmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(desktop): give the root-array draft test its own describe The records describe grew past the 150-line lint limit with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(doctor): inspect client-recorded desktop runners * fix(doctor): inspect client-recorded desktop runners * fix(doctor): reuse matching runner probes * fix(doctor): reuse matching runner probes * fix(setup): propagate cancellation to desktop actions * fix(setup): propagate cancellation to desktop actions * fix(setup): stop Hopper on cancel, never report a cancelled setup ready, and keep a failed action's client setting - The Hopper install gets setup's signal: Linux already honoured it, and the macOS installer now stops its downloads and stops before mounting or copying. - runSetup checks the signal after the final doctor and close, so a cancellation there raises instead of returning ready. - A failed pack or agentenv install no longer writes its archive or runner to client configs, so the setting the client already had stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(setup): keep the completed setup result in a cancellation after cleanup A cancellation during the final doctor or host close carried only the applied actions, dropping each client's result and backup path and the doctor report. It now carries the whole result as setup_result, as doctor's cancellation carries its report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(desktop): move launch execution to boundary suite * fix(desktop): redact build schema failure output * refactor(desktop): normalize schema failure once * fix(desktop): omit unsafe schema parser snippets * fix(desktop): preserve draft change and selected path evidence * test(desktop): group retained dconf note regressions * fix(desktop): respect archive entry format in draft checks * test(desktop): separate unowned-file draft note cases * fix(desktop): preserve explicitly selected journal documents * fix(xml): avoid scanning selected ancestors per tag * fix(desktop): exclude covered text runs from draft counts * fix: preserve text when drafting desktop checks * fix: restore nested desktop state aliases * refactor: share UUID recognition across draft checks * fix: retain multiline XML attribute context * fix: retain relationship identifiers in task drafts * docs: clarify desktop draft identifier handling * test(desktop): type formatting run cases as pairs * fix(desktop): carry document format into selector evaluation * test(desktop): separate markup boundary regression group * fix(desktop): propagate formats through draft selectors * fix(desktop): switch selector format within archive entries * test(desktop): remove duplicate seed filename case * fix(setup): retain diagnostics when cancellation overlaps failure * refactor(setup): keep command cancellation at its owner * test(setup): verify cancellation at the application boundary * test(desktop): pass JSON format in exported index parity * docs(desktop): clarify task checks before scratch export * fix(desktop): respell selected paths in file URLs * test(desktop): group captured document spelling cases * fix(desktop): bound draft preview serialization * fix(desktop): restore selected state aliases * fix(desktop): respect entry format in draft noise * test(desktop): match exported alias evidence at its JSON boundary * fix(desktop): retain app state logs in exported seeds * fix: align desktop numeric seed comparisons * test(desktop): keep one draft exclusion fixture import * test(desktop): retain log eligibility across draft test groups * fix(doctor): resolve empty registered desktop runner * test(desktop): use the framework pin in doctor fixtures * style: fix desktop launch test formatting * style: format native command path schema * fix(desktop): map selected external launch paths * style(desktop): format seed document regressions * test(desktop): group launch URL regressions * fix(desktop): omit redundant nested state aliases * fix(desktop): compare added free text as a whole * fix(desktop): check recompressed seeds as shipped * docs(desktop): clarify recovered seed format * test(desktop): group shipped gzip seed checks * fix: retain quoted identifier text in task drafts * fix: recognize Unicode XML in desktop state readers * fix(desktop): bound nested draft checks * fix(desktop): explain missing arrays in truncated views * fix(desktop): find XML type references in archives * test(desktop): remove duplicated capture assertions * fix(desktop): retain nested omitted text boundaries * style(desktop): format draft value imports * test(desktop): group model and draft value checks * fix(desktop): map captured launch paths once * refactor: remove unused whole-text guard state * refactor: stop passing unused text children * test: align free-text checks with whole-note policy * fix: validate recompressed desktop archive seeds * style(desktop): format archive seed regression * fix(desktop): retain placed-item sibling guards * fix(desktop): preserve archive paths and bound doctor reads * fix(doctor): refuse non-files before descriptor admission * fix(setup): track remaining cleanup resources across retries * test(desktop): specify XML format in heap pairing fixture * fix(doctor): query agentenv for model endpoint * fix(doctor): preserve uncertainty for empty endpoint override * style(desktop): format framework pin fixture * fix(desktop): redact runtime schema errors * fix(desktop): normalize captured file URLs * fix(desktop): report seeded files by capture path * fix(desktop): retain startup readiness failure stage * fix(desktop): retain JSON date checks * fix(desktop): preserve archive formatting checks * fix(desktop): preserve explicitly selected journal files * fix(desktop): report build command transport limits * docs(desktop): clarify unreadable dconf versions * test(desktop): remove duplicate history regression * fix(desktop): align XML names with exported reader * fix(desktop): create selected state alias targets * fix(desktop): inspect supported runtime archives * fix(desktop): retain record identity in draft checks * fix(desktop): inspect supported runtime archives * fix(setup): retain completed actions on cancellation * docs(desktop): clarify document lock exclusion * fix(desktop): scrub XML independently of reader compatibility * fix(desktop): preserve file aliases and reader diagnostics * style(desktop): format review fixes * style(desktop): format capture review fixes * fix(desktop): retain merged installer behavior --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
18 KiB
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. Capturing the app, exporting its bundle and witnessing the task on such a computer are in Building and witnessing an environment, and REA's skill for doing all of it from a request is desktop builds.
Set up a build host
On a Linux host with Node.js and Docker, from a REA checkout:
rea setup --client claude_code --desktop-build --install-agentenv
rea doctor --provider desktop_build --client claude_code
Setup shows its plan and asks before it changes anything; --dry-run only
prints it and --yes approves it. It packs REA's runtime archive into
~/.cache/rea/build-computer/rea-<version>.tar.xz (under $XDG_CACHE_HOME
when set, ~/Library/Caches on macOS), and packs again once REA's version
changes; an archive REA_BUILD_COMPUTER_REA_ARCHIVE already names is
recorded instead. It records REA_AGENT_ENV_COMMAND, REA_DOCKER_COMMAND and
REA_BUILD_COMPUTER_REA_ARCHIVE as absolute paths in that client's REA
registration, keeping a value the registration already has other than an
archive setup packed for an earlier REA version, and refreshes the skill.
--install-agentenv runs uv tool install agentenv-framework==0.9.1464
when uv is on PATH, and otherwise prints the command. Setup never
installs Docker or Node.js and never runs sudo. Restart the client
afterwards.
Doctor is read-only and checks, in order: a Linux amd64 or arm64 host,
Node.js, procps ps, the Docker CLI, a Docker daemon reachable without
sudo, 20 GB free where Docker keeps images, agentenv-framework 0.9.1464,
the runtime archive of this REA version, the build settings in the
registration or the environment, and the skill. Each failure names its fix.
It also reports, without counting them against healthy, whether a model
endpoint for solver runs is configured and which agentenv containers
earlier builds left. Pass --json for the full report.
When a setting is unset where the server starts, REA uses agent-env and
docker on PATH and the archive setup packed for this REA version. The
rest of this page is the same setup by hand.
Requirements
- A Linux host on amd64 or arm64 with about 20 GB of free disk for the images agentenv builds and pulls. amd64 and arm64 hosts both build natively; a host builds only its own architecture.
- Node.js at the version in
.nvmrc, installed with nvm or from nodejs.org, and REA run from a checkout. - Docker Engine and its
post-install steps:
add yourself to the
dockergroup, then log in again. - agentenv-framework 0.9.1464, the version bundles target. Install
uv first, then
run
uv tool install agentenv-framework==0.9.1464. SetREA_AGENT_ENV_COMMANDto the absolute path of the resultingagent-envprogram, normally$HOME/.local/bin/agent-env;command -v agent-envshows the path when its install directory is on yourPATH. Installing it with pip into a virtual environment instead needspython3.12-venvon stock Ubuntu. REA runs its helper,bridge/agentenv/computer.py, with that runner's Python: the interpreter on the program's#!line, or thepython3beside 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 holdsscripts/rea.mjs. The computer runs REA from it. From a checkout,npm run pack:build-computer-runtime -- /path/to/rea-prod.tar.xzwrites one:npm pack, its package folder asrea-prodwith the lockfile,npm ci --omit=dev --omit=optional, thenrea-prodas.tar.xz. The archive is a copy of the checkout: pack it again aftergit pulland a rebuild.
Set these where the MCP server starts; a shell export does not reach a server that is already running.
Claude Code
Without --desktop-build, rea setup --client claude_code registers REA
without these variables. Either
export them in the shell that starts claude, which starts REA's server with
its environment:
export REA_AGENT_ENV_COMMAND="$HOME/.local/bin/agent-env"
export REA_DOCKER_COMMAND="$(command -v docker)"
export REA_BUILD_COMPUTER_REA_ARCHIVE="$HOME/rea-runtime/rea-prod.tar.xz"
claude
or set them, as absolute paths, in the env of the mcpServers.rea entry
setup wrote, such as in ~/.claude.json:
{
"mcpServers": {
"rea": {
"command": "/absolute/path/to/node",
"args": ["/absolute/path/to/rea/scripts/rea.mjs", "mcp"],
"env": {
"REA_AGENT_ENV_COMMAND": "/home/ada/.local/bin/agent-env",
"REA_DOCKER_COMMAND": "/usr/bin/docker",
"REA_BUILD_COMPUTER_REA_ARCHIVE": "/home/ada/rea-runtime/rea-prod.tar.xz"
}
}
}
}
Solver runs are agent-env run commands the agent runs in its shell, and
agentenv refuses them without a model endpoint: export LITELLM_BASE_URL and
LITELLM_API_KEY in the shell that starts claude, whose Bash tool inherits
them, or set base_url and api_key under [model] in
.agentenv/config.toml.
Claude Code asks before each MCP tool call its permission settings do not
allow, and a build makes dozens of act_on_build_computer calls. You can
allow REA's tools in those settings' permissions.allow list:
mcp__rea__act_on_build_computer allows that tool, and mcp__rea allows all
of REA's.
A headless run, claude -p, ends when the skill asks its upfront questions,
and refuses the tool calls its permission settings do not allow. Put the
answers in the request: the install source when the app is not in the Ubuntu
archive, a synthetic seed or the file to start from, whether to review the
task before solver runs, and the solver runs' count, model and budget, or
none:
claude -p 'Build me a Gnote environment where the agent adds olive oil to the
shopping list note. Make a synthetic note. Then make 2 solver runs with
<model>, at most $4 in all, without a review first.'
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> from the computer
user's home, so its open and save dialogs start there rather than in the
framework's folder for the app, and keeps no seed and no data hooks. The app is not open when the computer starts:
start_build_capture opens it under REA's capture. When no capture is under
way, open it from the desktop's Applications menu, where agentenv lists it
under Other by the environment's name, such as mousepad; that entry launches
it through the app's server. The name is the desktop entry's last part when it
is reverse-DNS (com.github.xournalpp.xournalpp gives xournalpp), or else
the whole entry without a trailing version (glabels-3.0 gives glabels).
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.
{
"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, with its optional extra_packages beside it |
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. extra_packages names up to 16 more archive packages, by the same
name rules as package, for a runtime piece the app needs that its package
does not install: apt here skips Recommends, so MyPaint 2.0.1 exits at launch
without librsvg2-common, GTK's SVG icon loader, and Freeplane 1.7.10 needs
openjdk-17-jre, since its launcher refuses noble's default Java 21. The
export installs them beside the app too, as it does a .deb's
dependencies when apt offers the .deb's package; see
build and witness.
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. It also installs
libgtk-3-bin, for gtk-launch, and dconf-cli, so the capture reads the
app's settings in dconf, such as GnuCash's "Show tips at startup", and the
seed keeps them. An exported bundle carries none of it but dconf-cli, which
it installs when it loads the app's dconf keys.
From a bundle folder, such as one export-desktop-application wrote, REA
starts that bundle's tasks/smoke.json as it is:
{ "source": { "kind": "bundle", "path": "/home/ada/mousepad-bundle" } }
REA runs its helper with PYTHONDONTWRITEBYTECODE=1, which agentenv's own
import of the bundle's app class inherits, so a start leaves no __pycache__
in the folder and its files stay as they were shared.
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.
agentenv makes a start wait while another agentenv run on this host writes its
bundle's ids, so starts at the same time wait for each other, several minutes
when a few run together; the progress line then says it is waiting for another
agent-env run to finish writing this bundle's ids.
While it waits, REA sends an MCP progress notification every five seconds with
agentenv's latest line, so a client that resets its request timeout on
progress keeps waiting. REA then waits until the computer's readiness checks
pass and opens one MCP session to its driver. Readiness can pass before the
desktop has drawn, so REA takes a whole-screen PNG screenshot every 500 ms
while it is one colour throughout. The retries share 10 s, each given only
what is left of it, and when that runs out REA returns the latest frame;
progress notifications continue meanwhile. Given screenshot with a
region, format or quality, REA then takes one with them, a region
cropping that frame rather than capturing the screen again. The result holds
computer_id, agentenv's sandbox_id, the environment name or bundle folder,
extra_packages, what the request named beside the app for the export to
install too (an archive install's extra_packages or a .deb's
dependencies, empty otherwise), 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 2 to 50 [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 |
reset |
screenshot |
reset is not a driver tool: REA calls the app's data/reset, which restores
the bundle's seed and relaunches the app, then waits until the computer's
readiness checks pass again, sending an MCP progress notification every five
seconds while it waits. With screenshot: true it then takes a screenshot; if
that fails, the reset still counts as applied and its result has no frame
but a screenshot_error with the computer's message. A computer started from
a request has no data operations, so it refuses a reset.
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. A screenshot with region does not
capture the screen again; it crops the latest frame, so take a full
screenshot, or a wait with screenshot: true, first. That error keeps REA's own result,
computer_id included, as details.result, since the action or start has
happened.
Actions, the capture steps and the task checks on one computer run one at a time, in the order REA receives them; one that has to wait for the call before it sends an MCP progress notification every five seconds meanwhile. Different computers run side by side.
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
what the witness records; they are not tools of their own. 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. A close
cancels the call in progress on the computer, refuses the calls waiting for
it, and tears down once that call has ended. 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 for scope and verification.