From bd5302e49e8c78815188d748bdaa764cf27a71d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elberte=20Pl=C3=ADnio?= Date: Thu, 9 Jul 2026 20:20:03 -0300 Subject: [PATCH 1/5] feat: project run skill for dev and isolated lab launches Encodes the launch quirks agents kept relearning: Wayland leak (GDK_BACKEND=x11 + unset WAYLAND_DISPLAY), labtest single-instance identifier, private D-Bus for the tray, Xvfb+xfwm4, real-user-data warning (XDG dirs not keyed on identifier), inspect-then-kill pid cleanup. Canonical in .agents/skills/run with a .claude/skills symlink. Refs #35 --- .agents/skills/run/SKILL.md | 81 +++++++++++++++++++++++++++++++++++++ .claude/skills/run | 1 + 2 files changed, 82 insertions(+) create mode 100644 .agents/skills/run/SKILL.md create mode 120000 .claude/skills/run diff --git a/.agents/skills/run/SKILL.md b/.agents/skills/run/SKILL.md new file mode 100644 index 0000000..391f62a --- /dev/null +++ b/.agents/skills/run/SKILL.md @@ -0,0 +1,81 @@ +--- +name: run +description: Launch PickScribe in dev mode or headless lab mode to verify changes without touching the live desktop session when asked to run the app, screenshot it, or confirm a change works in the real app. +--- + +# Run PickScribe + +## Normal dev launch + +Use only in an empty desktop session you own. The real identifier is `com.pickforge.pickscribe`; a plain launch contacts any running instance and focuses its window. + +```bash +bun install --frozen-lockfile +bun run tauri dev +``` + +There is no codegen or sidecar build. It needs Bun, Rust, GTK3, WebKit2GTK 4.1, libayatana-appindicator, librsvg, OpenSSL; audio also needs whisper.cpp and system tools. + +## Isolated lab launch + +Use this for autonomous visual verification. A lab identity prevents the single-instance plugin from pinging the user's app, but it does **not** isolate PickScribe's XDG config/data directories. +Never use Clear all, delete history, sign out, or other destructive UI in a lab instance. + +Pick an unused display and frontend port, then start Vite separately: + +```bash +DISPLAY_NUM=97 +for candidate in {1421..1439}; do ! ss -ltnH "sport = :$candidate" | grep -q . && { PORT=$candidate; break; }; done +: "${PORT:?No free port in 1421-1439}" + +bun run dev -- --port "$PORT" --strictPort >"/tmp/pickscribe-vite-$PORT.log" 2>&1 & +for _ in {1..60}; do curl -fsS "http://127.0.0.1:$PORT/" >/dev/null && break; sleep 1; done +curl -fsS "http://127.0.0.1:$PORT/" >/dev/null +``` + +Start an Xvfb display and a window manager. `xfwm4` is required for maximize/restore controls: + +```bash +Xvfb ":$DISPLAY_NUM" -screen 0 1440x1000x24 -nolisten tcp >"/tmp/pickscribe-xvfb-$DISPLAY_NUM.log" 2>&1 & +xfwm4 --display=":$DISPLAY_NUM" --compositor=off >"/tmp/pickscribe-xfwm-$DISPLAY_NUM.log" 2>&1 & +``` + +GDK otherwise prefers Wayland: `DISPLAY=:N` alone can open on the live desktop. The app also has a tray icon, so give it a private D-Bus session. Force X11 and use the lab-only identifier and Vite URL: + +```bash +dbus-run-session -- env -u WAYLAND_DISPLAY GDK_BACKEND=x11 DISPLAY=":$DISPLAY_NUM" \ + bun run tauri dev --config \ + '{"identifier":"com.pickforge.pickscribe.labtest","build":{"devUrl":"http://127.0.0.1:'"$PORT"'","beforeDevCommand":""}}' \ + >"/tmp/pickscribe-lab-$DISPLAY_NUM.log" 2>&1 & +``` + +Do not pass `--hidden`: the normal config starts the `main` window visible. Wait for the Rust build; use `tail -f "/tmp/pickscribe-lab-$DISPLAY_NUM.log"` if needed. + +## Verify and screenshot + +Inspect the visible window, then capture the Xvfb root. ImageMagick `import` is available here: + +```bash +import -display ":$DISPLAY_NUM" -window root "/tmp/pickscribe-lab-$DISPLAY_NUM.png" +``` + +## Cleanup + +Find each PID first with `pgrep -f` (bracketed patterns avoid matching the search). Confirm it is the lab, then +run the matching `bash` kill command separately: + +```bash +pgrep -af "$PWD/node_modules/.bin/[t]auri dev --config" +bash -lc 'kill ' +pgrep -af "$PWD/target/debug/[p]ickscribe-app" +bash -lc 'kill ' +pgrep -af "[v]ite.*--port $PORT" +bash -lc 'kill ' +pgrep -af "[x]fwm4 --display=:$DISPLAY_NUM" +bash -lc 'kill ' +pgrep -af "[X]vfb :$DISPLAY_NUM" +bash -lc 'kill ' +``` + +Each `bash -lc` call must do nothing except kill the inspected PID. Never use `pkill -f` in a compound command: +its pattern can match the wrapper shell and terminate it instead (exit 144). diff --git a/.claude/skills/run b/.claude/skills/run new file mode 120000 index 0000000..d5bea65 --- /dev/null +++ b/.claude/skills/run @@ -0,0 +1 @@ +../../.agents/skills/run \ No newline at end of file From b2b2587c29f9395f0ece35092dde55d230eeea6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elberte=20Pl=C3=ADnio?= Date: Thu, 9 Jul 2026 20:37:10 -0300 Subject: [PATCH 2/5] feat: sandboxed lab profile for isolated launches Lab launches now override HOME + XDG dirs to a fresh temp profile (CARGO_HOME/RUSTUP_HOME kept real so tauri dev still compiles), matching the sibling apps after their reviews proved the labtest identifier alone leaks real user data. Verified: lab created its own pickscribe.db and WebKit storage under the temp home, real config/data mtimes unchanged. Whisper models are an explicit opt-in symlink. Free X display scanned instead of hard-coded; lab home removed in cleanup. --- .agents/skills/run/SKILL.md | 48 +++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/.agents/skills/run/SKILL.md b/.agents/skills/run/SKILL.md index 391f62a..81ca215 100644 --- a/.agents/skills/run/SKILL.md +++ b/.agents/skills/run/SKILL.md @@ -1,6 +1,6 @@ --- name: run -description: Launch PickScribe in dev mode or headless lab mode to verify changes without touching the live desktop session when asked to run the app, screenshot it, or confirm a change works in the real app. +description: Launch PickScribe in dev mode or a sandboxed headless lab profile when asked to run the app, screenshot it, or confirm a change works in the real app. --- # Run PickScribe @@ -8,23 +8,20 @@ description: Launch PickScribe in dev mode or headless lab mode to verify change ## Normal dev launch Use only in an empty desktop session you own. The real identifier is `com.pickforge.pickscribe`; a plain launch contacts any running instance and focuses its window. - ```bash bun install --frozen-lockfile bun run tauri dev ``` - There is no codegen or sidecar build. It needs Bun, Rust, GTK3, WebKit2GTK 4.1, libayatana-appindicator, librsvg, OpenSSL; audio also needs whisper.cpp and system tools. ## Isolated lab launch -Use this for autonomous visual verification. A lab identity prevents the single-instance plugin from pinging the user's app, but it does **not** isolate PickScribe's XDG config/data directories. -Never use Clear all, delete history, sign out, or other destructive UI in a lab instance. - -Pick an unused display and frontend port, then start Vite separately: - +Each lab starts from an empty profile; destructive UI changes only `$LAB_HOME`. Inherited environment variables, portals, and system services are outside HOME/XDG isolation. +Pick unused X and Vite ports, then start Vite separately: ```bash -DISPLAY_NUM=97 +REAL_HOME=$HOME; LAB_HOME=$(mktemp -d /tmp/pickscribe-lab-home.XXXX) +for n in $(seq 90 120); do [ ! -e "/tmp/.X11-unix/X$n" ] && DISPLAY_NUM=$n && break; done +: "${DISPLAY_NUM:?No free X display in 90-120}" for candidate in {1421..1439}; do ! ss -ltnH "sport = :$candidate" | grep -q . && { PORT=$candidate; break; }; done : "${PORT:?No free port in 1421-1439}" @@ -32,50 +29,49 @@ bun run dev -- --port "$PORT" --strictPort >"/tmp/pickscribe-vite-$PORT.log" 2>& for _ in {1..60}; do curl -fsS "http://127.0.0.1:$PORT/" >/dev/null && break; sleep 1; done curl -fsS "http://127.0.0.1:$PORT/" >/dev/null ``` - Start an Xvfb display and a window manager. `xfwm4` is required for maximize/restore controls: - ```bash Xvfb ":$DISPLAY_NUM" -screen 0 1440x1000x24 -nolisten tcp >"/tmp/pickscribe-xvfb-$DISPLAY_NUM.log" 2>&1 & xfwm4 --display=":$DISPLAY_NUM" --compositor=off >"/tmp/pickscribe-xfwm-$DISPLAY_NUM.log" 2>&1 & ``` - -GDK otherwise prefers Wayland: `DISPLAY=:N` alone can open on the live desktop. The app also has a tray icon, so give it a private D-Bus session. Force X11 and use the lab-only identifier and Vite URL: - +GDK otherwise prefers Wayland: `DISPLAY=:N` alone can open on the live desktop. The app has a tray icon, so give it a private D-Bus session. `REAL_HOME` preserves the existing Cargo and Rustup installations: ```bash dbus-run-session -- env -u WAYLAND_DISPLAY GDK_BACKEND=x11 DISPLAY=":$DISPLAY_NUM" \ + HOME="$LAB_HOME" XDG_CONFIG_HOME="$LAB_HOME/.config" XDG_DATA_HOME="$LAB_HOME/.local/share" XDG_CACHE_HOME="$LAB_HOME/.cache" \ + CARGO_HOME="$REAL_HOME/.cargo" RUSTUP_HOME="$REAL_HOME/.rustup" \ bun run tauri dev --config \ '{"identifier":"com.pickforge.pickscribe.labtest","build":{"devUrl":"http://127.0.0.1:'"$PORT"'","beforeDevCommand":""}}' \ >"/tmp/pickscribe-lab-$DISPLAY_NUM.log" 2>&1 & ``` - -Do not pass `--hidden`: the normal config starts the `main` window visible. Wait for the Rust build; use `tail -f "/tmp/pickscribe-lab-$DISPLAY_NUM.log"` if needed. +Basic boot needs no real-profile files. To test dictation, opt in to model-only access before launch: +```bash +mkdir -p "$LAB_HOME/.local/share/whisper.cpp" +ln -s "$REAL_HOME/.local/share/whisper.cpp/models" "$LAB_HOME/.local/share/whisper.cpp/models" +``` +Do not link real config, data, or credentials. Do not pass `--hidden`: the normal config starts the `main` window visible. ## Verify and screenshot -Inspect the visible window, then capture the Xvfb root. ImageMagick `import` is available here: - +Wait for the Rust build, inspect the visible window, then capture the Xvfb root. ImageMagick `import` is available here: ```bash import -display ":$DISPLAY_NUM" -window root "/tmp/pickscribe-lab-$DISPLAY_NUM.png" +find "$LAB_HOME" -maxdepth 5 -type f ``` ## Cleanup -Find each PID first with `pgrep -f` (bracketed patterns avoid matching the search). Confirm it is the lab, then -run the matching `bash` kill command separately: - +Find each PID first with `pgrep -f` (bracketed patterns avoid matching the search). Confirm it is the lab, then run the matching `bash` kill command separately: ```bash -pgrep -af "$PWD/node_modules/.bin/[t]auri dev --config" -bash -lc 'kill ' pgrep -af "$PWD/target/debug/[p]ickscribe-app" bash -lc 'kill ' +pgrep -af "$PWD/node_modules/.bin/[t]auri dev --config" +bash -lc 'kill ' pgrep -af "[v]ite.*--port $PORT" bash -lc 'kill ' pgrep -af "[x]fwm4 --display=:$DISPLAY_NUM" bash -lc 'kill ' pgrep -af "[X]vfb :$DISPLAY_NUM" bash -lc 'kill ' +rm -rf "$LAB_HOME" ``` - -Each `bash -lc` call must do nothing except kill the inspected PID. Never use `pkill -f` in a compound command: -its pattern can match the wrapper shell and terminate it instead (exit 144). +Each `bash -lc` call must do nothing except kill the inspected PID. Never use `pkill -f` in a compound command: its pattern can match the wrapper shell and terminate it instead (exit 144). From eb5703308326ff21c98d11e9e3d207603d4e71af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elberte=20Pl=C3=ADnio?= Date: Thu, 9 Jul 2026 21:06:17 -0300 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20address=20review=20=E2=80=94=20safe?= =?UTF-8?q?=20lab=20defaults,=20robust=20one-shot=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seeds a safe lab config (local_only, paste off) and unsets cloud API keys so an empty lab profile cannot paste into the real session or call cloud APIs; whisper-cli/models are an explicit opt-in; lab state persists via /tmp/pickscribe-lab.env for separate shell calls; ss guarded; Xvfb waited on; app kill gated on lab environ; lab identifier suffixed with display number to avoid concurrent-lab single-instance pings. Verified end-to-end: LOCAL-ONLY badge active in the lab screenshot, empty profile, clean teardown. --- .agents/skills/run/SKILL.md | 56 ++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/.agents/skills/run/SKILL.md b/.agents/skills/run/SKILL.md index 81ca215..94ecf00 100644 --- a/.agents/skills/run/SKILL.md +++ b/.agents/skills/run/SKILL.md @@ -3,8 +3,6 @@ name: run description: Launch PickScribe in dev mode or a sandboxed headless lab profile when asked to run the app, screenshot it, or confirm a change works in the real app. --- -# Run PickScribe - ## Normal dev launch Use only in an empty desktop session you own. The real identifier is `com.pickforge.pickscribe`; a plain launch contacts any running instance and focuses its window. @@ -16,36 +14,41 @@ There is no codegen or sidecar build. It needs Bun, Rust, GTK3, WebKit2GTK 4.1, ## Isolated lab launch -Each lab starts from an empty profile; destructive UI changes only `$LAB_HOME`. Inherited environment variables, portals, and system services are outside HOME/XDG isolation. -Pick unused X and Vite ports, then start Vite separately: +Each lab starts from an empty, safe profile; anything outside HOME/XDG isolation (inherited environment variables, portals, and system services) can still touch real state. +Prepare the profile and persist its values for separate shell calls: ```bash +bun install --frozen-lockfile +command -v ss >/dev/null || { echo "ss (iproute2) required" >&2; false; } REAL_HOME=$HOME; LAB_HOME=$(mktemp -d /tmp/pickscribe-lab-home.XXXX) for n in $(seq 90 120); do [ ! -e "/tmp/.X11-unix/X$n" ] && DISPLAY_NUM=$n && break; done : "${DISPLAY_NUM:?No free X display in 90-120}" for candidate in {1421..1439}; do ! ss -ltnH "sport = :$candidate" | grep -q . && { PORT=$candidate; break; }; done : "${PORT:?No free port in 1421-1439}" - -bun run dev -- --port "$PORT" --strictPort >"/tmp/pickscribe-vite-$PORT.log" 2>&1 & -for _ in {1..60}; do curl -fsS "http://127.0.0.1:$PORT/" >/dev/null && break; sleep 1; done -curl -fsS "http://127.0.0.1:$PORT/" >/dev/null +mkdir -p "$LAB_HOME/.config/pickscribe" +printf '%s\n' '[general]' 'local_only = true' '' '[paste]' 'method = "none"' 'copy_to_clipboard = false' > "$LAB_HOME/.config/pickscribe/config.toml" +printf 'REAL_HOME=%s\nPORT=%s\nDISPLAY_NUM=%s\nLAB_HOME=%s\n' "$REAL_HOME" "$PORT" "$DISPLAY_NUM" "$LAB_HOME" > /tmp/pickscribe-lab.env ``` -Start an Xvfb display and a window manager. `xfwm4` is required for maximize/restore controls: +Start Vite, Xvfb, and the lab. Wait for Xvfb before starting xfwm4 or the app: ```bash -Xvfb ":$DISPLAY_NUM" -screen 0 1440x1000x24 -nolisten tcp >"/tmp/pickscribe-xvfb-$DISPLAY_NUM.log" 2>&1 & -xfwm4 --display=":$DISPLAY_NUM" --compositor=off >"/tmp/pickscribe-xfwm-$DISPLAY_NUM.log" 2>&1 & +source /tmp/pickscribe-lab.env +setsid bun run dev -- --port "$PORT" --strictPort "/tmp/pickscribe-vite-$PORT.log" 2>&1 & +for _ in {1..60}; do curl -fsS "http://127.0.0.1:$PORT/" >/dev/null && break; sleep 1; done; curl -fsS "http://127.0.0.1:$PORT/" >/dev/null +setsid Xvfb ":$DISPLAY_NUM" -screen 0 1440x1000x24 -nolisten tcp "/tmp/pickscribe-xvfb-$DISPLAY_NUM.log" 2>&1 & +for _ in {1..50}; do [ -e "/tmp/.X11-unix/X$DISPLAY_NUM" ] && break; sleep 0.1; done +[ -e "/tmp/.X11-unix/X$DISPLAY_NUM" ] || { echo "Xvfb did not start" >&2; false; } +setsid xfwm4 --display=":$DISPLAY_NUM" --compositor=off "/tmp/pickscribe-xfwm-$DISPLAY_NUM.log" 2>&1 & +setsid dbus-run-session -- env -u WAYLAND_DISPLAY -u DEEPSEEK_API_KEY -u OPENAI_API_KEY -u OLLAMA_API_KEY -u PICKSCRIBE_API_KEY GDK_BACKEND=x11 DISPLAY=":$DISPLAY_NUM" \ + HOME="$LAB_HOME" XDG_CONFIG_HOME="$LAB_HOME/.config" XDG_DATA_HOME="$LAB_HOME/.local/share" XDG_CACHE_HOME="$LAB_HOME/.cache" CARGO_HOME="$REAL_HOME/.cargo" RUSTUP_HOME="$REAL_HOME/.rustup" \ + bun run tauri dev --config '{"identifier":"com.pickforge.pickscribe.labtest'"$DISPLAY_NUM"'","build":{"devUrl":"http://127.0.0.1:'"$PORT"'","beforeDevCommand":""}}' "/tmp/pickscribe-lab-$DISPLAY_NUM.log" 2>&1 & ``` -GDK otherwise prefers Wayland: `DISPLAY=:N` alone can open on the live desktop. The app has a tray icon, so give it a private D-Bus session. `REAL_HOME` preserves the existing Cargo and Rustup installations: -```bash -dbus-run-session -- env -u WAYLAND_DISPLAY GDK_BACKEND=x11 DISPLAY=":$DISPLAY_NUM" \ - HOME="$LAB_HOME" XDG_CONFIG_HOME="$LAB_HOME/.config" XDG_DATA_HOME="$LAB_HOME/.local/share" XDG_CACHE_HOME="$LAB_HOME/.cache" \ - CARGO_HOME="$REAL_HOME/.cargo" RUSTUP_HOME="$REAL_HOME/.rustup" \ - bun run tauri dev --config \ - '{"identifier":"com.pickforge.pickscribe.labtest","build":{"devUrl":"http://127.0.0.1:'"$PORT"'","beforeDevCommand":""}}' \ - >"/tmp/pickscribe-lab-$DISPLAY_NUM.log" 2>&1 & -``` -Basic boot needs no real-profile files. To test dictation, opt in to model-only access before launch: +GDK otherwise prefers Wayland: `DISPLAY=:N` alone can open on the live desktop. The private D-Bus session isolates the tray; `REAL_HOME` preserves Cargo and Rustup. The identifier suffix (`.labtest$DISPLAY_NUM`) avoids the live app AND other agents' concurrent labs — two labs on one identifier ping each other and the second exits. The seeded config disables cloud cleanup and paste-through—never test dictation paste-through against the real session. + +Basic boot needs no real-profile files. To test dictation, opt in to model and whisper-cli access before launch: ```bash -mkdir -p "$LAB_HOME/.local/share/whisper.cpp" +source /tmp/pickscribe-lab.env +WHISPER_CLI=$(command -v whisper-cli) || { echo "whisper-cli required" >&2; false; } +mkdir -p "$LAB_HOME/.local/bin" "$LAB_HOME/.local/share/whisper.cpp" +ln -s "$WHISPER_CLI" "$LAB_HOME/.local/bin/whisper-cli" ln -s "$REAL_HOME/.local/share/whisper.cpp/models" "$LAB_HOME/.local/share/whisper.cpp/models" ``` Do not link real config, data, or credentials. Do not pass `--hidden`: the normal config starts the `main` window visible. @@ -54,17 +57,20 @@ Do not link real config, data, or credentials. Do not pass `--hidden`: the norma Wait for the Rust build, inspect the visible window, then capture the Xvfb root. ImageMagick `import` is available here: ```bash +source /tmp/pickscribe-lab.env import -display ":$DISPLAY_NUM" -window root "/tmp/pickscribe-lab-$DISPLAY_NUM.png" find "$LAB_HOME" -maxdepth 5 -type f ``` ## Cleanup -Find each PID first with `pgrep -f` (bracketed patterns avoid matching the search). Confirm it is the lab, then run the matching `bash` kill command separately: +Find each PID first with `pgrep -f` (bracketed patterns avoid matching the search). Confirm the app PID has `$LAB_HOME` in its environment before killing it: ```bash +source /tmp/pickscribe-lab.env pgrep -af "$PWD/target/debug/[p]ickscribe-app" +tr '\0' '\n' < /proc//environ | grep -qF "$LAB_HOME" || { echo "not the lab app" >&2; false; } bash -lc 'kill ' -pgrep -af "$PWD/node_modules/.bin/[t]auri dev --config" +pgrep -af "$PWD/node_modules/.bin/[t]auri dev --config.*com.pickforge.pickscribe.labtest$DISPLAY_NUM" bash -lc 'kill ' pgrep -af "[v]ite.*--port $PORT" bash -lc 'kill ' @@ -72,6 +78,6 @@ pgrep -af "[x]fwm4 --display=:$DISPLAY_NUM" bash -lc 'kill ' pgrep -af "[X]vfb :$DISPLAY_NUM" bash -lc 'kill ' -rm -rf "$LAB_HOME" +rm -rf "$LAB_HOME"; rm -f /tmp/pickscribe-lab.env ``` Each `bash -lc` call must do nothing except kill the inspected PID. Never use `pkill -f` in a compound command: its pattern can match the wrapper shell and terminate it instead (exit 144). From 83923ae682f9f1253d89c2a3f5f869c6fb05f5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elberte=20Pl=C3=ADnio?= Date: Thu, 9 Jul 2026 21:20:53 -0300 Subject: [PATCH 4/5] fix: per-checkout lab state, recorder isolation, robust guards Lab state moves to $PWD/.lab.env (gitignored, one lab per checkout); PICKSCRIBE_STATE_DIR points recorder temp WAVs/logs at the lab home (verified the env var in src/engine/recorder.rs); launch block fails fast when Vite never becomes ready; whisper-cli opt-in falls back to ~/.local/bin when not on PATH. --- .agents/skills/run/SKILL.md | 25 +++++++++++++++---------- .gitignore | 1 + 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.agents/skills/run/SKILL.md b/.agents/skills/run/SKILL.md index 94ecf00..d705ad8 100644 --- a/.agents/skills/run/SKILL.md +++ b/.agents/skills/run/SKILL.md @@ -15,8 +15,9 @@ There is no codegen or sidecar build. It needs Bun, Rust, GTK3, WebKit2GTK 4.1, ## Isolated lab launch Each lab starts from an empty, safe profile; anything outside HOME/XDG isolation (inherited environment variables, portals, and system services) can still touch real state. -Prepare the profile and persist its values for separate shell calls: +One lab per checkout: state lives in `$PWD/.lab.env`, so concurrent labs need separate worktrees. Prepare the profile: ```bash +set -e bun install --frozen-lockfile command -v ss >/dev/null || { echo "ss (iproute2) required" >&2; false; } REAL_HOME=$HOME; LAB_HOME=$(mktemp -d /tmp/pickscribe-lab-home.XXXX) @@ -26,27 +27,31 @@ for candidate in {1421..1439}; do ! ss -ltnH "sport = :$candidate" | grep -q . & : "${PORT:?No free port in 1421-1439}" mkdir -p "$LAB_HOME/.config/pickscribe" printf '%s\n' '[general]' 'local_only = true' '' '[paste]' 'method = "none"' 'copy_to_clipboard = false' > "$LAB_HOME/.config/pickscribe/config.toml" -printf 'REAL_HOME=%s\nPORT=%s\nDISPLAY_NUM=%s\nLAB_HOME=%s\n' "$REAL_HOME" "$PORT" "$DISPLAY_NUM" "$LAB_HOME" > /tmp/pickscribe-lab.env +printf 'REAL_HOME=%s\nPORT=%s\nDISPLAY_NUM=%s\nLAB_HOME=%s\n' "$REAL_HOME" "$PORT" "$DISPLAY_NUM" "$LAB_HOME" > "$PWD/.lab.env" ``` Start Vite, Xvfb, and the lab. Wait for Xvfb before starting xfwm4 or the app: ```bash -source /tmp/pickscribe-lab.env +source "$PWD/.lab.env" +set -e setsid bun run dev -- --port "$PORT" --strictPort "/tmp/pickscribe-vite-$PORT.log" 2>&1 & -for _ in {1..60}; do curl -fsS "http://127.0.0.1:$PORT/" >/dev/null && break; sleep 1; done; curl -fsS "http://127.0.0.1:$PORT/" >/dev/null +for _ in {1..60}; do curl -fsS "http://127.0.0.1:$PORT/" >/dev/null 2>&1 && break; sleep 1; done +curl -fsS "http://127.0.0.1:$PORT/" >/dev/null || { echo "Vite never became ready on :$PORT" >&2; false; } setsid Xvfb ":$DISPLAY_NUM" -screen 0 1440x1000x24 -nolisten tcp "/tmp/pickscribe-xvfb-$DISPLAY_NUM.log" 2>&1 & for _ in {1..50}; do [ -e "/tmp/.X11-unix/X$DISPLAY_NUM" ] && break; sleep 0.1; done [ -e "/tmp/.X11-unix/X$DISPLAY_NUM" ] || { echo "Xvfb did not start" >&2; false; } setsid xfwm4 --display=":$DISPLAY_NUM" --compositor=off "/tmp/pickscribe-xfwm-$DISPLAY_NUM.log" 2>&1 & setsid dbus-run-session -- env -u WAYLAND_DISPLAY -u DEEPSEEK_API_KEY -u OPENAI_API_KEY -u OLLAMA_API_KEY -u PICKSCRIBE_API_KEY GDK_BACKEND=x11 DISPLAY=":$DISPLAY_NUM" \ - HOME="$LAB_HOME" XDG_CONFIG_HOME="$LAB_HOME/.config" XDG_DATA_HOME="$LAB_HOME/.local/share" XDG_CACHE_HOME="$LAB_HOME/.cache" CARGO_HOME="$REAL_HOME/.cargo" RUSTUP_HOME="$REAL_HOME/.rustup" \ + HOME="$LAB_HOME" XDG_CONFIG_HOME="$LAB_HOME/.config" XDG_DATA_HOME="$LAB_HOME/.local/share" XDG_CACHE_HOME="$LAB_HOME/.cache" \ + PICKSCRIBE_STATE_DIR="$LAB_HOME/state" CARGO_HOME="$REAL_HOME/.cargo" RUSTUP_HOME="$REAL_HOME/.rustup" \ bun run tauri dev --config '{"identifier":"com.pickforge.pickscribe.labtest'"$DISPLAY_NUM"'","build":{"devUrl":"http://127.0.0.1:'"$PORT"'","beforeDevCommand":""}}' "/tmp/pickscribe-lab-$DISPLAY_NUM.log" 2>&1 & ``` GDK otherwise prefers Wayland: `DISPLAY=:N` alone can open on the live desktop. The private D-Bus session isolates the tray; `REAL_HOME` preserves Cargo and Rustup. The identifier suffix (`.labtest$DISPLAY_NUM`) avoids the live app AND other agents' concurrent labs — two labs on one identifier ping each other and the second exits. The seeded config disables cloud cleanup and paste-through—never test dictation paste-through against the real session. Basic boot needs no real-profile files. To test dictation, opt in to model and whisper-cli access before launch: ```bash -source /tmp/pickscribe-lab.env -WHISPER_CLI=$(command -v whisper-cli) || { echo "whisper-cli required" >&2; false; } +source "$PWD/.lab.env" +WHISPER_CLI=$(command -v whisper-cli || echo "$REAL_HOME/.local/bin/whisper-cli") +[ -x "$WHISPER_CLI" ] || { echo "whisper-cli required" >&2; false; } mkdir -p "$LAB_HOME/.local/bin" "$LAB_HOME/.local/share/whisper.cpp" ln -s "$WHISPER_CLI" "$LAB_HOME/.local/bin/whisper-cli" ln -s "$REAL_HOME/.local/share/whisper.cpp/models" "$LAB_HOME/.local/share/whisper.cpp/models" @@ -57,7 +62,7 @@ Do not link real config, data, or credentials. Do not pass `--hidden`: the norma Wait for the Rust build, inspect the visible window, then capture the Xvfb root. ImageMagick `import` is available here: ```bash -source /tmp/pickscribe-lab.env +source "$PWD/.lab.env" import -display ":$DISPLAY_NUM" -window root "/tmp/pickscribe-lab-$DISPLAY_NUM.png" find "$LAB_HOME" -maxdepth 5 -type f ``` @@ -66,7 +71,7 @@ find "$LAB_HOME" -maxdepth 5 -type f Find each PID first with `pgrep -f` (bracketed patterns avoid matching the search). Confirm the app PID has `$LAB_HOME` in its environment before killing it: ```bash -source /tmp/pickscribe-lab.env +source "$PWD/.lab.env" pgrep -af "$PWD/target/debug/[p]ickscribe-app" tr '\0' '\n' < /proc//environ | grep -qF "$LAB_HOME" || { echo "not the lab app" >&2; false; } bash -lc 'kill ' @@ -78,6 +83,6 @@ pgrep -af "[x]fwm4 --display=:$DISPLAY_NUM" bash -lc 'kill ' pgrep -af "[X]vfb :$DISPLAY_NUM" bash -lc 'kill ' -rm -rf "$LAB_HOME"; rm -f /tmp/pickscribe-lab.env +rm -rf "$LAB_HOME"; rm -f "$PWD/.lab.env" ``` Each `bash -lc` call must do nothing except kill the inspected PID. Never use `pkill -f` in a compound command: its pattern can match the wrapper shell and terminate it instead (exit 144). diff --git a/.gitignore b/.gitignore index f798670..6999fd7 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ src-tauri/gen/schemas/ # Updater signing keys — never commit; set as CI secrets instead. src-tauri/pickscribe-updater.key* +.lab.env From bb679b2f0c1d0b74af590470be85f15856b36c87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elberte=20Pl=C3=ADnio?= Date: Thu, 9 Jul 2026 21:36:12 -0300 Subject: [PATCH 5/5] fix: private XDG_RUNTIME_DIR keeps lab clipboard off the real session method=none is clipboard-only mode and copy_to_clipboard still runs; wl-copy finds the real compositor through the default socket in the inherited runtime dir even with WAYLAND_DISPLAY unset. The lab now gets a private XDG_RUNTIME_DIR (also blocks ydotool), verified by booting the lab and checking the app's environ. Prepare refuses to overwrite live lab state; cleanup guards on missing state. --- .agents/skills/run/SKILL.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.agents/skills/run/SKILL.md b/.agents/skills/run/SKILL.md index d705ad8..5714412 100644 --- a/.agents/skills/run/SKILL.md +++ b/.agents/skills/run/SKILL.md @@ -18,9 +18,11 @@ Each lab starts from an empty, safe profile; anything outside HOME/XDG isolation One lab per checkout: state lives in `$PWD/.lab.env`, so concurrent labs need separate worktrees. Prepare the profile: ```bash set -e +[ ! -f "$PWD/.lab.env" ] || { echo "lab state exists — clean up the previous lab first" >&2; false; } bun install --frozen-lockfile command -v ss >/dev/null || { echo "ss (iproute2) required" >&2; false; } REAL_HOME=$HOME; LAB_HOME=$(mktemp -d /tmp/pickscribe-lab-home.XXXX) +mkdir -m 700 "$LAB_HOME/runtime" for n in $(seq 90 120); do [ ! -e "/tmp/.X11-unix/X$n" ] && DISPLAY_NUM=$n && break; done : "${DISPLAY_NUM:?No free X display in 90-120}" for candidate in {1421..1439}; do ! ss -ltnH "sport = :$candidate" | grep -q . && { PORT=$candidate; break; }; done @@ -42,10 +44,10 @@ for _ in {1..50}; do [ -e "/tmp/.X11-unix/X$DISPLAY_NUM" ] && break; sleep 0.1; setsid xfwm4 --display=":$DISPLAY_NUM" --compositor=off "/tmp/pickscribe-xfwm-$DISPLAY_NUM.log" 2>&1 & setsid dbus-run-session -- env -u WAYLAND_DISPLAY -u DEEPSEEK_API_KEY -u OPENAI_API_KEY -u OLLAMA_API_KEY -u PICKSCRIBE_API_KEY GDK_BACKEND=x11 DISPLAY=":$DISPLAY_NUM" \ HOME="$LAB_HOME" XDG_CONFIG_HOME="$LAB_HOME/.config" XDG_DATA_HOME="$LAB_HOME/.local/share" XDG_CACHE_HOME="$LAB_HOME/.cache" \ - PICKSCRIBE_STATE_DIR="$LAB_HOME/state" CARGO_HOME="$REAL_HOME/.cargo" RUSTUP_HOME="$REAL_HOME/.rustup" \ + XDG_RUNTIME_DIR="$LAB_HOME/runtime" PICKSCRIBE_STATE_DIR="$LAB_HOME/state" CARGO_HOME="$REAL_HOME/.cargo" RUSTUP_HOME="$REAL_HOME/.rustup" \ bun run tauri dev --config '{"identifier":"com.pickforge.pickscribe.labtest'"$DISPLAY_NUM"'","build":{"devUrl":"http://127.0.0.1:'"$PORT"'","beforeDevCommand":""}}' "/tmp/pickscribe-lab-$DISPLAY_NUM.log" 2>&1 & ``` -GDK otherwise prefers Wayland: `DISPLAY=:N` alone can open on the live desktop. The private D-Bus session isolates the tray; `REAL_HOME` preserves Cargo and Rustup. The identifier suffix (`.labtest$DISPLAY_NUM`) avoids the live app AND other agents' concurrent labs — two labs on one identifier ping each other and the second exits. The seeded config disables cloud cleanup and paste-through—never test dictation paste-through against the real session. +GDK otherwise prefers Wayland: `DISPLAY=:N` alone can open on the live desktop. The private D-Bus session isolates the tray; `REAL_HOME` preserves Cargo and Rustup. The identifier suffix (`.labtest$DISPLAY_NUM`) avoids the live app AND other agents' concurrent labs — two labs on one identifier ping each other and the second exits. The seeded config disables cloud cleanup and typing; `method = "none"` still copies to a clipboard, but the private `XDG_RUNTIME_DIR` keeps `wl-copy` (default-socket lookup) and `ydotool` off the real session — clipboard writes land only on the lab display. Never test dictation paste-through against the real session. Basic boot needs no real-profile files. To test dictation, opt in to model and whisper-cli access before launch: ```bash @@ -71,7 +73,9 @@ find "$LAB_HOME" -maxdepth 5 -type f Find each PID first with `pgrep -f` (bracketed patterns avoid matching the search). Confirm the app PID has `$LAB_HOME` in its environment before killing it: ```bash +[ -f "$PWD/.lab.env" ] || { echo "no lab state in this checkout" >&2; false; } source "$PWD/.lab.env" +: "${PORT:?}" "${DISPLAY_NUM:?}" "${LAB_HOME:?}" pgrep -af "$PWD/target/debug/[p]ickscribe-app" tr '\0' '\n' < /proc//environ | grep -qF "$LAB_HOME" || { echo "not the lab app" >&2; false; } bash -lc 'kill '