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>
121 lines
5.6 KiB
Bash
Executable File
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 $?
|