mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 00:36:35 +00:00
* fix(desktop): release a capture when its start is cancelled after launch Cancelling start_build_capture during the opened app's screenshot left the capture running, so the next start was refused. The capture is now kept for the owner's cleanup, and the next start stops it first. * fix(desktop): keep a published witness error and tag an app data refusal A second failure to close a published witness returns the original error, so its record and cleanup lease stay bound. A reset the app itself refuses is an AppDataRefusal; only a reply that is not that refusal stays unsettled. A missing child of a cut view is undecided with the same description in both evaluators, and the shared cases cover it. * fix(desktop): align a cut view's missing text with the Python check A missing child of a truncated view is undecided for the same sentence in both evaluators, including the pointer the view does not contain. * fix(desktop): keep one published witness cleanup across retries A published record and its bound cleanup error stay together. A later close failure returns that error, and the unused path that built a fresh adapter error is gone. * fix(desktop): keep a bundle task read inside the witness result A cancelled, refused, or failed read of the bundle's task files returns the witness's Result, including a cleanup lease when a handle would not close. * test(desktop): pass the settle timing when reading a witness bundle The witness takes the settle timing before the bundle path, so the bundle-read tests keep the path in its own argument. * fix(desktop): bound a guest file answer and split a long controller argument A data/get whose base64 answer would exceed a quarter of the V8 heap is refused before it is read. Controller JSON that exceeds Linux's single-argument limit travels as several arguments the controller joins. * fix(desktop): keep an underreported guest file from raising its read A data/get answer is admitted only when its base64 body fits in the heap quarter after the copies held during parse. Decoded bytes larger than the computer reported are dropped. * fix(desktop): grade shipped seeds and keep selected settings and sqlite logs Seed checks follow the files the bundle ships, so an omitted lock is absent. An incomplete checkpoint is not reported as no change. A selected link keeps its spelling, and a database's wal and journal stay supporting state. Closing the fake computer stops a detached capture. * fix(desktop): keep added-record checks off ids, dates, and joined timestamps A position-named record's fields keep their paths, so an identifier, a stamped date, and text past the limit are not suggested checks. A whole-text check does not climb into an ancestor whose text joins a timestamp. * fix(desktop): bound home fetches and keep unsettled edits readable Split home-folder reads by the guest answer budget, keep a capacity error from the capture output, and drop the superseded task-argument refusal. Grade kept text per line, leave unretained dconf unread, and reserve by-position notes for XML children. Map a linked database's sidecars through the selected spelling and stop a detached capture only when its start identity still owns the pid. * fix(desktop): describe unretained dconf as unread The key comparison treats a missing copy as unknown. The comment still said that copy counted as an empty key list. * test(desktop): keep a capture capacity error through endedCapture An output past the guest answer ceiling stays the resource constraint, with its heap recovery and non-retryable status, instead of a generic capture-exit message. * fix(desktop): keep nested save times and formatting runs out of checks A field check drops save-time leaves before it joins the field, so a nested timestamp is not part of the suggested text. A text check beside a save time uses the paragraph when the diff names a formatting run. * test(desktop): read paragraph pointers from checks that have them * test(desktop): expect a large task to export intact The 96 KiB refusal is gone. A prompt past that size reaches the computer and comes back unchanged, instead of failing before export. * fix(desktop): keep real edits that save-side heuristics were dropping Repeated unnamed fields stay content checks. A formula that still has text is not renumbered with its ExprID. One gained style attribute is the edit; a repeated fill-in stays upkeep. Phone numbers and plain clock times stay text, and a decimal is the element's whole text. An older verifier matches on the task it states, not its evaluator source. * fix(desktop): cover repeated records and keep timestamps out of checks An added record whose words are already in the seed still gets the list's text when that text fits. Readiness is a start reason, so a final check names seed locks without matching the message. Field text split by a timestamp stays separate, and a timestamp line is not offered as a guard. * fix(desktop): compare a cell's joined decimal within tolerance The whole-element check keeps numeric tolerance, so 437.80 and a long float still match 437.8, including when a run splits the digits. A different number or other text does not. Both evaluators read an element's text for that comparison. * fix(desktop): grade a number from a complete XML element only A tolerance check joins an element's text only in an XML file or an XML archive entry, and leaves that check undecided when the view is cut, so a dropped descendant cannot turn the kept runs into a passing number. A JSON object with the same shape stays the value itself. Equivalent decimals and a split run on a whole cell still match. * fix(desktop): accept an older grader only when its index helper agrees The witness still matches a bundle whose evaluator is this REA's, or the older index expression alone when no path segment is a leading-zero number. A truncated verifier, a changed grader, or that older helper beside a segment such as 01 no longer counts as the same task. * fix(desktop): require the added record and leave save times out of list checks A record a selector names is checked itself, so its words have to be in that record. When an unnamed record only repeats words the seed already has, a list that holds a save time is checked by the text around that time. A later save of the same records still passes, and a missing or rewritten record fails. * fix(desktop): judge the older index helper by pointer segments only A leading zero in a file name or a URL does not change how the older index helper reads a pointer. The witness still rejects that helper when a pointer segment is a number such as 01. * fix(desktop): do not suggest list text the seed already passes A repeated record is shown by text the seed does not hold. The seed's own text is not a check. A pointer inside a check value is not a pointer segment for the older index helper. * fix(desktop): keep a linked settings file in the folder the link points at A selected ~/.config alias no longer moves MuseScore settings to that alias. The seed keeps settings/MuseScore or Documents, which is the path the export opens the note from. * fix(desktop): split home fetches by answer size and keep a size refusal A batch of tiny files stops before each path's envelope exceeds the host answer budget. A capture output past that ceiling stays a resource error instead of becoming a generic exit. * fix(desktop): seed a selected link under the path the app uses The selected spelling wins, so ~/.config/MuseScore is not rewritten to the folder it points at and the note stays the one launched document. A database wal and journal follow that database. A launch argument is respelled the same way. Teardown signals a detached PID only when its start identity still matches. * fix(desktop): keep a witness's actions and an incomplete checkpoint A failed witness save retries the action log it first recorded. A deletion left unconfirmed between the seed and the end is not reported as no change. Windows packs npm through a shell, which can start npm.cmd. * fix(desktop): read verifier pointers from the parsed checks A brace inside a check value is not an object boundary. A leading-zero pointer after that value still rejects the older index helper. * fix(desktop): prefer the most specific home spelling and retarget launch files A selected parent directory no longer hides a settings folder walked through a link. A launch filename under that folder is replaced with the seeded file's absolute path. * fix(desktop): pack the build runtime through npm's JavaScript entry npm pack and npm ci no longer spawn npm.cmd through a shell, so a temporary directory whose path contains spaces stays one argument. * fix(desktop): spell launch settings and keep a database log exact A database log matches only that file, so a neighboring folder is not moved. A launch option and the working directory use the same home spelling as the seed. * fix(desktop): move a state database's logs with the database The write-ahead log and journal follow the spelling the database received, including a walked state link. A neighboring folder stays put. A separate --config argument uses that spelling too. * fix(desktop): keep a selected file ahead of a database-log guess A file selected as itself keeps that spelling even when its name ends in -journal. Relative launch arguments are resolved in the captured working directory before that directory moves. * fix(desktop): keep outside-home database logs and option values A selected database outside the home keeps its write-ahead log. Launch options are rewritten only when they name a captured file or settings path. * fix(desktop): follow a database mapping and own limitation text A selected database keeps its log when a parent folder moves or the file sits outside the home. Trash and seed-lock tests compare the owning notice with the paths, and launch options that are not captured paths stay as written. * fix(desktop): retarget a settings folder that holds captured files A nested profile directory is a captured path when a seeded file is inside it. An option value that only resolves under the home stays as written. * fix(desktop): keep a program word and a directory's neighbor The program after env stays the program even when a captured file sits in a folder of that name. A selected directory is not a database, so a neighboring file keeps the parent folder's spelling. * fix(desktop): record whether a selected path is a directory Capture stores the walked path's kind. A neighboring file of a selected directory keeps the parent folder's spelling even when that directory never changes and its children are not content roots. * fix(desktop): mark containing paths without a pairwise scan A recorded path marks its known ancestors as directories in one walk up the path, instead of comparing every recorded path with every other. * fix(desktop): keep a selected folder that is named like a database log A folder the caller selected stays a selected root when its name ends in -journal or -wal. A generated log beside a database file still does not. * fix(desktop): refuse a build command that exceeds the argument vector Splitting one argument still leaves the whole command under Linux's E2BIG limit. A request past that budget is refused before the process starts. * fix(desktop): remap a captured path in an env assignment The program word stays the program. A NAME=value assignment keeps its name and follows a captured path value when that path's spelling moves. * fix(desktop): keep oversized text out of draft guards A value or line longer than the draft's text bound is not copied into a guard, so one large frame cannot fill the task by itself. * fix(desktop): keep an empty env value empty An empty assignment is not a path. Resolving it named the working directory and rewrote the value when that directory's spelling moved. * fix(desktop): check build runtime export capability * fix(desktop): provide complete runtime repack command * fix(desktop): retain runtime probe failures * test(desktop): identify capture independently of schema preflight * fix(desktop): preserve document URLs and value formats * fix(desktop): bound added checks and omit noisy text guards * 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 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): 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 * 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 * style(desktop): format the noisy-record draft files with the pinned oxfmt 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 * 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 * 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 * 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): 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 * style(desktop): format capture review fixes --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
208 lines
12 KiB
Markdown
208 lines
12 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. Capturing the
|
|
app, exporting its bundle and witnessing the task on such a computer are in
|
|
[Building and witnessing an environment](desktop-build-witness.md), and REA's
|
|
skill for doing all of it from a request is
|
|
[desktop builds](https://github.com/morluto/rea/blob/main/.agents/skills/reverse-engineer-anything/references/desktop-build.md).
|
|
|
|
## 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. From a
|
|
checkout, `npm run pack:build-computer-runtime -- /path/to/rea-prod.tar.xz`
|
|
writes one: `npm pack`, its package folder as
|
|
`rea-prod` with the lockfile, `npm ci --omit=dev --omit=optional`, then
|
|
`rea-prod` as `.tar.xz`.
|
|
|
|
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>` 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.
|
|
|
|
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 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. 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` |
|
|
| `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`. 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](specs/desktop-app-environments/build-computer/spec.md)
|
|
for scope and verification.
|