Files
iris/bin/iris-browser
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

121 lines
5.6 KiB
Bash
Executable File

#!/bin/sh
# iris-browser — the Chrome-family launcher iris is pointed at (`iris --chrome ~/.local/bin/iris-browser`).
#
# Two jobs, both invisible to iris:
#
# 1. Pick the browser. Brave first — it is Chromium underneath, so CDP and the profile format are the
# same — then Chromium, then Chrome. Iris only knows it asked for "a chrome".
#
# 2. Carry the user's sessions into the capture, read-only. The persistent profile is the one the
# desktop's browser signs into (~/.config/officer/browser-profile); captures ride COPIES of its
# cookies in a throwaway profile, so a capture can never corrupt or lock the profile that holds the
# sessions, and the desktop browser being open never collides with a capture.
#
# Per-user isolation is $HOME: whoever runs iris gets their own profile, their own sessions, and nothing
# of anyone else's. That is the kernel's answer, not this script's.
#
# The launcher (chromiumoxide) may pass its own --user-data-dir. If it does, we seed the sessions INTO
# that directory and change nothing else — overriding it could break how the launcher finds the DevTools
# socket. If it does not, we provide an ephemeral directory ourselves and clean it up.
set -u
PROFILE="${OFFICER_BROWSER_PROFILE:-$HOME/.config/officer/browser-profile}"
BROWSER=""
for candidate in brave-browser brave chromium chromium-browser google-chrome google-chrome-stable; do
if BROWSER="$(command -v "$candidate" 2>/dev/null)" && [ -n "$BROWSER" ]; then
break
fi
done
if [ -z "$BROWSER" ]; then
echo "iris-browser: no Chrome-family browser found (looked for brave, chromium, chrome)" >&2
echo "iris-browser: install the desktop plugin, or any of the above" >&2
exit 127
fi
# Cookies plus the key that decrypts them. Login Data rides along so "remembered" form logins survive.
# Everything else in a profile is cache and preference noise a capture does not need.
#
# ── The -wal files are not optional ──
#
# These are SQLite databases in WAL mode, and a recent write may live ONLY in the write-ahead log: the
# browser has not checkpointed it into the .db yet. Copying `Cookies` without `Cookies-wal` therefore
# produces a profile that is missing exactly the newest sessions — sign in, capture immediately, and the
# capture is signed out, which reads as "sessions do not work" rather than as a missing file.
#
# `-shm` is deliberately NOT copied: it is shared memory for live coordination between processes, it is
# rebuilt from the WAL on open, and a stale one is worse than none.
#
# Failures are REPORTED, not swallowed. The skill tells the user to sign in again when a capture comes
# back logged out; that advice is useless if the real cause was an unreadable profile, and silence is
# what made the two indistinguishable.
seed_one() {
[ -f "$1" ] || return 0
cp "$1" "$2" 2>/dev/null || echo "iris-browser: could not copy $(basename "$1") — captures may be signed out" >&2
}
seed_sessions() {
dest="$1"
[ -d "$PROFILE/Default" ] || {
echo "iris-browser: no profile at $PROFILE — captures will be signed out until you sign in there" >&2
return 0
}
mkdir -p "$dest/Default" || return 0
seed_one "$PROFILE/Local State" "$dest/Local State"
for db in "Cookies" "Login Data"; do
seed_one "$PROFILE/Default/$db" "$dest/Default/$db"
seed_one "$PROFILE/Default/$db-wal" "$dest/Default/$db-wal"
done
}
# Did the launcher already choose a profile directory?
#
# Both spellings. Chrome accepts `--user-data-dir=X` and `--user-data-dir X`, and matching only the first
# would send us down the ephemeral path while the launcher used its own directory — two profiles, and the
# sessions seeded into the one nobody opened.
CALLER_DIR=""
take_next=0
for arg in "$@"; do
if [ "$take_next" -eq 1 ]; then
CALLER_DIR="$arg"
take_next=0
continue
fi
case "$arg" in
--user-data-dir=*) CALLER_DIR="${arg#--user-data-dir=}" ;;
--user-data-dir) take_next=1 ;;
esac
done
# --password-store=basic keeps cookie encryption portable between the profile and the copy: without it a
# keyring-backed browser writes cookies that the copied `Local State` key cannot decrypt, and the capture
# is signed out for a reason nothing reports. Passed on BOTH paths — chromiumoxide's own DEFAULT_ARGS
# happen to include it today, which is not something this script should depend on.
if [ -n "$CALLER_DIR" ]; then
seed_sessions "$CALLER_DIR"
exec "$BROWSER" "$@" --password-store=basic
fi
# CHECKED. `set -u` does not catch this: mktemp failing leaves EPHEMERAL an empty string, which becomes
# `--user-data-dir=` — and Chrome reads an empty value as "use the default profile", i.e. the user's real
# one. A capture would then run in, and write to, the browser they actually use.
if ! EPHEMERAL="$(mktemp -d "${TMPDIR:-/tmp}/iris-profile.XXXXXX")" || [ -z "$EPHEMERAL" ]; then
echo "iris-browser: could not create a temporary profile directory — refusing to launch" >&2
exit 1
fi
trap 'rm -rf "$EPHEMERAL"' EXIT INT TERM
seed_sessions "$EPHEMERAL"
# NOT exec'd, unlike the branch above, because the trap has to outlive the browser to remove the profile —
# and a profile holding copies of the user's cookies is not something to leave in /tmp. The cost is that
# this shell stays alive as the parent; `wait` is what makes a signal to it reach the browser rather than
# killing the wrapper and orphaning the child.
#
# Our --user-data-dir goes AFTER the launcher's args: for a repeated switch Chrome honours the last one.
"$BROWSER" "$@" --user-data-dir="$EPHEMERAL" --password-store=basic &
BROWSER_PID=$!
trap 'kill "$BROWSER_PID" 2>/dev/null; rm -rf "$EPHEMERAL"' EXIT INT TERM
wait "$BROWSER_PID"
exit $?