Files
iris/PLUGIN.md
T
pastilhasandClaude Opus 5 083e1f1f3b fix what the review found: signals, identity, the profile the wrapper nearly used
Ten findings from a review of the first commit. The ones that bite in normal use:

An unchecked mktemp left EPHEMERAL empty, which becomes `--user-data-dir=` — and Chrome reads that as
"use the default profile", i.e. the browser the user actually signs into. `set -u` does not catch it,
because the variable is set, just empty. Now checked and refused. That branch also never exec'd and
had no way to reach the browser with a signal; it now backgrounds, records the pid, and traps.

Signal handlers registered AFTER a build that takes minutes, so a pm2 restart during a first start
killed this process and left cargo compiling as an orphan — then started a second one on the same
target directory. Handlers now register first and shutdown kills the build.

`which('iris')` treated presence as identity. A truncated binary from an interrupted install is
present, reports ready, and never rebuilds. It now runs `--version` and checks — the same lesson
os-deps.ts already learned as `identify`.

Cookies were copied without Cookies-wal. These are WAL databases, so the newest session may exist
only in the log — copy the .db alone and the capture is signed out for exactly the account that just
signed in. PLUGIN.md had this backwards, citing WAL as the reason it was safe; corrected there too.

Also: --password-store=basic was missing from the branch that actually runs, working only because
chromiumoxide's DEFAULT_ARGS happens to supply it; seeding failures were silent, so the skill's
"sign in again" advice was offered for an unreadable profile it cannot fix; only --user-data-dir=X was
detected and not the space-separated form; installWrapper was unguarded at top level and crash-looped
on an unwritable ~/.local/bin; and it truncated the wrapper in place rather than renaming over it.

The searchPath copy is gone — the platform now exports locateBinary and this asks it. That copy is
what made the manifest probe refuse a rustup-only machine the sidecar could build on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 00:04:34 +00:00

4.7 KiB

Iris — the decision record

Written 2026-08-20, the day the plugin was designed in conversation and first laid down. The decisions below are the conversation's residue; where one reverses later, record it here rather than deleting it.

Not a fork

Upstream (iris-screenshot on crates.io, github.com/brijr/iris) is used as-is. The obvious reason to fork was authentication — iris has no --cookie/--header flags — and the decision was that flags are the wrong mechanism anyway: credentials on command lines end up in transcripts and process lists. The session lives in a browser profile the user signs into through the desktop plugin, and the capture borrows a copy. The human authenticates; the agent only ever captures.

Build with cargo, in the sidecar

The owner's call: cargo is a declared dependency, installed before iris, and iris compiles from source on the machine. No prebuilt releases to host, no artefacts to cross-compile, and the plugin has no binary of its own to distribute.

The platform's installer deliberately has no build step (osDependencies installs packages and fetches release binaries, nothing more), so the build lives in the one process the platform starts for a plugin: the sidecar. First start compiles; later starts probe and skip. --locked honours upstream's lockfile — the platform's frozen-lockfile principle, one language over.

Known consequence, accepted: distro cargo is usually too old. Upstream declares rust-version = 1.88; Ubuntu's apt cargo is ~1.75. The manifest still declares distro packages (right on Arch/Fedora, and the honest default), and the sidecar checks the version before building and prints the rustup one-liner when it refuses. Auto-running rustup's curl-pipe-sh was considered and rejected — that is a decision the owner makes at a terminal, not one a sidecar makes silently.

No browser in osDependencies

Three reasons. Ubuntu's apt chromium is a snap stub, so a truthful package map does not exist across distros. The user prefers Brave, which is Chromium underneath — same CDP, same profile format — so the right declaration is "any of a family", which osDependencies cannot express. And the browser this plugin wants is the desktop plugin's, because a browser nobody can see is a browser nobody can sign in to. bin/iris-browser resolves brave → chromium → chrome at run time and fails with a sentence when none exists.

The profile contract

~/.config/officer/browser-profile is the persistent, per-user profile. The desktop plugin's browser should launch with --user-data-dir pointing there and --password-store=basic (portable cookie encryption — keyring-backed cookies do not survive copying). Captures never open that profile directly: the wrapper copies Local State, Default/Cookies and Default/Login Data into a throwaway profile, which avoids Chrome's profile lock entirely and makes the persistent profile read-only to captures.

This contract predates the desktop plugin. When that plugin lands, this path is what it must honour.

Open items, in order of bite

  1. Nothing has run. No live install, no capture, no build. The whole verification table is owed.
  2. How chromiumoxide launches decides which wrapper branch runs. If it passes its own --user-data-dir, the wrapper seeds cookies into that directory; if not, the wrapper appends its own ephemeral one. Both are written; neither is proven. Also unproven: that a seeded cookie copy is accepted (Chrome ties cookie encryption to Local State, which is copied for exactly that reason).
  3. Members. The wrapper isolates per-user by $HOME, but the binary lands in the owner's ~/.local/bin, and a confined member's agent cannot read it there. If members should capture, the binary needs a shared home. Deferred until the desktop plugin defines the member story.
  4. Cookie copy under a live browser. Copying a SQLite file mid-write can catch a torn state. Being WAL does not make this safe — it is what makes the copy INCOMPLETE, because a recent write may live only in Cookies-wal until the browser checkpoints it. This originally read "the cookie DB is WAL and short-lived reads are fine", which had the reasoning backwards: the wrapper copied Cookies alone, so a session created moments earlier was the one guaranteed to be missing. -wal is now copied alongside each database (-shm deliberately is not — it is live shared memory, rebuilt on open). A torn read of the pair is still possible under a live browser and is still unverified.
  5. permissions: [] is honest today (nothing platform-gated to protect) but means the owner cannot dock-gate or role-gate the plugin's existence. Revisit if an api/ or web/ arrives.