#!/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 $?