diff --git a/.agents/skills/reverse-engineer-anything/SKILL.md b/.agents/skills/reverse-engineer-anything/SKILL.md index a974b8c698..4784bfeabb 100644 --- a/.agents/skills/reverse-engineer-anything/SKILL.md +++ b/.agents/skills/reverse-engineer-anything/SKILL.md @@ -137,6 +137,9 @@ explicit path or endpoint and do not need it. - Agent environment (AgentEnv bundle) of a web application: read [environments](references/environments.md) before recording; it records with `capture_browser_scenario` and writes the bundle with `export_environment_bundle`. +- Task for a desktop application's exported environment, from a + `capture-desktop-application` folder: read + [desktop application tasks](references/environments.md#tasks-for-a-desktop-application-environment). - Fiddler Everywhere traffic or a native `.saz` capture: read the [Fiddler workflow](references/runtime-observation.md#fiddler-everywhere-traffic-debugging). Inspect saved SAZ with `inspect_web_network_capture` and `format: "saz"`; @@ -521,6 +524,8 @@ as complete while required questions remain open. - Agent environments (AgentEnv bundles) of a web application, recorded from a live instance or a HAR: [references/environments.md](references/environments.md) +- Tasks for a desktop application's exported environment: + [references/environments.md](references/environments.md#tasks-for-a-desktop-application-environment) ## Finish the task diff --git a/.agents/skills/reverse-engineer-anything/references/environments.md b/.agents/skills/reverse-engineer-anything/references/environments.md index 0be64fbab2..3157c81c99 100644 --- a/.agents/skills/reverse-engineer-anything/references/environments.md +++ b/.agents/skills/reverse-engineer-anything/references/environments.md @@ -8,7 +8,9 @@ confirmed. The workflow records the application once with again if a hint names a gap, then writes the bundle with `export_environment_bundle`. For a HAR the user already has, skip to [export](#export-and-hand-off) through `inspect_web_network_capture`, or use -`rea environment-build `. +`rea environment-build `. For a task in a desktop +application's exported environment, read +[its section](#tasks-for-a-desktop-application-environment). ## Before recording @@ -285,3 +287,77 @@ See [agent environments](https://github.com/morluto/rea/blob/main/docs/agent-environments.md) for what the state, replay and the bundle establish, and [evidence workflows](evidence-workflows.md) for the manifest and state views. + +## Tasks for a desktop application environment + +Use this when the user has a folder from `rea capture-desktop-application` and +wants a task for the environment `rea export-desktop-application` makes from +it: a prompt for a computer-use agent and checks that grade what the agent +left in the app's files. You write the prompt and the checks; REA validates +the task against the capture and exports it. These commands are CLI only. + +### Describe the change + +Run `rea describe-desktop-application-task CAPTURE --format json`. Read: + +- `owned_paths`: the only paths whose files the app serves, so the only files + a check can read. +- `files`: each file there whose state differs between the capture's first + and last version, with its `role`, the `baseline` and `final` versions and + the net `change`. Text changes list each added and removed line once, and + `line_counts` gives lines found at both ends whose count changed. +- `task`: a draft whose `checks` are suggestions from the net change of the + documents the user worked on, or of the app's own state when no document + changed. + +### Write the prompt + +Say what the user wants done, in their words and the app's terms: "Add buy +milk to my to-do list and save it". Never say how the file stores it: no +paths the user would not name, line formats, XML, keys or SQL, and never the +checks. Replace the draft's placeholder; the export refuses it. + +### Pick the checks + +Checks must express only the observed change: + +- Keep a suggestion only if it says what the user did. Drop those that follow + from how the app saves: a format rewrite, renumbered lines, counters. A + LibreCAD drawing gaining four holes was rewritten from DXF R12 to 2007, and + the draft suggests counts of `SECTION`, `ENDSEC` and `CIRCLE`; only + `CIRCLE` equals 9 is the user's change. +- Never check unchanged content, whole files, or values the app stamps: + times, ids it assigns, window sizes. REA refuses ISO 8601 date-times; the + rest is your judgment. +- Prefer a few robust checks: whole lines over positions, rows by meaningful + columns over generated ids, `at_least` when the user may add more. + +Kinds: `file_present` and `file_absent`; `lines_present` and `lines_absent` +(`lines`); `line_count` (`exact` or `contains`, and `equals`, `at_least` or +`at_most`); `value_equals` and `value_absent` (a JSON `pointer` into the view, +such as `/groups/GROUP/KEY` for a key file); `row_present` and `row_absent` +(`table`, `where` by column) and `row_count`. Each names its `file` as a path +under the home. The +[desktop application tasks](https://github.com/morluto/rea/blob/main/docs/desktop-application-tasks.md) +guide has the exact rules. + +### Export the task + +Write the task as JSON with a `name`, the `prompt` and the `checks`, then run +`rea export-desktop-application CAPTURE DIRECTORY --task TASK.json`. REA keeps +the task only when its checks fail on the seed and all pass on the captured +end state. A refusal names the check: one that fails on the end state does not +match what the user left; "every check already passes on the seed" means no +check captures the change. + +When the export refuses an application that no apt repository installs, ask +the user for the `.deb` file they installed it from and capture with +`--package-file`, then ask where the computer may download that file and pass +it as `--package-url`. Never fetch a vendor's download page yourself: such +sites often refuse automated downloads, and the copy and its licence are the +user's to choose. + +Tell the user the task's name, prompt and checks, and how to measure a solve +rate: run `agent-env run DIRECTORY --task NAME --agent claude-code` with +`--model MODEL` three times and count the runs reported `passed`. Do not run +agents unless asked. The bundle's seed is the user's real data: keep it local. diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index d644d593eb..d121d861e5 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -61,6 +61,22 @@ export default defineConfig({ { text: "Electron observation", link: "/electron-observation" }, { text: "Electron fuses", link: "/electron-fuses" }, { text: "Process capture", link: "/process-capture" }, + { + text: "Desktop application capture", + link: "/desktop-application-capture", + }, + { + text: "Desktop application state", + link: "/desktop-application-state", + }, + { + text: "Desktop application export", + link: "/desktop-application-export", + }, + { + text: "Desktop application tasks", + link: "/desktop-application-tasks", + }, { text: ".NET assemblies", link: "/managed-code-analysis" }, { text: "Python packaged applications", diff --git a/docs/cli.md b/docs/cli.md index db22675191..dedf9f638d 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -968,6 +968,10 @@ Choose the guide for the target and intended interaction: - [Electron fuses](electron-fuses.md): read packaged fuse bytes before expecting `--inspect` to reach an Electron main process. - [Node/Electron Inspector](javascript-runtime-observation.md): record script locations and execution contexts. - [Process capture](process-capture.md): run an executable and compare terminal, exit and filesystem observations. +- [Desktop application capture](desktop-application-capture.md): launch a Linux desktop application and record its windows and home-directory state at checkpoints while it is used. +- [Desktop application state](desktop-application-state.md): read a capture folder into the application's state files, typed views of each version and kinds of change. +- [Desktop application export](desktop-application-export.md): write a capture folder as an agentenv computer app bundle with its class, skill and seed. +- [Desktop application tasks](desktop-application-tasks.md): describe how a capture's files changed, then validate an agent-written task against the capture and export it with its verifier. Runtime requests name the target and actions. Launched targets run with your user permissions; consult the chosen guide for host requirements and effects. @@ -1319,7 +1323,9 @@ the command's signal handling. Commands routed through the shared `withCommandCancellation` handler in the CLI set status `130` for SIGINT or `143` for SIGTERM after their operation settles. Current source registrations use that handler for process capture and comparison (`capture-process`, -`compare-process-captures`), JavaScript application analysis (`analyze` when it +`compare-process-captures`), desktop application capture and state +(`capture-desktop-application`, `inspect-desktop-application-state`, +`export-desktop-application`, `describe-desktop-application-task`), JavaScript application analysis (`analyze` when it selects the JavaScript lane, and `analyze-javascript-application`), selected browser, web and Electron commands (including `analyze-web-bundle`, `observe-web-execution`, `capture-browser-scenario` and diff --git a/docs/desktop-application-capture.md b/docs/desktop-application-capture.md new file mode 100644 index 0000000000..b9f56a3db9 --- /dev/null +++ b/docs/desktop-application-capture.md @@ -0,0 +1,305 @@ +# Desktop application capture + +`rea capture-desktop-application` launches an installed Linux desktop +application, watches it while a person or a driver uses it, and records what +it is, which windows it opened and how the files under the home directory +changed. The result is the starting point for turning the application into an +agent environment: the inventory says how to install and launch it, the windows +say how to recognise it, and the checkpoints hold the state it read and wrote. + +The application runs with the current user's permissions, in the current +desktop session, against the user's real files. This is not a sandbox. The +capture folder keeps copies of real files from the home directory, so treat it +as private data and keep it local. + +## Host support + +Capture runs on Linux with an X11 display: `DISPLAY` must be set, and `xprop` +(from `x11-utils`) must be installed, or `REA_XPROP_COMMAND` must name it. +The window manager must support the EWMH client list (`_NET_CLIENT_LIST`, +listed in `_NET_SUPPORTED`), as xfwm4, Mutter, KWin and Openbox do; without it +the capture is refused, because no window could be seen. A supported list that +is unset, as xfwm4 leaves it while no window is open, means no windows. Applications that run under XWayland are visible; +native Wayland windows are not. Other hosts are refused before anything is +launched. + +## Capture an application + +Name the application by its desktop entry id, the entry's absolute path, or an +executable: + +```sh +rea capture-desktop-application --desktop-entry org.xfce.mousepad --output ./mousepad-capture +rea capture-desktop-application --executable gnucash --argument /home/ada/books.gnucash --output ./gnucash-capture +``` + +Desktop entries are looked up in `$XDG_DATA_HOME/applications` and then each +`$XDG_DATA_DIRS` directory; an entry installed as a symbolic link, as flatpak +and Nix do, is followed to its file. The entry's `Exec` line is expanded with no files; +`--argument` values are appended in its place, and the application runs in the +entry's `Path` or the home directory. When the line starts with +`env NAME=value`, the program is found on the `PATH` those assignments set, +with an empty or relative entry read from the folder the application runs +in, and their XDG folders name the state directories. Otherwise the program is +looked up on the host's `PATH` in the order the launch searches it, and one +found only through a relative or empty entry is refused, since the exported +computer would not have that entry. An `--executable` runs from +the current directory, as it would from a shell, and a relative path to it is +resolved there. + +Before writing anything the command refuses to start if a window of the +application is already open, or if the window list cannot be read. A launch +beside a running instance can hand off to it, and that instance's unsaved +state would be missing from the baseline. It then records the starting state, +launches the application in its own process group with a run token in its +environment, and reads commands from stdin, one per line: + +| Line | Effect | +| ------------------------------------------- | ----------------------------------------------------------- | +| empty, `checkpoint`, or `checkpoint ` | Record the current state as a checkpoint | +| any other text except `stop` | Record a checkpoint named by that text | +| `stop`, or the end of stdin | Record a final checkpoint, close the application and finish | + +An unnamed checkpoint is named `checkpoint-` after its time in the +session. A name that repeats an earlier checkpoint, or is one of the capture's +own names (`baseline`, `stop`, `time-limit`, `exit`), is refused with a +progress line on stderr; no checkpoint is taken and the session continues. + +The session also finishes when the launched process has exited and the +application's processes and windows are all gone in two consecutive samples, +or when `--max-duration-seconds` passes. Before deciding the processes are +gone, the capture looks for any process still carrying the run token, so a +worker whose launcher exited before it was sampled keeps the session open. A +sample whose window list could not be read is not taken as an empty one; the +limitations say how many there were. One queued line is handled between +samples, and reading stdin pauses while 16 lines wait. Progress lines on stderr say when +the window has opened and when each checkpoint is taken. + +When the session ends the capture sends SIGTERM to the launched process, its +process group and every process that carries the run token, waits up to five +seconds for them to exit, and then removes every process that still carries +the token, whether or not it stayed in the process group. If that cleanup +cannot be verified after a second attempt, the capture is still written, and +the command then fails, naming the run token and process group that may +remain. Interrupting the command (Ctrl-C) ends the session the same way +without a further checkpoint; the capture is still written with end reason +`cancelled`, and the command exits with status 130. Interrupted before the +application is launched, the command writes no capture and removes the +content it had retained. + +If the capture fails for any other reason before `capture.json` is written, +the application is closed the same way and the content already retained is +removed from the capture folder. The error names any process or file that +could not be removed. + +A person types these lines in the terminal. An agent can drive the +application with a tool such as `xdotool` and write the lines to a FIFO that +it passes as stdin. Because the end of stdin stops the session, the driver +must keep the FIFO open for writing between checkpoints, for example with +`exec 3>capture.fifo` in a shell, rather than open and close it for each +line. + +| Option | Default | Meaning | +| ------------------------ | ------- | ----------------------------------------------------------------------------------------- | +| `--state-path` | none | File or directory whose starting content is retained too (repeatable) | +| `--exclude` | none | Path left out of every checkpoint walk (repeatable); the output folder is always left out | +| `--max-duration-seconds` | 3600 | Stop the session after this long | +| `--max-entries` | 250,000 | Paths recorded per checkpoint walk, its roots included | +| `--max-file-bytes` | 64 MiB | Largest file whose content is retained | +| `--max-content-bytes` | 1 GiB | Total retained content | +| `--package-file` | none | The `.deb` file the application was installed from; its facts are recorded, not the file | + +`--state-path`, `--exclude` and `--output` refuse an empty value, which +would otherwise name the current directory. + +## The capture folder + +The output folder must be new, or empty and owned by the current user; the +command makes it private (mode 0700). It holds: + +- `capture.json`: the Evidence record, also printed on stdout. +- `content/`: the bytes of each retained file, stored once per + distinct content. + +The Evidence operation is `capture_desktop_application` and its predicate type +is `rea.desktop-application-capture`. Its result has four parts. + +**Application.** The desktop entry (id, path, name, `Exec`, icon, +`StartupWMClass`, categories, MIME types), the launch command and working +directory, the executable as found on `PATH` and after resolving links, and the +package that installed it. A Debian package records its name, version, +architecture and the apt sources that list the installed version, with the +origin (`o=`) apt reports for each source in `source_origins`, such as `Ubuntu` +for the Ubuntu archive and its mirrors or `LP-PPA-` for a PPA; an empty +source list means the version came from a local `.deb` or the package lists +are missing, which `package_lists_present` tells apart. + +Each source whose origin is not `Ubuntu` is also recorded in +`apt_repositories`. `apt-get indextargets` names the sources file entry apt +read it from and its release, so flat repositories match too, whether their +suite is `./` (Typora's `deb https://downloads.typora.io/linux ./`) or `/` +(pkgs.k8s.io's). The record holds that entry (`sources_file`, `uri`, `suite`, +`components`, none for a flat repository, and `architectures`, the entry's +`arch=` or `Architectures:` restriction, empty when it has none), `release`, +the release's Suite (apt's `a=`) or else its Codename, and the key that signs +it. A user name or password in an entry's URI (`https://user:password@host/…`) +is never recorded: REA removes it from every URI it reads, in the sources file +and in `apt-get indextargets` and `apt-cache policy` output alike, so entries +match whether or not apt prints it, and sets `uri_credentials_removed` on a repository whose URI held +one (a capture written before this was recorded omits the field). The key is +found by verification, not by name: the candidates are the +entry's `signed-by` keyrings, or else every `.gpg` and `.asc` key in +`/etc/apt/trusted.gpg.d` and `/etc/apt/trusted.gpg`, and each is checked with +`gpgv` against the release's `InRelease` (or `Release` with `Release.gpg`) +beside its index in apt's lists folder. A key counts when gpgv reports a +valid signature by it and no bad, expired, revoked or unverifiable signature +by it, whatever gpgv's exit status: a release also signed by a key the host +lacks makes gpgv fail. Each candidate is copied, an armored key decoded to a +binary keyring, into a private temporary folder that is removed afterwards; a +folder REA cannot remove fails the capture with the folder as its cleanup +resource. The key that verifies is recorded in `signing_key` with its path, +SHA-256, format (`binary` or `armored`) and primary fingerprint, and its bytes +are retained under `content/`. A key file holding one key is retained as it +is; from one holding several, such as a legacy `/etc/apt/trusted.gpg`, only +the transferable key with the verifying fingerprint is retained, in binary, so +the bundle trusts no other key. When no candidate verifies, `gpgv` is +missing, apt holds no signed release, no sources entry matches, the entry's +`Signed-By` holds the key itself, or the key is larger than +`--max-file-bytes` or the content budget, `signing_key` is null and +`signing_key_reason` says why. Ubuntu Pro's ESM repositories (origins such as +`UbuntuESMApps`, host `esm.ubuntu.com`) are not checked: apt reaches them only +with the host's Ubuntu Pro credentials, which a computer does not have. + +Unless the installed version comes from the Ubuntu 24.04 archive, so for a +vendor repository's package and for a local install that no apt source lists, +`library_packages` lists the packages that own the shared libraries the +executable loads at start; the walk runs beside the repository check, and not +at all for an archive package, whose export does not use it. REA reads +DT_NEEDED from the resolved executable and from each library it finds, and +looks each soname up as ld.so does: in the RPATH of the needing image and of +each image up its chain of loaders to the executable when the needing image +has no RUNPATH, then in its RUNPATH (with `$ORIGIN` expanded and paths +normalized), and then in the `ldconfig -p` cache entries for the executable's +machine. It asks `dpkg-query -S` for each library's package. Libraries in the +executable's own folder belong to the application: they are walked but not +listed, and neither is the application's own package. One batched +`apt-cache policy` then keeps only the packages the Ubuntu 24.04 archive or a +recorded vendor repository with a verified key offers, in any component its +sources entry lists, since the export declares them all; the rest, such as +packages from a PPA or a local `.deb`, are listed in +`uninstallable_library_packages`. Sonames found nowhere are listed in +`missing_libraries`, and libraries no package owns in `unpackaged_libraries`. +`library_packages_reason` says why the lists are incomplete: the executable is +not an ELF image (a launcher script, for example), `ldconfig`, `dpkg-query` or +`apt-cache` is missing, a library could not be read or names more than 512 +libraries, or the walk stopped after 512 libraries. + +A sources file, key or library whose handle REA cannot close fails the capture +with the file as its cleanup resource. When the repository check and the +library walk both fail, the capture reports both failures and every folder or +file either could not release. + +An application installed from a `.deb` file you downloaded, such as VariCAD +from varicad.com, has no apt source a computer could install it from. Name +that file with `--package-file` so the export can have the computer download +it instead. REA reads the file's control fields with +`dpkg-deb --field FILE Package Version Architecture Depends Pre-Depends`, and +when its Package, Version and Architecture are the installed package's, it +reads the file once more for its SHA-256 and size and records `package_file`: +the file's `name` without its folder, `sha256`, `size`, `package`, `version`, +`architecture`, and the `depends` and `pre_depends` entries as the file +declares them. Otherwise `package_file_reason` says why, for example that +`dpkg-deb` is not installed, that the file holds another version than the +installed one, or that the file changed between the two reads (its device, +inode, size and times are compared), since its fields and its hash could then +describe different files. The capture never copies the file: it is the vendor's, and only +these facts are kept. + +Snaps, flatpaks and +AppImages are recognised from their paths, and flatpaks also from a +`flatpak run` launch, whose application id is the first operand after +flatpak's options and their values. Electron applications are marked by +the fuse sentinel in their binary or a `resources/app.asar` beside it. State +directories are the XDG config, data, state and cache folders and the +dot-folder named after the package, the executable, the desktop entry's full +id and its last part, and the entry's name, with whether each existed at the +start. Dotfiles in the home named after the application (`.rc`, or +`.` followed by `-`, `.` or `_`, for a name of three or more characters) +that exist at the start join them, since an application may only read such a +file during the capture. A `flatpak run` launch is named after its application +id rather than the `flatpak` launcher, and adds `~/.var/app/`. + +**Session.** When it started, how long it ran and why it ended; the launched +process's exit status; every process seen in the application's tree; every +client window with its class pair, titles, owner pid and type, marked when it +belongs to the application; when the first application window appeared; the +most common class pair of the application's windows, for matching its window +later; files the application's processes held open under the walked roots; the first +8 KiB of the launched process's stderr; and whether cleanup was verified. Each +process records whether the capture started it: the launched process, a +process carrying the capture's run token, or a descendant of one, even when +its environment cannot be read, as for a zombie or a sandboxed renderer. +Processes are told apart by their start time, so a pid reused by another +process is not counted as the application's. + +A window belongs to the application when its owner pid is one of the +application's live processes or its class matches the desktop entry's +`StartupWMClass`, the executable's name, the entry's full id or its last part, +or a flatpak's application id. A flatpak's windows report pids from its +sandbox, so they are matched by class only. +Ownership is checked again at every sample, because a window can appear before +the process that owns it is found. A matching window's pid joins the +application's processes, so a launch handed to a process the capture did not +start, such as a D-Bus activated service, is still followed. That process does +not carry the run token, so the capture never signals it and leaves it +running. + +**State.** The walked roots (the home directory plus any content root +outside it), the content roots (state directories, `--state-path` values and +absolute `--argument` values, except `/` or a directory that contains the home +directory, which are named in the limitations instead; a selected path that is +not a directory brings the `-wal` and `-journal` paths beside it, so a database +selected on its own is captured with its logs), the state directories +as walked (their physical spelling, which differs from the inventory's when one +is a symlink), the paths every walk left out (the output folder and +`--exclude` values), and the checkpoints in order: `baseline` before +launch, each requested checkpoint, `stop` or `time-limit`, and `exit` after the +application has gone. Each checkpoint records how many paths its walk saw, +whether it was truncated, how many paths it could not read or directories on +another device it did not enter (`unchecked_paths`), which paths were created, modified, deleted or +changed type since the previous checkpoint, and which files it retained. A +path counts as created or deleted only where the walk listed its nearest +existing directory completely; differences under a directory that could not be +listed, or that a truncated walk did not reach, are counted as unconfirmed +instead. A walk does not enter a directory on another filesystem than its +root, such as an NFS, sshfs or other FUSE mount under the home directory; the +limitations list each such mount point. The +baseline retains every file under the content roots; later checkpoints retain +every changed file. A change records the file's type, size, mode, +modification time and, when known, the SHA-256 of its content before and +after, plus whether it lies in a state directory and whether the application +held it open. A file is retained only if it is still the version the walk +recorded and does not change while it is read. A file larger than the +remaining content budget is not read, unless stored content of the same size +could make it a duplicate. Files that could not be +retained are listed with the reason: too large, over the content budget, +changed since the walk, changed during the read, or unreadable. +Directories the application held files open in, but whose starting content was +not retained, are listed so a second capture can pass them with +`--state-path`. + +**Limitations.** What the capture could not establish, such as a truncated +walk or a missing window. + +## What the capture does not establish + +Changes are found by comparing walks, using each path's size, modification +time, inode and mode, so a change undone between two checkpoints is not seen +and a rewrite that keeps all four is missed. A change is not attributed to the +process that made it: other desktop services write under the home directory +too, such as the dconf database, the window manager's settings and GTK's +recently used list. The markers for state directories and open files are +evidence for attribution, not proof. + +Clicks and keystrokes are not recorded. Window titles are sampled every half +second, so short-lived titles can be missed. diff --git a/docs/desktop-application-export.md b/docs/desktop-application-export.md new file mode 100644 index 0000000000..ab687e849a --- /dev/null +++ b/docs/desktop-application-export.md @@ -0,0 +1,223 @@ +# Desktop application export + +`rea export-desktop-application ` turns a folder +written by [desktop application capture](desktop-application-capture.md) into an +agentenv bundle holding one computer app environment: the real application, +installed from the distribution archive, its vendor's apt repository or the +`.deb` file it was installed from on the `ubuntu-24.04` computer base, launched +at boot, seeded with the user's state and serving that state through agentenv's +data operations. + +```sh +rea export-desktop-application ./mousepad-capture ./mousepad-bundle +agent-env run ./mousepad-bundle --dry-run +``` + +The export reads the capture and its [state view](desktop-application-state.md); +it does not run the application. `--name` sets the environment name; otherwise +it comes from the desktop entry id or the program, with an `app-` prefix when +that name would not be a portable bundle name (for example all digits). The +output directory must be new or empty. `--task` adds a task an agent wrote for +the capture, once REA has validated it against the capture; see +[desktop application tasks](desktop-application-tasks.md). `--package-url` +says where the computer downloads the package of an application installed from +a `.deb` file; see [Applications from a .deb file](#applications-from-a-deb-file). + +The command returns Evidence with operation `export_desktop_application` and +predicate type `rea.desktop-application-export`, listing the environment name, +owned paths, seed files and written files. A failed export, cancellation +included, removes the bundle it had not yet published, so no partial copy of the +user's data remains. If the bundle was published but its output handles could +not be closed, the command reports a cleanup error that carries the export +Evidence; the bundle itself is complete. + +## The bundle + +| Path | Contents | +| -------------------------------- | -------------------------------------------------------------------------------------------------- | +| `envs//env.toml` | `type = "computer_app"`, the module and class, `computer = "ubuntu-24.04"`, `driver = "reference"` | +| `envs//.py` | The `ComputerAppEnvironment` class | +| `envs//SKILL.md` | What the app is, its window class and where its files live | +| `envs//seed/` | The application's files from before the capture, by home-relative path | +| `envs//apt-keys/` | The signing key of each vendor apt repository the app installs from | +| `envs//install-package.sh` | With `--package-url`: the run script that installs the downloaded package with dpkg | +| `tasks/smoke.json` | A task that deploys the app | +| `tasks/.json` | With `--task`: the agent's task, its verifier in `artifacts/` and the eval in `evals/` | +| `README.md` | What was exported, from which capture, and what to keep in mind | + +The class file is `_app.py` with hyphens as underscores, for example +`mousepad_app.py`: agentenv imports it under its stem, so a plain `.py` +could shadow a standard library module such as `calendar`. A class name that +would collide with a name the module imports or defines, such as `Path`, takes +an `App` suffix. + +The class declares: + +- `Packages(apt=[...])` with the package the capture found. A vendor package + is named `/`, its repository's release (the Suite apt + reports as `a=`, or else its Codename), so apt takes it from that repository + even when the Ubuntu archive offers a higher version or epoch, as noble's + transitional `firefox` does over packages.mozilla.org's; it is named alone + when the release names neither, or when the capture was written before + releases were recorded. Its library packages follow, so a package that + leaves out dependencies on the libraries it loads, as Typora's Electron + package does, still starts; library packages the capture found neither the + Ubuntu 24.04 archive nor a declared repository offers are left out, with a + limitation. An archive package's declared dependencies are trusted as they + are. The package must come from the Ubuntu 24.04 archive or a mirror of it, + or from a vendor apt repository whose signing key the capture verified and + retained. A source counts as the archive when apt reports its origin as + `Ubuntu` for a noble suite; a capture made before origins were recorded + counts only Canonical's own archive hosts. +- For a vendor repository, `sources=[AptSource(apt=..., suite=..., + components=[...], key="apt-keys/.gpg")]`, with the retained key copied + to that path (`.asc` when it is armored, which agentenv tells by its + content). apt takes one key per URI and suite, so sources that share them, + such as one repository listed for two architectures, are declared once, with + their components merged; one with a different key is left out. A repository + whose key the capture did not verify, an Ubuntu Pro (ESM) repository, one + whose sources entry's URI held a user name or password (which the capture + did not record and the export never writes; the reason names neither them + nor the URI), one not reached over http or https, or one whose `arch=` + restriction leaves out the capture host's architecture is refused with its + reason (a capture + written before restrictions were recorded counts as unrestricted); when + another usable repository lists the same version it is left out instead, + with a limitation. The export also refuses snaps, flatpaks, AppImages, and a + package no recorded repository lists unless it was installed from a `.deb` + file the capture recorded and `--package-url` is given, since the computer + could not install them by name, and a capture written before credentials + were removed whose repository URI still holds them, since it counted that + repository's libraries as installable; capture it again. +- `@computer_app(supports=...)` with both `amd64` and `arm64` for an archive + package, which the archive serves for both, and only the capture host's + architecture for a vendor package, since its repository may serve no other. + A vendor package captured on any other architecture is refused. A package + installed from its `.deb` file supports only that file's architecture. +- A launch at boot with the captured command, ready when a window of the + captured class appears and one of them is listed in the taskbar and is not + typed as a splash: a splash screen or a dialog of the class skips the + taskbar or says it is a splash, and an app whose splash shows first would + otherwise count as ready before its own window opens. Arguments that are the captured home or under it, and + `