The launcher's Copy Password command, built alone and last as the plan required, because every line of it is the security design: the secret travels rbw to wl-copy through a pipe -- never argv, never a file -- and the copy carries wl-clipboard's --sensitive hint, which vicinae's clipboard history documents it ignores. That claim was not taken on faith: a plain probe landed in the live history database and a sensitive one did not, before any of this was written. A transient timer clears the clipboard after thirty seconds. An unconfigured rbw gets a setup message; a vault that locks between list and get gets an honest failure instead of an empty copy claiming success. rbw joins desktop-packages, and the contract pins the whole journey with a stub vault, including that the secret never appears on a command line.
263 lines
9.9 KiB
Bash
Executable File
263 lines
9.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# The launcher's OS-parity commands: power menu, reminders, and the dmenu
|
|
# pick-lists (windows, processes, SSH hosts, recent files).
|
|
#
|
|
# What must stay true:
|
|
#
|
|
# 1. Every shipped command is one Vicinae accepts. `vicinae script check`
|
|
# exits 0 even on rejection, so its output is what counts.
|
|
# 2. Power commands reach the session's own doors: logind's lock signal,
|
|
# uwsm for logout, systemctl for the rest. No bespoke session teardown.
|
|
# 3. A reminder is a transient user timer: a parseable delay becomes exactly
|
|
# one systemd-run call, nonsense is refused without creating anything.
|
|
# 4. A pick-list acts on the line that was picked -- the index math between
|
|
# dmenu's output and the hidden columns is exactly the kind of off-by-one
|
|
# that survives a reading.
|
|
# 5. Escape is a choice: every list exits 0 and does nothing.
|
|
#
|
|
# Backends are stubbed on PATH; the process test kills its own sleep, never a
|
|
# real one.
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
scripts="$repo_dir/config/local/share/vicinae/scripts"
|
|
remind="$repo_dir/config/dot/quickshell/scripts/panama-remind"
|
|
pick="$repo_dir/config/dot/quickshell/scripts/panama-pick"
|
|
|
|
findings=()
|
|
note() { findings+=("$1"); }
|
|
|
|
work="$(mktemp -d)"
|
|
trap 'rm -rf "$work"' EXIT
|
|
calls="$work/calls"
|
|
stub_dir="$work/bin"
|
|
mkdir -p "$stub_dir"
|
|
|
|
# ── 1. Vicinae accepts every new command ─────────────────────────────────────
|
|
|
|
commands=(lock-screen suspend-system log-out reboot-system power-off
|
|
remind-me list-reminders pick-color copy-password
|
|
switch-window force-quit-window kill-process ssh-hosts recent-files)
|
|
for command in "${commands[@]}"; do
|
|
path="$scripts/$command"
|
|
[[ -x "$path" ]] || { note "$command is missing or not executable"; continue; }
|
|
grep -qE '@vicinae\.mode silent' "$path" \
|
|
|| note "$command is not a silent command"
|
|
if command -v vicinae >/dev/null 2>&1; then
|
|
output="$(vicinae script check "$path" 2>&1)"
|
|
[[ "$output" == *Error* ]] && note "Vicinae rejects $command: $output"
|
|
fi
|
|
done
|
|
|
|
# ── 2. Power commands use the session's own doors ────────────────────────────
|
|
|
|
grep -q 'loginctl lock-session' "$scripts/lock-screen" \
|
|
|| note 'lock-screen does not use the logind lock signal hypridle listens for'
|
|
grep -q 'uwsm stop' "$scripts/log-out" \
|
|
|| note 'log-out does not end the session through uwsm'
|
|
grep -q 'systemctl suspend' "$scripts/suspend-system" \
|
|
|| note 'suspend-system does not use systemctl'
|
|
grep -q 'systemctl reboot' "$scripts/reboot-system" \
|
|
|| note 'reboot-system does not use systemctl'
|
|
grep -q 'systemctl poweroff' "$scripts/power-off" \
|
|
|| note 'power-off does not use systemctl'
|
|
|
|
# ── 3. Reminders ─────────────────────────────────────────────────────────────
|
|
|
|
for stub in systemd-run systemctl notify-send; do
|
|
cat >"$stub_dir/$stub" <<STUB
|
|
#!/usr/bin/env bash
|
|
printf '%s %s\n' "$stub" "\$*" >>"$calls"
|
|
STUB
|
|
chmod +x "$stub_dir/$stub"
|
|
done
|
|
|
|
: >"$calls"
|
|
PATH="$stub_dir:$PATH" "$remind" add "1h30m" "stretch" >/dev/null 2>&1 \
|
|
|| note 'a valid relative reminder failed'
|
|
grep -q -- '--on-active=5400s' "$calls" \
|
|
|| note '1h30m did not become a 5400s timer'
|
|
grep -q -- '--description=stretch' "$calls" \
|
|
|| note 'the reminder text does not ride the unit description'
|
|
|
|
: >"$calls"
|
|
PATH="$stub_dir:$PATH" "$remind" add "soonish" "x" >/dev/null 2>&1 \
|
|
&& note 'a nonsense delay was accepted'
|
|
grep -q 'systemd-run' "$calls" \
|
|
&& note 'a nonsense delay still created a timer'
|
|
|
|
: >"$calls"
|
|
PATH="$stub_dir:$PATH" "$remind" cancel "panama-remind-1-2" >/dev/null 2>&1
|
|
grep -q 'systemctl --user stop panama-remind-1-2.timer' "$calls" \
|
|
|| note 'cancel does not stop the named timer'
|
|
|
|
# ── 4 & 5. Pick-lists act on the picked line ─────────────────────────────────
|
|
|
|
# dmenu stub: record the list it was shown, answer with $DMENU_ANSWER
|
|
# (unset = Escape).
|
|
cat >"$stub_dir/vicinae" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
cat >"$DMENU_SEEN"
|
|
[[ -n "${DMENU_ANSWER:-}" ]] || exit 1
|
|
printf '%s\n' "$DMENU_ANSWER"
|
|
STUB
|
|
chmod +x "$stub_dir/vicinae"
|
|
export DMENU_SEEN="$work/dmenu-seen"
|
|
|
|
# hyprctl stub: serves a two-window fixture, records dispatches.
|
|
cat >"$stub_dir/hyprctl" <<STUB
|
|
#!/usr/bin/env bash
|
|
if [[ "\$1" == "clients" ]]; then cat "$work/clients.json"; exit 0; fi
|
|
printf 'hyprctl %s\n' "\$*" >>"$calls"
|
|
STUB
|
|
chmod +x "$stub_dir/hyprctl"
|
|
|
|
sleep 300 &
|
|
victim=$!
|
|
cat >"$work/clients.json" <<FIXTURE
|
|
[
|
|
{ "address": "0xaaa", "pid": 999999, "mapped": true, "title": "First", "class": "one", "workspace": { "id": 1 } },
|
|
{ "address": "0xbbb", "pid": $victim, "mapped": true, "title": "Second", "class": "two", "workspace": { "id": 2 } }
|
|
]
|
|
FIXTURE
|
|
|
|
# Picking the second window focuses the second address.
|
|
: >"$calls"
|
|
DMENU_ANSWER=1 PATH="$stub_dir:$PATH" "$pick" window >/dev/null 2>&1
|
|
grep -q 'hyprctl dispatch focuswindow address:0xbbb' "$calls" \
|
|
|| note 'picking the second window did not focus the second address'
|
|
|
|
# Escape does nothing.
|
|
: >"$calls"
|
|
PATH="$stub_dir:$PATH" "$pick" window >/dev/null 2>&1 \
|
|
|| note 'Escape from the window list is treated as a failure'
|
|
grep -q 'dispatch' "$calls" && note 'Escape still dispatched a focus'
|
|
|
|
# Force quit kills the picked pid -- our own sleep, which must die.
|
|
DMENU_ANSWER=1 PATH="$stub_dir:$PATH" "$pick" quit-window >/dev/null 2>&1
|
|
sleep 0.2
|
|
kill -0 "$victim" 2>/dev/null \
|
|
&& { note 'force quit did not terminate the picked window process'; kill -9 "$victim" 2>/dev/null; }
|
|
|
|
# Kill process: the list is ps output; picking line one signals that pid.
|
|
sleep 300 &
|
|
victim2=$!
|
|
cat >"$stub_dir/ps" <<STUB
|
|
#!/usr/bin/env bash
|
|
printf '%s 99.0 fake-hog\n' "$victim2"
|
|
STUB
|
|
chmod +x "$stub_dir/ps"
|
|
DMENU_ANSWER=0 PATH="$stub_dir:$PATH" "$pick" process >/dev/null 2>&1
|
|
sleep 0.2
|
|
kill -0 "$victim2" 2>/dev/null \
|
|
&& { note 'kill-process did not signal the picked pid'; kill -9 "$victim2" 2>/dev/null; }
|
|
|
|
# SSH hosts come from ~/.ssh/config, wildcards excluded, session in a terminal.
|
|
fake_home="$work/home"
|
|
mkdir -p "$fake_home/.ssh" "$fake_home/.local/share"
|
|
cat >"$fake_home/.ssh/config" <<'SSH'
|
|
Host alpha
|
|
HostName a.example
|
|
Host *
|
|
User git
|
|
Host beta gamma
|
|
SSH
|
|
cat >"$stub_dir/kitty" <<STUB
|
|
#!/usr/bin/env bash
|
|
printf 'kitty %s\n' "\$*" >>"$calls"
|
|
STUB
|
|
chmod +x "$stub_dir/kitty"
|
|
|
|
: >"$calls"
|
|
DMENU_ANSWER=1 HOME="$fake_home" PATH="$stub_dir:$PATH" "$pick" ssh >/dev/null 2>&1
|
|
grep -qx '\*' "$DMENU_SEEN" && note 'a wildcard Host is offered as a session'
|
|
grep -q 'kitty --detach ssh beta' "$calls" \
|
|
|| note 'picking the second host did not open a session to it'
|
|
|
|
# Recent files decode their URIs; a %20 must come back as a space.
|
|
cat >"$fake_home/.local/share/recently-used.xbel" <<'XBEL'
|
|
<?xml version="1.0"?>
|
|
<xbel version="1.0">
|
|
<bookmark href="file:///tmp/older%20file.txt" modified="2026-08-20T10:00:00Z"/>
|
|
<bookmark href="file:///tmp/newer.txt" modified="2026-08-21T10:00:00Z"/>
|
|
</xbel>
|
|
XBEL
|
|
cat >"$stub_dir/xdg-open" <<STUB
|
|
#!/usr/bin/env bash
|
|
printf 'xdg-open %s\n' "\$*" >>"$calls"
|
|
STUB
|
|
chmod +x "$stub_dir/xdg-open"
|
|
|
|
: >"$calls"
|
|
DMENU_ANSWER=1 HOME="$fake_home" PATH="$stub_dir:$PATH" "$pick" recent >/dev/null 2>&1
|
|
grep -q 'xdg-open /tmp/older file.txt' "$calls" \
|
|
|| note 'the picked recent file was not opened with its URI decoded (newest-first order, %20 as space)'
|
|
|
|
# ── 6. Passwords: the secret's whole journey is a pipe ───────────────────────
|
|
#
|
|
# The rules that make Copy Password safe to ship at all:
|
|
# * the secret is never an argument to anything (argv is world-readable)
|
|
# * it reaches wl-copy with --sensitive, the hint vicinae's clipboard
|
|
# history documents it ignores -- without it, passwords land in a
|
|
# plaintext FTS database
|
|
# * a clipboard clear is scheduled the moment the copy happens
|
|
# * an unconfigured rbw degrades to a setup message, never an error
|
|
|
|
secret='s3cr3t-fixture-value'
|
|
cat >"$stub_dir/rbw" <<STUB
|
|
#!/usr/bin/env bash
|
|
printf 'rbw %s\n' "\$*" >>"$calls"
|
|
case "\$1" in
|
|
list) printf 'github\tgib\nrouter\t\n';;
|
|
get) printf '%s\n' '$secret';;
|
|
esac
|
|
STUB
|
|
chmod +x "$stub_dir/rbw"
|
|
cat >"$stub_dir/wl-copy" <<STUB
|
|
#!/usr/bin/env bash
|
|
printf 'wl-copy %s\n' "\$*" >>"$calls"
|
|
cat >"$work/wl-copy-stdin"
|
|
STUB
|
|
chmod +x "$stub_dir/wl-copy"
|
|
|
|
: >"$calls"; : >"$work/wl-copy-stdin"
|
|
DMENU_ANSWER=0 PATH="$stub_dir:$PATH" "$pick" password >/dev/null 2>&1 \
|
|
|| note 'copying the first password entry failed'
|
|
grep -q "$secret" "$calls" \
|
|
&& note 'the secret appears on a command line'
|
|
grep -qx "$secret" "$work/wl-copy-stdin" \
|
|
|| note 'the secret does not reach wl-copy on stdin'
|
|
grep -q 'wl-copy.*--sensitive' "$calls" \
|
|
|| note 'the copy is not marked sensitive, so it lands in clipboard history'
|
|
grep -q 'rbw get -- github gib' "$calls" \
|
|
|| note 'the picked entry name and user do not reach rbw get, flag-safe'
|
|
grep -qE 'systemd-run .*--on-active=30s .*wl-copy --clear' "$calls" \
|
|
|| note 'no clipboard clear is scheduled after the copy'
|
|
|
|
# Escape copies nothing.
|
|
: >"$calls"
|
|
PATH="$stub_dir:$PATH" "$pick" password >/dev/null 2>&1
|
|
grep -q 'wl-copy' "$calls" && note 'Escape from the password list still copied something'
|
|
|
|
# Unconfigured rbw is a setup hint, not a failure.
|
|
cat >"$stub_dir/rbw" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
exit 1
|
|
STUB
|
|
chmod +x "$stub_dir/rbw"
|
|
: >"$calls"
|
|
PATH="$stub_dir:$PATH" "$pick" password >/dev/null 2>&1 \
|
|
|| note 'an unconfigured rbw is treated as a crash instead of a setup hint'
|
|
grep -q 'not set up' "$calls" \
|
|
|| note 'an unconfigured rbw does not explain how to set it up'
|
|
|
|
if (( ${#findings[@]} > 0 )); then
|
|
printf 'launcher commands contract: %d finding(s)\n' "${#findings[@]}" >&2
|
|
printf ' - %s\n' "${findings[@]}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
printf 'launcher commands contract: PASS\n'
|