Repository navigation
feat(desktop): add a validated task to exported desktop applications - #8940
earakely-scale wants to merge 82 commits into
Conversation
Add `capture-desktop-application`, a CLI-only, Linux and X11 capture that launches an installed desktop application, watches it while it is used and writes capture Evidence plus SHA-256 addressed content: - inventory: desktop entry, launch command, executable, package and its apt sources (or snap, flatpak, AppImage), Electron markers and candidate state directories; - session: the launched process tree, client windows from xprop with the first application window time and a window match, and files held open; - state: a stat walk of the home directory before launch, at checkpoints read from stdin, at stop and after exit, with created, modified, deleted and retyped paths and the content of changed files and state directories. It refuses non-Linux hosts, missing DISPLAY or xprop, a non-empty output folder and an application whose window is already open, and signals only processes it started. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cancelled captures - Launch through the owned provider path with a run token; release with SIGTERM, then the token-verified group cleanup, after every session. Processes without the token are never signalled; each records whether it carries it, and the session records whether cleanup was verified. - Report creations and deletions only where a walk listed the nearest existing directory; count the rest as unconfirmed. - Retain a file only when it is still the walked version, through owned no-follow reads; charge the budget only for new content; create files exclusively in a private output folder owned by the caller. - Walk content roots outside the home directory, compare paths in their physical spelling, and run executables from the caller's directory. - Refuse when the window list cannot be read, re-evaluate window ownership each sample, handle one queued request per sample with a bounded stdin queue, and write the capture with end reason `cancelled` on interruption. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Allow a capture cancelled before any requested checkpoint to hold only the baseline. - Retry an incomplete stop once; if cleanup is still unverified, write the capture and fail with the run token and process group as cleanup resources. - Classify path changes after a walk (ENOTDIR and other selected-file errors) as changed_since_walk instead of failing the capture. - Treat a non-directory ancestor as proof that nothing existed beneath it. - Report a before digest only for the file version it was read from. - Derive stderr_truncated from stderr's own byte counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Content reads pass retainFailedClose, so a handle whose close fails stays owned by the content store; the capture retries those closes after it is written and fails with the output folder as the cleanup resource if one still cannot be closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add inspect-desktop-application-state, a CLI command that reads a capture folder into the application's state: every retained or changed file with its role and reasons, a typed JSON view of each version (SQLite through the shared worker, dconf through a private dconf dump, JSON, XML, key files, text) and the kinds of change between versions (lines, values, keys, rows). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When both the launched processes and a retained file's handle remain unresolved after the capture is written, one cleanup error now lists both reasons and both resource sets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add export-desktop-application, which writes an agentenv bundle with one computer_app environment from a capture folder and its state view: the apt package, a boot launch matched on the captured window class with a window ready check, owned paths from the state roles, a seed of the baseline files, data hooks that call agentenv's add/get/reset helpers with a typed JSON view on get, a skill, a README and a deploy smoke task. The desktop CLI commands now load their services lazily. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Read key files, XML attributes, dconf groups and SQLite tables into dictionaries without inherited properties, so captured keys such as __proto__ stay data. - Version databases with their write-ahead log as of each checkpoint, and stage only digest-verified bytes for the SQLite and dconf readers. - Detect the format per version, report format changes, keep JSON null and empty containers as values, keep XML text and preserved whitespace, and mark truncated views (including XML depth) and comparisons of them. - Number versions and changes by checkpoint index, decide baseline presence from the captured type, and keep tagged SQLite integers, text bytes and blobs distinct. - Carry the capture's limitations and omitted content forward, match state directories by their home path, index changes once per capture, keep failed handle closes owned, and finish the CLI through the shared owner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cation-export # Conflicts: # src/application/desktop/DesktopApplicationState.test.ts # src/application/desktop/DesktopApplicationState.ts # src/application/desktop/desktopCapture.fixture.ts # src/cli/desktopCaptureCommands.ts
- Read coverage flags only from formats whose readers write them, so JSON fields named truncated stay data. - Fail a SQLite version whose write-ahead log existed but was not retained or does not match its digest, instead of reading the database alone. - Close retained content on every exit, including cancellation, and report a close that still fails together with the primary failure. - Finish the CLI with the canonical operation name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cation-export # Conflicts: # src/cli/desktopCaptureCommands.ts
- Refuse packages not from the Ubuntu 24.04 archive, since the computer installs by name from that archive. - Start the launch command in the captured working directory. - Respell the home path only in files the state view read as text. - Report files that were not retained instead of dropping them silently. - Load captured dconf keys for apps that own no files. - Keep home-level files as owned paths; rename reserved Python class names. - Mark truncated SQLite and text views in the class's get view, and read file types the way the state view does. - Release the output tree through a resource scope, and read seeds through an owner closed on every exit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…L values - Stage database bytes in a private runtime root released through a resource scope, so a failed removal keeps its owner and is reported with the read's outcome. - Report cleanup facts and owners from desktop failures instead of wrapping them as a plain adapter error. - Record the state directories as walked, in their physical spelling, and match roles against both spellings, so files in a symlinked state directory are not mistaken for selected documents. - Keep whitespace-only XML leaf values; drop only formatting between elements. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep whitespace-only leaf values and xml:space="preserve" text in the generated get view; drop only formatting between elements. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…captures - Release retained-content handles whose close failed through a resource scope on every exit; a release that fails again reports the exact handle owners. A read that finished keeps its bytes when only its close failed. - Accept captures written before state directories were recorded, and say in the state view's limitations that symlinked state directories may be misclassed for them. - Compute each XML element's whitespace context once instead of scanning siblings for every text run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Try the captured working directory only when an argument is relative, and start the app even when that folder is missing. - Cut the generated XML view at REA's node and depth limits, and keep one unreadable file from failing data/get. - Accept Ubuntu 24.04 mirrors, not only the canonical hosts; PPAs and other repositories are still refused. - Read seeds through the retained-content owner's new signature. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Stage a retained -journal beside its database so the SQLite worker accepts an inactive journal and refuses a hot one; a -wal or -journal that existed but was not retained (including one omitted at the baseline) makes the version unreadable. - Keep tables that read when another fails, and let cancellation and unfinished cleanup propagate instead of becoming view errors. - Read text as a key file only when every line parses as one. - Rank a file named in a window title or selected by the caller as a document even when the application holds it open; match titles on file-name boundaries; leave the display name out of dconf matching. - Bound JSON and key file views (4 MiB, 500 levels, exact numbers) and report too-large, missing and altered retained content separately. - Add no version or change while a database stays absent. - Read staged databases in place instead of copying them again. - Give Evidence the same limitations as the result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Load captured dconf keys without D-Bus: install dconf-cli, compile the existing and captured key files into the user database, and warn on failure instead of stopping the server. - Start relative-argument launches in the captured working directory, the home included, since the framework launches from the app folder; respell the home itself and option values under it. - Read views in a thread, skip files over 64 MiB, align the generated readers with the state view's, and close SQLite connections. - Never own the home or a shared base folder; map a symlinked state directory's files back to their home spelling. - Report omitted files and unretained opened folders. - Name the module so it cannot shadow another, and keep the class off the module's own names; derive names the bundle check accepts. - Write seeds as they are read, and remove an unpublished bundle on failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Ask the launch process group and token carriers to exit before killing them, on errors too; report incomplete cleanup with its facts. - Count descendants of the launch as started by the capture even when their environment cannot be read; treat a reused pid as a new process. - Follow symlinked desktop entries; name a flatpak's state after its application id; match flatpak windows by name only. - Stop on end of input; refuse empty, duplicate and reserved checkpoint names; require whitespace after the checkpoint keyword. - Stay on each walk root's device, and never walk or retain `/` or a directory containing the home; say so in the limitations. - Refuse a window manager that does not publish _NET_CLIENT_LIST. - Remove retained content when a capture fails before it is written. - Skip reading a file larger than the remaining content budget. - Record apt's archive origin for each package source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lication-state # Conflicts: # docs/desktop-application-capture.md # src/cli/desktopCaptureCommands.ts
xfwm4 leaves _NET_CLIENT_LIST unset while it manages no windows, so the capture refused an ordinary desktop with nothing open. Read _NET_SUPPORTED with the list: an unset list means no windows when the window manager supports it, and the capture is refused only when it does not. `xprop`'s "no such atom" output now reads as unset too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A selected path that is not a directory brings its -wal and -journal as content roots, so the capture records whether they exist and retains them. - The state view treats a file as absent only under recorded content roots and refuses a database whose log presence is unknown. - JSON changes list containers too, so an array that becomes an object with the same members is a change. - Key file entries escape the key as a JSON pointer segment, so [a/b] c and [a] b/c stay apart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An unreadable window list is unknown, not zero windows, and is named in the limitations. - Before ending the session, live run-token carriers no parent link reached are tracked; an unreadable process table leaves the exit unknown. - An env prefix's assignments find the program and name its XDG folders. - flatpak run options that take a value are skipped with it, -- is honoured and a /arch/branch suffix is dropped from the id. - State directories include the entry's full id. - Path options refuse empty values; roots count toward the entry limit. - A capture file whose close failed stays owned and is retried on discard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…into feat/desktop-application-state
…to feat/desktop-application-export
- Sources files parse into one stanza per line or deb822 paragraph with its URIs and suites kept as lists, so no URI x suite product is built. - Repository architectures and release are optional, so captures written before they were recorded parse; the export treats them as unrestricted and unknown. - Library sources are matched against every index of a declared repository's URI, suite and components. - Two failures side by side keep both causes and both cleanup owners, and a host file whose close fails reports a lease on its handle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nto feat/desktop-vendor-packages # Conflicts: # src/domain/desktop/desktopEntry.test.ts # src/domain/desktop/desktopEntry.ts
… feat/desktop-app-tasks
…hrough members - The draft's key file lookup uses the state view's escaped `group/key`, so a key holding `/` or `~` still gets its suggestion. - JSON and XML changes now list containers too; the draft suggests an added or removed container through its listed members, and a container only when none of its members is listed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A program, or an empty or relative PATH entry, is resolved from the folder the program starts in, as the launch does, so `env PATH=. app` with the entry's Path still finds the app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JSON and XML changes are found by walking both trees with one shared pointer path, building a pointer only for a listed value, so memory follows depth rather than every container's full path. A pointer over 4096 characters is counted but not listed. A 256-level document with 4000-character keys went from about 251 MiB of heap to under 1 MiB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The exported reader parses the whole text before an inexact number makes it JSON with an error, so JSON Lines with an integer above 2^53 stay text as in REA. Over-long integer spellings are unsafe without converting them. The guide no longer claims large integers keep their value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…into feat/desktop-application-state
…to feat/desktop-application-export
The CLI alias check reads `-wal` and `-journal` in help text as short flags, so the help names the write-ahead log and rollback journal instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Source URIs lose any user name and password as they are parsed, the way apt splits them, and so do index-target URIs and descriptions and the sources apt-cache policy lists, so matching works whether or not apt prints them. - A repository whose entry held credentials is marked `uri_credentials_removed`, and the export refuses it, or an older capture whose URI still holds them, with a reason that names no URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…to feat/desktop-application-export
…nto feat/desktop-vendor-packages # Conflicts: # docs/desktop-application-export.md
… feat/desktop-app-tasks
Program paths and PATH entries are joined rather than resolved, so `..` after a symbolic link means what it means to the launch. The lookup moves to its own module. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The exported reader only needs to know whether any number was inexact, so it keeps a boolean instead of every rejected spelling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| const describe = (repository: AptRepository): string => | ||
| repository.uri === null | ||
| ? repository.source | ||
| : `${withoutCredentials(repository.uri)} ${[repository.suite, ...repository.components].join(" ")}`; |
There was a problem hiding this comment.
Older captures expose passwords
The new credential scrub still returns raw repository.source when uri is null. Older supported captures can hold a username and password here when no sources entry matched. That password reaches export errors, or skipped and returned Evidence when another repository is usable. The no-repository error also prints raw installed.sources.
This violates the repository requirement to redact transport credentials. Normalize these older fields at the capture-reading boundary too.
How this was verified: Older source strings pass capture admission unchanged, and the raw fallback flows into export errors and Evidence without credential removal.
Context Used: AGENTS.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/domain/desktop/appExportInstall.ts
Line: 58-61
Comment:
**Older captures expose passwords**
The new credential scrub still returns raw `repository.source` when `uri` is null. Older supported captures can hold a username and password here when no sources entry matched. That password reaches export errors, or `skipped` and returned Evidence when another repository is usable. The no-repository error also prints raw `installed.sources`.
This violates the repository requirement to redact transport credentials. Normalize these older fields at the capture-reading boundary too.
**How this was verified:** Older source strings pass capture admission unchanged, and the raw fallback flows into export errors and Evidence without credential removal.
**Context Used:** AGENTS.md ([source](https://github.com/morluto/rea/blob/main/AGENTS.md))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| for (const directory of directories) { | ||
| const candidate = resolve(startedIn, directory, program); | ||
| if (await isExecutable(candidate)) return candidate; |
There was a problem hiding this comment.
Relative executables fail after export
resolveProgram now accepts an executable relative to the application's working folder, but export still launches the original argv. For an apt-installed app with Path=/opt/app and Exec=./app, with no extra arguments, capture succeeds. The exported launchCommand checks only arguments after the executable when deciding whether to restore that folder. It therefore runs ./app from the environment folder and never reaches ready.
Use the resolved executable for export, or account for a relative executable when restoring the working folder.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/desktop/LinuxApplicationInventory.ts
Line: 174-176
Comment:
**Relative executables fail after export**
`resolveProgram` now accepts an executable relative to the application's working folder, but export still launches the original `argv`. For an apt-installed app with `Path=/opt/app` and `Exec=./app`, with no extra arguments, capture succeeds. The exported `launchCommand` checks only arguments after the executable when deciding whether to restore that folder. It therefore runs `./app` from the environment folder and never reaches ready.
Use the resolved executable for export, or account for a relative executable when restoring the working folder.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.A capture written before credentials were removed counted the credentialed repository's libraries as installable, so the export refuses it whole and asks for a new capture rather than declaring packages no source offers. Recorded policy sources are redacted in export messages too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…into feat/desktop-application-state # Conflicts: # src/application/desktop/DesktopCaptureRefusals.ts
…to feat/desktop-application-export
…nto feat/desktop-vendor-packages # Conflicts: # src/desktop/LinuxApplicationInventory.ts
… feat/desktop-app-tasks
A login on a real host reads as a secret to the commit scanner, so the older capture's fixture uses apt.example.com like the other credential tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… feat/desktop-app-tasks
| : (environment.PATH?.split(delimiter) ?? []).map((directory) => | ||
| directory === "" || directory === "." | ||
| ? `${startedIn}/${program}` | ||
| : `${from(directory)}/${program}`, |
There was a problem hiding this comment.
With /snap/bin/ on PATH, this join returns /snap/bin//firefox, which neither snap pattern in packageFromPath accepts. If the launcher is the standard symlink to the snapd executable, identifyPackage falls through to Debian package lookup and records snapd instead of the Firefox snap. Export can then install snapd and try to launch a missing app.
Keep the symlink-aware lookup, but make package detection accept equivalent separators and . segments without losing the snap launcher's identity.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/desktop/ProgramLookup.ts
Line: 29-32
Comment:
**Snap apps become `snapd`**
With `/snap/bin/` on `PATH`, this join returns `/snap/bin//firefox`, which neither snap pattern in `packageFromPath` accepts. If the launcher is the standard symlink to the snapd executable, `identifyPackage` falls through to Debian package lookup and records `snapd` instead of the Firefox snap. Export can then install `snapd` and try to launch a missing app.
Keep the symlink-aware lookup, but make package detection accept equivalent separators and `.` segments without losing the snap launcher's identity.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Summary
An exported desktop app can now carry one task: a prompt for a computer-use agent, and declarative checks that grade the files the agent leaves in the app. An agent writes the prompt and the checks from the change the capture recorded. REA validates the task against the capture and keeps it only when its checks fail on the seed and all pass on the captured end state. The bundle then holds the task, a verifier that needs no model, and an eval that runs it.
The spec, plan and verification are in
docs/specs/desktop-app-environments/tasks/, and the user guide isdocs/desktop-application-tasks.md.Problem and expected behavior
export-desktop-applicationgives a computer that runs the app on its seed, but nothing for an agent to do in it and nothing to grade. The change the user made during the capture is the natural task. But REA cannot turn it into a prompt: what the user meant is a judgment, and REA runs no model.After this change:
describe-desktop-application-tasklays out the change: each owned file that differs between the capture's first and last version, with both views and the net change. It also drafts checks.export-desktop-application --task task.jsonaccepts the task only if it is a real task for this capture.Change and scope
Task file (
src/domain/desktop/appTask.ts):{name, prompt, checks}with 1 to 32 checks of ten kinds:file_presentandfile_absent;lines_present,lines_absentandline_count(lines equal toexactor holdingcontains; equals, at_least or at_most);value_equalsandvalue_absentby JSON pointer;row_present,row_absentandrow_counton SQLite tables.Refused at parse time:
Evaluator (
appTaskChecks.ts): evaluates the checks on the typed views the state view already produces.value_equalsof a whole object or array.Verifier (
appTaskPython.ts): rendersverify.py, the same evaluator in Python with the checks inlined.data/getunder the run's control-token grant.describe-desktop-application-task <capture>(new command): returns the application, its owned paths, the changed files with their net change (lines,values,keys,rows, orcreated,deleted,format,content,unreadable), and a draft task with suggested checks.export-desktop-application --task <file>:tasks/<name>.json,artifacts/<name>-check/verify.pyandevals/<env>.toml, and the README describes the task and a solve-rate recipe.Not included:
data/getserves.Contract and boundary impact
describe_desktop_application_taskoperation (predicate typerea.desktop-application-task-draft), then the task stage ofexport_desktop_application.describe-desktop-application-task;export-desktop-application,--task, given at most once (a second is refused);task.deploy_env,deploy_agent,add_skills,start_computer_recording,prompt_agent,env_outcome_verifier,end_computer_recording).data/get.Expected effects:
Evidence and regression coverage
Tests added:
verify.pyunderpython3 -Iwith a stub protocol client. The two agree on every result and description.regexfield.-walchanged), and a file whose truncated views match while it changed. It leaves out a file with one version.Mousepad, captured fresh (
~/Documents/todo.txt, "buy milk" added under "buy bread"): the draft listed the document and suggestedlines_present"buy milk":{"name": "change", "prompt": "REPLACE: what the user asks to have done in the app, in their own words.", "checks": [{"id": "check-1", "kind": "lines_present", "file": "Documents/todo.txt", "lines": ["buy milk"]}]}A task with three checks (
milk,bread, andone-milkcounting exactly one "buy milk" line) was exported:{"name": "add-milk", "checks": 3, "failing_on_seed": ["milk", "one-milk"]}Tasks checking only "buy bread", or checking "buy eggs", were refused with no bundle written:
Check "check-1" fails on the captured end state: Documents/todo.txt has no line "buy eggs".LibreCAD (four holes added to a plate, which the save rewrote from DXF R12 to 2007): the draft suggested counts of
SECTION,ENDSECandCIRCLE. A task withCIRCLEequals 9 was exported: 5 on the seed and 9 at the end.Typora, captured fresh (a to-do "Buy trail snacks" and a table row "Sun | Lake swim" added to
~/Documents/trip.md): among about 25 changed files under.config/Typora, the draft suggested only the document's two new lines. A task checking both, with "- [ ] Book the cabin" kept, was exported withsnacksandsundayfailing on the seed. Tasks checking only the cabin, or a kayak to-do, were refused.In an agentenv computer (agentenv-framework 0.9.1431, local Docker, arm64,
--no-emulate):agent-env run --dry-runplanned the eval's one task with Claude Code as the solver.deploy_env, then the exported verifier) scored 0 on the seed. It recordedmilkandone-milkfalse andbreadtrue.computer_click,computer_keyandcomputer_typeadded "buy milk" in Mousepad and saved it. The exportedverify.py, called under the control-token grant as the step calls it, then passed all three checks.one-milk. Afterdata/reset, the checks read as on the seed again.data/resetrestored the seed verdict.Solve rate (the documented recipe,
--agent claude-code --model anthropic/claude-sonnet-5-5, three runs per task on bundles re-exported from the final code):add-milkplan-sundayadd-holesRecordings of the runs. These are the computer's own screen recordings (10 fps, cursor shown), with each run's segments joined and nothing else edited. Below is the first run of each task; the table links all nine.
add-milkplan-sundayadd-holesRemaining proof gaps:
verify.pyfrom beforebced17cf2. That commit changed onlyline_countwithcontainsandvalue_equalsof whole values on truncated views, both covered by the cross-language tests.For evidence-bearing changes:
Validation performed
npm run typecheck,lint,lint:typed,format:check,build:cached,knip,jscpd,docs:check,docs:build,verify:test-discoveryandverify:architecture-guardspassed. There are no lint diagnostics in the touched files.cliInputSchemaRequired,cliToolCoverage,cliStartupImports) passed.The hand-written diff is about 1,300 lines of source, 1,400 of tests and fixtures, and 400 of docs. The task file, its evaluator, its Python rendering and the export's validation only make sense together, so this is kept as one PR.
Compatibility, safety, and release
--taskis unchanged.verify.pyis rendered from them with values inlined as JSON.data/get, under the grant the run gives it.Author readiness
Human owner: @earakely-scale
Before merge
type(scope): outcome.🤖 Generated with Claude Code