Files
reverse-engineering-anything/docs/specs/desktop-app-environments/capture/spec.md
T
df2d018b30 feat(desktop): read gzip documents as the document inside (#9111)
* 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>
2026-11-09 20:48:08 +08:00

8.3 KiB

Capture a Linux desktop application in use

Outcome

rea capture-desktop-application launches an installed Linux desktop application on the user's X11 display, watches it while a person or a driver uses it, and writes a capture folder: Evidence in capture.json and the bytes of retained files in content/<sha256>. The Evidence says what the application is and how it was installed, how to launch and recognise it, when its window appeared, and how files under the home directory changed between checkpoints, with the content of the files involved. Later steps read this folder to build the environment; this step builds nothing.

Requirements

  • The baseline records each observed non-file path and its type without following symlinks, so a later state reader can distinguish an absent SQLite sidecar from an observed non-file.

  • The command is CLI only. A capture is a long interactive session whose checkpoints come from stdin lines (empty, checkpoint [name] or other text) and which ends on stop or the end of stdin, when the launched process has exited and the application's processes and windows are gone, or at the time limit. One queued line is handled per sample, and stdin reading pauses while 16 wait. A repeated name or one of the capture's own names (baseline, stop, time-limit, exit) is refused on stderr and the session continues. The application has exited only after two consecutive samples with the launched process gone, no process carrying the run token left, and a window list that was read and holds none of its windows; an unreadable list counts as unknown and is named in the limitations.

  • It runs only on Linux with DISPLAY set and xprop available (or REA_XPROP_COMMAND). Before writing or launching anything it refuses other hosts, a request naming neither or both of --desktop-entry and --executable, an empty --state-path, --exclude or --output value, a program found only through a relative or empty entry of the host's PATH (lookup follows the launch's search order; an env prefix's PATH travels with the command and may hold such entries), an output folder that is not new or empty or is owned by another user, an unreadable window list, a window manager that does not publish _NET_CLIENT_LIST (EWMH), and an application that already has a window open. The output folder is made private (0700) and its files are created exclusively (0600).

  • The inventory resolves a desktop entry id through $XDG_DATA_HOME and $XDG_DATA_DIRS, following an entry installed as a symbolic link, expands its Exec line with no files and appends --argument values, resolves the program on PATH (the one an env NAME=value prefix sets, whose XDG folders also name the state directories), and identifies the package: snap, flatpak and AppImage from paths (and the AppImage magic, and a flatpak run launch), otherwise the dpkg owner of the launched desktop entry, or else of the executable, with its version, architecture, the apt sources listing the installed version and the o= origin apt reports for each source. The entry's package is the one that installs the application: LibreOffice Writer's entry belongs to libreoffice-writer, while the libreoffice it runs belongs to libreoffice-common, which does not depend on Writer. When the two differ, executable_package names the executable's, with executable_package_reason when neither the Ubuntu 24.04 archive nor a declared vendor repository offers it or apt could not say; each vendor repository records in supplies the library packages and executable's package only vendor repositories offer that its package files list. When a package owns the entry but none owns the program, nor the file it resolves to, executable_package_reason says so without executable_package. A dpkg-query lookup that does not finish or exits with an error leaves the package unknown, with a limitation naming the path. It marks Electron applications by the fuse sentinel or resources/app.asar, and lists candidate state directories under the XDG folders and dot-folders named after the package, executable, desktop entry's full id and last part, and name, and every folder present there whose name is one of those in another letter case, such as GIMP's ~/.config/GIMP, and KDE's <name>rc in the config folder, such as ~/.config/tellicorc; a flatpak run launch uses the application id (its first operand after options and their values) instead of the launcher's name and adds ~/.var/app/<id>. An --executable runs from the caller's directory, which also resolves a relative path to it.

  • Paths are compared in their physical spelling. The walked roots are the home directory and every content root outside it: the state directories, --state-path values and absolute arguments other than / or a directory containing the home directory, which the limitations name instead, plus the -wal and -journal paths beside each of those selected paths that is not a directory, so a database selected on its own is captured with its logs. Each selected path is also recorded as named, beside its physical spelling, so a role can follow from the name the caller gave it. The baseline walk runs before launch and retains every file under the content roots; each later checkpoint reports created, modified, deleted and retyped paths against the previous walk and retains every changed file. A creation or deletion is reported only where the walk listed the path's nearest existing directory completely; the rest are counted as unconfirmed. Walks do not follow symbolic links, do not enter a directory on another device than their root (the limitations name each such mount point), leave out the output folder and --exclude paths, which the capture records as excluded, and stop at --max-entries, roots included.

  • Retention opens files without following links or blocking, keeps a file only if it is still the version the walk recorded and does not change while read, respects --max-file-bytes, and charges --max-content-bytes only for content not already stored. A file over the remaining budget is not read unless stored content of the same size could make it a duplicate. Every omission is recorded with its reason.

  • The application is launched through REA's owned process path: its own process group and a run token in its environment. Every half second the session reads its process tree from procfs, the client list with xprop, and the files its processes hold open under the walked roots. A window belongs to the application by live owner pid or by class (including the entry's full id and a flatpak's application id), re-evaluated every sample; a flatpak's windows are matched by class only. A matching window's pid is followed too. Processes are identified by pid and start time, and each records whether the capture started it: the launched process, a run-token carrier or a descendant of one.

  • When the session ends it records a stop or time-limit checkpoint if the application is still running, sends SIGTERM to the launched process, its process group and every process carrying the run token, waits up to five seconds, removes every process still carrying the run token, and records an exit checkpoint. Processes without the token are never signalled outside the launch group. Cancellation ends the session without further checkpoints, releases the application the same way, and still writes the capture with end reason cancelled; cancellation before launch writes no capture and removes the retained content. Any other failure before capture.json is written releases the application the same way and removes the retained content; processes or files that remain are reported as cleanup facts with the failure.

  • The Evidence uses provider rea-desktop version 1, operation capture_desktop_application and predicate type rea.desktop-application-capture; its parameters carry only counts and the launch kind.

Non-goals

  • No state view, format readers, export, tasks or verification; later steps own those.
  • No click or keystroke recording, no Wayland-native windows, no MCP tool.
  • No writer attribution. State-directory and open-file markers are evidence, not proof.
  • No redaction of the user's files. The capture folder is private data.