Skip to content

Prompt about unauthorized Thunderbolt devices - #12510

Open
s-gato wants to merge 8 commits into
omacom:quattrofrom
s-gato:prompt-unauthorized-thunderbolt
Open

s-gato wants to merge 8 commits into
omacom:quattrofrom
s-gato:prompt-unauthorized-thunderbolt

Conversation

@s-gato

@s-gato s-gato commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

What

When a Thunderbolt device connects unauthorized (BIOS security level user/secure), its USB ports stay silently dead — power delivery and displays keep working, so the failure is invisible. Omarchy ships bolt (#1894) but nothing ever surfaces a waiting device. This adds a prompt: one critical toast per waiting device, whose click opens a floating terminal running a gum menu — Authorize for this session / Authorize permanently (enroll) / Skip — driving boltctl.

Proposal discussion: #12508.

Merge order (required): this PR ships its systemd user units through omarchy-pkgs, so it must merge together with, or after, omarchy-pkgs#713. Without those units, systemctl --user enable --now in first-run fails and repeats on every login until the package ships them.

Why explicit-only

#934's blanket auto-authorize was rejected over the DMA exposure. This keeps that line: no udev rules, no auto-authorization, and nothing runs boltctl without a human choosing it in the menu.

How

  • bin/omarchy-thunderbolt-check (hidden): scans /sys/bus/thunderbolt/devices, notifies once per waiting device — state file records only successful notifications so a failed handover retries; ids are pruned once a device leaves or becomes authorized. Notification state persists across sessions (~/.local/state), so a device toasted once stays quiet on later logins until it disconnects or is authorized — intentional, it avoids re-nagging about the same dock every session. Mirrors omarchy-migrate-notify's omarchy-notification-wait race guard, click-handover, and tty fallback.
  • bin/omarchy-thunderbolt: the reviewer. Verifies the device reports authorized in sysfs (authorized != 0, not just 1) after the action before claiming success; drops the device from the notification state so a future unauthorized event starts fresh.
  • default/systemd/user/omarchy-thunderbolt-notify.{service,path,timer}: a oneshot gated on WAYLAND_DISPLAY plus an opt-out toggle file (~/.local/state/omarchy/toggles/thunderbolt-auth-off), a 30s backstop timer, and a PathChanged unit kept as a best-effort instant trigger — on current kernels kernfs emits no fsnotify create event, so connect detection is guaranteed by the timer, not the path watch.
  • First-run enablement in install/user/first-run/enable-user-units.sh, plus a migration that enables the units on existing installs and falls back to writing the wants symlink directly when no live user manager is available (e.g. an update from a TTY).

Relationship to #11874

#11874 (acrogenesis) adds a broader USB/Thunderbolt approval system and edits the same enable-user-units.sh unit list, so the two will conflict textually at merge and both would prompt for the same Thunderbolt device. They don't need to ship together, but whichever lands second needs a rebase, and the double-prompt overlap should be settled before both land.

Tested

  • ./test/cli green on current quattro (router group + command metadata); style per AGENTS.md; systemd-analyze verify clean.
  • test/shell.d/thunderbolt-test.sh (new): detection, duplicate suppression, prune on unplug, replug, nameless/empty device names, retry after a failed toast, state merge, and the menu's Skip/cancel/authorize/enroll/failure paths, against a synthetic sysfs tree with stubbed boltctl/gum/notification helpers.
  • Migration dry-run twice under a fake $HOME: idempotent, correct symlink fallbacks.
  • Hardware end-to-end on a ThinkPad X390 with a TB3 dock: boltctl forget → replug → toast → click → menu → enroll → dock USB restored.

@llstrk

llstrk commented Sep 25, 2026

Copy link
Copy Markdown

Automated AI review

Community review: Independent automated community review, unaffiliated with the Omarchy team, intended to help prepare PRs for their review.

Outcome: At the pinned head, the prompt never appears. The click command is rejected by omarchy-notification-send, the notification state never prunes, and the three new units are not packaged, which also breaks first-run enablement of the existing user units. Verified: the explicit-only stance holds. boltctl runs only after a menu choice, Esc and Ctrl-C exit without action, and detection selects the correct sysfs nodes.

The toast is never sent: --exec receives one quoted string

bin/omarchy-thunderbolt-check passes the click command as a single argument. bin/omarchy-notification-send (lines 167-175, unchanged on quattro) rejects a whitespace-containing --exec word and exits 1 before calling D-Bus. test/shell.d/notification-send-test.sh asserts that rejection.

-    --exec "omarchy-launch-floating-terminal-with-presentation omarchy-thunderbolt"; then
+    --exec omarchy-launch-floating-terminal-with-presentation omarchy-thunderbolt; then

In a sandboxed run of the unmodified script against a synthetic Thunderbolt tree, every check printed notify failed, will retry, made zero Notify calls and left the state file empty.

Impact: No toast ever appears. Each timer run logs the failure to the user journal. The menu is reachable only by running omarchy thunderbolt by hand.

Suggested change: Pass the words separately, as bin/omarchy-migrate-notify does (diff above).

Pruning prints pruned: but never removes the id

Lines 114-118 echo pruned: $known_id, then lines 120-121 write every key of notified back to the state file. With only the --exec fix applied, the synthetic run showed:

Step State file Toast
Plug in, unauthorized uid recorded shown
Unplug pruned: printed, uid kept none
Replug, still unauthorized uid kept none
Authorized outside the menu pruned: printed, uid kept none

Impact: After one toast, a device stays silent until it is authorized through this menu. Skip, a dismissed toast, direct boltctl, unplug, replug and reboot all leave it silenced, because the state lives in ~/.local/state. This contradicts the PR description and the code comment.

Suggested change: Add unset 'notified[$known_id]' inside the prune loop. With both fixes, the synthetic run behaved as described. If "once per session" is intended, $XDG_RUNTIME_DIR would match the comment at line 54.

The new units are not packaged, which breaks first-run enablement

User units reach /usr/lib/systemd/user through explicit install lines in the omarchy-pkgs pkgbuilds/omarchy-settings/PKGBUILD, and test/shell.d/config-test.sh (package_defaults) tracks them. None of the three omarchy-thunderbolt-notify units appears in either place.

enable-user-units.sh: systemctl --user enable --now bt-agent ... omarchy-crash-watch <3 missing units>
  -> systemd resolves every name before linking any (src/shared/install.c)
  -> "Unit ... does not exist", exit 1, no links created, --now phase never reached
  -> none of the listed units is enabled or started on a new install

An offline systemctl --root check on a scratch tree reproduced this: one missing name, exit 1, zero links. The control without the missing name linked every unit.

Impact: If the PR ships without packaging, new installs lose bt-agent, omarchy-sleep-lock, omarchy-migrate-notify, omarchy-fcitx5, omarchy-crash-watch and the rest of that list. On existing installs, the migration's fallback writes dangling .wants links.

Suggested change: Add install lines for the three units in omarchy-pkgs, and add them to package_defaults in config-test.sh. The crash-watch and fcitx5 changes did the same.

The path unit does not fire when a device connects

Entries under /sys/bus/thunderbolt/devices are created by bus_add_device() → sysfs_create_link() → kernfs_add_one(), which emits no inotify create event. The only kernfs fsnotify call is FS_MODIFY from kernfs_notify(), and systemd's PathChanged mask does not include IN_MODIFY (src/core/path.c). The directory always exists because the module loads from the initramfs. This is based on kernel and systemd source only, not observed on hardware.

Impact: The 30 s timer is the only hot-plug trigger, so detection takes up to about 35 s rather than happening at connect. The comment in the .path unit overstates this.

Suggested change: Either drop the .path unit and describe the timer accurately, or use a real event source. One option is a trigger-only udev rule setting ENV{SYSTEMD_USER_WANTS}, which authorizes nothing. Another is a watcher on boltctl monitor.

The migration starts only the service in a live session

migrations/1789816652.sh enables all three units but starts only omarchy-thunderbolt-notify.service. That runs one scan and exits, and the timer and path stay inactive until the targets are next activated, normally at the next login.

Impact: After omarchy update in a graphical session, a device plugged in later in that session is not detected.

Suggested change: Also start the .timer, and the .path if it is kept.

Smaller points

  • Secure-mode success is reported as failure. When bolt authorizes a stored device with its key, the kernel reports authorized as 2. bin/omarchy-thunderbolt accepts only 1, so it prints "Failed to authorize" and skips the confirmation toast. Accepting any non-zero value avoids this.
  • Devices without device_name are invisible. The kernel hides device_name when the device's DROM lacks a name or fails to parse. Both scripts require that attribute, so such a device gets no toast, and the menu says nothing is waiting, although bolt can still authorize it by numeric ID. This is uncommon on certified hardware. A fallback to the numeric vendor/device IDs, or to the unique id, would cover it.
  • "Enrolled devices" counts connected devices. Piped boltctl list marks every connected device with * whether or not it is stored, and marks disconnected enrolled devices with o. Rename the label, or count stored entries.
  • Polling on every install. The timer has no condition of its own, so it fires every 30 s on machines without Thunderbolt and for users who set the opt-out file, and each run adds info-level journal lines. ConditionPathExistsGlob=/sys/bus/thunderbolt/devices/domain* plus the toggle condition on the timer would keep it inert there.
  • Rebase. The branch conflicts with quattro in install/user/first-run/enable-user-units.sh, where quattro added owed.service and a hook line. Keep both sides. After the rebase, the migration timestamp is older than three quattro migrations.

Review information

Test scope: Source review of the PR against bolt 0.9.11, systemd 262, Linux kernel Thunderbolt/kernfs sources (v6.17 and v7.2.6), gum 2.0.1 and current omarchy-pkgs. Sandboxed runs executed the unmodified PR scripts, the notification helper and the migration against a synthetic sysfs tree, with boltctl, gum, busctl and systemctl stubbed. A separate offline systemctl --root check covered enable semantics. No Thunderbolt hardware, live bolt daemon, user systemd session or notification daemon was used, and the full ./test/cli and ./test/shell suites were not run. Path-unit and polling behavior are from source only.

AI process: Opus 5.5 Medium coordination and synthesis, independent Opus 5.5 Xhigh and GPT 6 Sol Xhigh technical assessments with targeted cross-checks of single-sourced claims, Opus 5.5 Medium editorial check.

Opt out: To stop receiving these reviews, reply to this comment saying so.

s-gato and others added 2 commits September 25, 2026 09:28
A Thunderbolt device connecting unauthorized keeps its USB ports silently
dead while power and displays keep working, so the failure is invisible:
Omarchy ships bolt, but nothing ever surfaces a waiting device. Show one
critical toast per waiting device whose click opens the authorization
menu, with a path unit for instant trigger and a timer backstop.
Authorization stays explicit and per-device.
- Pass --exec words separately so omarchy-notification-send accepts the
  click command (single quoted word was rejected; toast never fired)
- Actually unset pruned ids in the state file so a replugged device
  notifies again instead of staying silenced
- Accept any non-zero authorized value: bolt's stored-key path reports 2
  and was reported as a failure
- Fall back to unique_id when the kernel hides device_name, so nameless
  devices still get a toast
- Rename the misleading 'Enrolled devices' label to 'Connected devices'
- Add Thunderbolt + opt-out conditions to the timer so machines without
  a controller do not log every 30s
- Correct the path-unit comment: kernfs does not reliably deliver create
  events; the 30s timer is the guaranteed trigger
- Start timer and path (not just the oneshot service) in the migration
  when a graphical session is live
@s-gato
s-gato force-pushed the prompt-unauthorized-thunderbolt branch from 44d3486 to 2ae0d79 Compare September 25, 2026 09:30
@s-gato

s-gato commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto quattro (keeping the new owed.service + theme-set hook line) and pushed fixes for the reported issues, each verified with a sandboxed harness (synthetic sysfs tree, stubbed boltctl/gum/omarchy-notification-send/systemd):

Fixed:

  • Toast never fired — --exec now passes the words separately (validated against a stub that reproduces omarchy-notification-send's single-word rejection).
  • Pruning never removed the id — added unset "notified[$known_id]" in the prune loop. Plug→unplug→replug now re-notifies (was silenced before the fix).
  • authorized=2 reported as failure — menu now accepts any non-zero authorized; stored-key authorizations report success.
  • Devices without device_name — attribute is now optional; falls back to unique_id so the device still gets a toast (kernel hides device_name when the DROM lacks a name).
  • Timer polling everywhere — added ConditionPathExistsGlob=/sys/bus/thunderbolt/devices/domain* + the opt-out condition to the timer itself, so non-Thunderbolt machines and opted-out users don't log every 30s.
  • Migration started only the service — now starts timer + path + service in a live session, reporting per-unit failures.
  • Path unit comment overstated — corrected to say kernfs create events are unreliable and the 30s timer is the guaranteed trigger; left the unit in place as a best-effort latency reduction.
  • "Enrolled devices" mislabel — renamed to "Connected devices".

Testing: 11-check regression suite (toast on connect, no duplicates, prune-on-unplug, re-notify on replug, authorized=2 handling, nameless devices, quiet exit on non-Thunderbolt) — all passing after the rebase.

Still open (needs upstream/omarchy-pkgs):

  • Packaging the three units in omarchy-pkgs PKGBUILD + config-test.sh package_defaults (different repo — happy to do it if maintainer wants).
  • Replacing the path unit with a udev rule / boltctl monitor if a real connect-time trigger is wanted; I kept it as a harmless best-effort since the timer guarantees detection.

@llstrk

llstrk commented Sep 27, 2026

Copy link
Copy Markdown

Automated AI review

Community review: Independent automated community review, unaffiliated with the Omarchy team, intended to help prepare PRs for their review.

Outcome: Follow-up on the earlier review and the author's reply, checked at 2ae0d797. Verified: most of the earlier points are fixed. Two issues remain.

  • The units are still not packaged. If the PR ships before that packaging change, at the new base it also stops first-run from completing.
  • The new timer condition, which the earlier review suggested, can turn off detection for a whole session on some hardware. That suggestion is corrected below.
Earlier point Status at 2ae0d797
--exec passed as one string Resolved. The toast is sent with a two-word click command.
Pruning never removed the id Resolved. Unplug and replug now notifies again, and a device authorized outside the menu is pruned.
Units not packaged Still present, with larger impact (below).
Path unit does not fire on connect Partly addressed. The comment no longer promises an instant trigger (see smaller points).
Migration started only the service Resolved. The migration starts the timer, path and service. The TTY fallback is idempotent.
authorized=2 reported as failure Resolved.
Devices without device_name invisible Resolved. They get a toast and a menu entry named by unique_id.
"Enrolled devices" label Resolved. "Connected devices" matches boltctl list output.
Timer polls on every install Changed. Machines without Thunderbolt are now quiet, but see the new finding.
Rebase conflict / migration timestamp Conflict resolved. The timestamp point is withdrawn: omarchy-migrate runs every pending migration in name order, and this migration does not depend on a later one, so its older timestamp has no practical effect.
Explicit-only stance Verified, still holds. boltctl runs only after a menu choice. Esc, Ctrl-C and Skip make no call.

The author's run_suite.sh cannot run as committed. An independent sandboxed harness reproduced the behaviors it claims on the new head, and the old head failed them as expected.

Without packaging, first-run no longer completes

The three units are still missing from the omarchy-settings and omarchy-settings-dev PKGBUILDs in omarchy-pkgs and from package_defaults in test/shell.d/config-test.sh. docs/file-layout.md asks for an install -Dm644 line in the omarchy-settings PKGBUILD for this kind of file. An offline systemctl --root enable of the exact first-run list produced these results:

Unit files present Exit Links created
Existing units only, new names in the list 1 0
Existing list without the new names (control) 0 7
New units installed 0 10

At the rebased base the failure goes further than the earlier review described:

omarchy-provision-first-run
  └─ enable-user-units.sh (set -euo pipefail)
       systemctl --user enable --now ... <3 missing units>  -> exit 1, nothing enabled
       omarchy-hook-install theme-set ...                   -> skipped
  -> step failed, first-run marker not written
  -> first-run retries at every login, including the welcome and Wi-Fi/update notifications

Impact: On a new install built without the packaging change, none of the first-run user units are enabled, the theme-set hook is not installed, and first-run repeats at every login. omarchy-settings-dev builds from the quattro tip, so a dev build right after a merge would be affected unless the omarchy-pkgs change lands with it.

Suggested change: Add install -Dm644 lines for the three units in both PKGBUILDs, and add the three units to package_defaults. Open PR #10582 rewrites this same enable call as a per-unit loop, which would contain the failure. If both land, the three new units need to go into its units=(...) list.

New: the timer condition can disable detection for the whole session

The timer now has ConditionPathExistsGlob=/sys/bus/thunderbolt/devices/domain*. The earlier review suggested this condition, and it has a gap:

  • systemd checks a unit's conditions only when the unit starts (src/core/unit.c). A running timer never rechecks them, and a skipped timer is not retried.
  • domainN exists only while the controller's PCI device is bound (drivers/thunderbolt/pci.c, domain.c).
  • boltctl(1) notes that a controller not in "native enumeration mode" can be completely powered down by the firmware. The kernel's Thunderbolt admin guide says to plug in a device "so that the host controller appears".
login, nothing plugged in, controller powered down
  timer:   domain* absent -> skipped, not retried this session
  path:    no event on connect (kernfs)
  service: one scan at login, nothing found
device plugged in later -> no toast until the timer next starts (normally next login)
before 2ae0d797: the unconditional timer found it within about 30 to 35 s

This is based on kernel, systemd and bolt sources and docs only. It was not observed on hardware, and how many machines behave this way was not measured.

Impact: On affected machines, a device connected after login gets no prompt for the rest of the session. The path-unit comment says "the 30s backstop timer is what guarantees detection", which does not hold there.

Suggested change: Remove the domain* condition from the timer and keep the opt-out condition. That brings back the 30 s polling on every x86_64 install, because the thunderbolt module is in the x86_64 initramfs and /sys/bus/thunderbolt exists whether or not a controller is present. Alternatively, a trigger-only udev rule on Thunderbolt device add events (TAG+="systemd" plus ENV{SYSTEMD_USER_WANTS}) authorizes nothing and would replace both the path unit and the timer.

Smaller points

  • Dash-leading device names break the menu. device_name comes from the device's DROM without filtering. bin/omarchy-thunderbolt passes it to gum style as a positional argument. In a sandboxed run with the real gum 2.0.0:

    • A name like -x exits 80 before the prompt. That also blocks any waiting device listed after it in the same run.
    • A name like --help prints gum's help instead of the name. After a successful enroll, the success line exits 80 under set -e, so the confirmation toast is skipped and the terminal reports failure although the device is authorized.

    Suggested change: gum style --bold -- "$name" and gum style --foreground 2 -- "$name is authorized for this system.". With those two changes and the empty-name fallback below, the patched run handled every tested case.

  • Empty device_name. The kernel can expose the attribute with an empty value, so the unique_id fallback is skipped and the toast and menu show a blank name. The flow still completes. Suggested change for both scripts: [[ -n ${device_names[$unique_id]} ]] || device_names[$unique_id]=$unique_id.

  • State outlives the session. The comment in bin/omarchy-thunderbolt-check says "this session", but the state is kept in ~/.local/state. A device that was toasted, then skipped or dismissed, and that stays connected across logout or reboot is not announced again until it disconnects. Either move the state to $XDG_RUNTIME_DIR or reword the comment.

  • Path unit. The new comment says PathChanged "may fire on some kernels". In Linux v7.2 kernfs, creating a device node emits no fsnotify event, so the unit stays inert. Keeping it is harmless on its own, but the comment could say it does not fire on connect.

  • run_suite.sh at the repo root. It hardcodes an author-local path, writes and deletes under /tmp/tbtest, and depends on stubs that are not in the PR. ./test/shell only runs test/shell.d/*-test.sh, so it is never picked up. Moving it into test/shell.d/ as a *-test.sh file with its stubs, or dropping it, would avoid committing a script nobody else can run.

  • Related PR Require approval for new USB and Thunderbolt devices by default #11874 adds a separate Thunderbolt approval prompt and appends units to the same enable-user-units.sh list. If both merge, one device could get two prompts, and the second merge conflicts on that list.

  • Stale text. The PR description still describes an instant PathChanged trigger and "Verifies authorized == 1".


Review information

Test scope: Source review of 2ae0d797 against quattro at c3e67f5d, with the earlier head 44d34868 as the negative control, current omarchy-pkgs, bolt 0.9.11, systemd v262 and Linux v7.2 sources. Sandboxed runs executed the PR scripts, changed only to point at a synthetic sysfs tree, with the real omarchy-notification-send and boltctl, busctl, gum and systemctl stubbed. The device-name cases used the real gum style, with only gum choose stubbed. They also covered the migration, the first-run script and an offline systemctl --root enable check (systemd 261.2). The repository's ./test/cli and ./test/shell suites showed no new failures: ./test/cli stops at the same environment-dependent check on base and head, and ./test/shell fails the same 5 of 252 files on both. No Thunderbolt hardware, live bolt daemon, user systemd session or notification daemon was used. The timer-condition and path-unit points are from source and documentation only. run_suite.sh was read, not run.

AI process: Opus 5.5 Medium coordination and synthesis, Opus 5.5 Xhigh technical review and final fact check, GPT 6 Sol Xhigh search for related issues, Opus 5.5 Medium editorial check.

Opt out: To stop receiving these reviews, reply to this comment saying so.

- Remove ConditionPathExistsGlob=domain* from the timer: systemd only
  evaluates conditions at unit start and a skipped timer is not retried,
  and domain* only exists while the controller's PCI device is bound, so
  on firmware-powered-down controllers detection went silent for the
  whole session. Keep only the opt-out condition; /sys/bus/thunderbolt
  exists whenever the module is loaded, so there is no per-fire cost
  concern on machines without hardware.
- Pass '--' before device names in every gum style call: DROM names are
  unfiltered and a leading-dash name was parsed as an option (exit 80,
  blocking later devices; --help printed gum's help and, under set -e,
  skipped the success toast after a successful authorization).
- Fall back to unique_id when device_name exists but is empty: the -r
  test passes on an empty attribute and the toast showed a blank name.
- Correct the path unit comment: kernfs emits no fsnotify create event,
  so the unit does not fire on connect on current kernels; the 30s
  timer is the guaranteed trigger.
- Reword the check script's state comment: the state intentionally
  persists across sessions (avoid re-nagging about the same dock).
@s-gato

s-gato commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

All points addressed at 08355977, verified with an expanded 23-check regression suite (original T1–T7 plus 13 new checks; the sandbox now enforces real gum 2.0.0 option parsing, so a dash-leading positional genuinely exits 80 — the previous stub accepted it):

Timer condition (removed). Dropped ConditionPathExistsGlob=domain* and kept only the opt-out condition. Agreed on the mechanics: conditions evaluate only at unit start, skipped timers don't retry, and domain* is absent while the PCI device is unbound — a whole-session detection hole on firmware-powered-down controllers. The timer comment now documents why there is deliberately no hardware condition.

Dash-leading names (fixed). Every gum style call now passes -- before the name. With the strict stub, the old code exited 80 on -x and blocked later devices; the fixed flow authorizes -x/--help-named devices end-to-end with the success toast intact.

Empty device_name (fixed). Both scripts now fall back to unique_id when the attribute exists but is empty (-r alone missed that case; verified with an empty attribute file).

Path unit comment (corrected). Now states plainly that kernfs emits no fsnotify create event, so the unit does not fire on connect on current kernels and the 30s timer is the guaranteed trigger.

State-lifetime comment (reworded). Kept ~/.local/state deliberately — a dismissed device stays quiet across logins until it disconnects or is authorized, which avoids re-nagging about the same dock every session — and documented that in the comment instead of moving the state.

Packaging: agreed it's a ship-blocker for new installs and it lives in omarchy-pkgs — I'll open that PR (three install -Dm644 lines in both PKGBUILDs + package_defaults) and cross-link it here. Noted #10582's per-unit loop; if it lands first I'll add the three units to its list.

run_suite.sh: removed from the branch in the next commit (it's my local harness; happy to contribute a proper test/shell.d/ test instead if maintainers want one).

#11874 double-prompt: acknowledged — will coordinate with that PR's author on the shared enable-user-units.sh list.

Suite result: 23/23 passing.

The config test asserts every file the omarchy-settings PKGBUILD
installs exists in the source tree. The units are now installed by
omacom/omarchy-pkgs#713, so list them here to keep both halves of
the packaging contract checked.
@s-gato

s-gato commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Packaging PR is up: omacom/omarchy-pkgs#713 — installs the three units in both omarchy-settings and omarchy-settings-dev (with the [[ -f ]] forward-compat guard so pins without the units still build), and this PR now also lists them in package_defaults in config-test.sh, so the packaging contract is asserted on both sides. Ideal merge order: omarchy-pkgs#713 first or together.

@llstrk

llstrk commented Sep 30, 2026

Copy link
Copy Markdown

Automated AI review

Community review: Independent automated community review, unaffiliated with the Omarchy team, intended to help prepare PRs for their review.

Outcome: Follow-up on the second review and author reply, at 6caba24c: one merge-order defect remains.

First-run fails if this lands before omarchy-pkgs#713

Dev builds track quattro. Without #713 they ship none of the three units, so enable --now in enable-user-units.sh exits 1, nothing is enabled and first-run repeats at every login until the units ship.

Suggested change: Merge omarchy-pkgs#713 first or together; say so in the PR description.

Reproducer (fails at omarchy-pkgs master, passes with #713)

Run from an omarchy checkout at 6caba24c, with an omarchy-pkgs checkout next to it (../omarchy-pkgs), once at current omarchy-pkgs master (5a49a3f, without #713) and once at the #713 head (80db2be). config-test.sh needs jq, lua and python3. The second script builds omarchy-settings with that checkout's own package() (only ImageMagick is stubbed), then runs an offline systemctl --root enable of the exact unit list in install/user/first-run/enable-user-units.sh.

first-run-offline.sh:

#!/bin/bash
# Run from an omarchy checkout at 6caba24c. $1: an omarchy-pkgs checkout.
# Builds omarchy-settings with that checkout's package(), then enables the
# first-run unit list offline against the packaged user units.
set -u
pkgs=${1:?usage: first-run-offline.sh /path/to/omarchy-pkgs}
work=$(mktemp -d)
mkdir -p "$work/src" "$work/pkg" "$work/bin"
ln -s "$PWD" "$work/src/omarchy"
# Icon thumbnails are irrelevant here; stand in for ImageMagick.
printf '#!/bin/bash\nout=${!#}; out=${out#PNG32:}; mkdir -p "$(dirname "$out")"; : >"$out"\n' >"$work/bin/magick"
chmod +x "$work/bin/magick"
(
  set -e
  export CARCH=x86_64 srcdir="$work/src" pkgdir="$work/pkg" PATH="$work/bin:$PATH"
  source "$pkgs/pkgbuilds/omarchy-settings/PKGBUILD"
  package
) >/dev/null
echo "package() exit: $?"
ls "$work/pkg/usr/lib/systemd/user" | grep thunderbolt || echo "no Thunderbolt units packaged"
# owed.service ships in the owe package, not omarchy-settings; stand in for it.
printf '[Service]\nExecStart=/bin/true\n[Install]\nWantedBy=default.target\n' >"$work/pkg/usr/lib/systemd/user/owed.service"
units=$(sed -n '/enable --now/,/^$/p' install/user/first-run/enable-user-units.sh | grep -o '[a-z0-9-]*\.\(service\|path\|timer\)')
systemctl --root="$work/pkg" --global enable $units >/dev/null 2>"$work/err"
echo "systemctl enable exit: $?"
grep -v -e '^Created symlink' -e 'is added as a dependency' "$work/err"
echo "links created: $(find "$work/pkg/etc/systemd/user" -type l 2>/dev/null | wc -l)"

omarchy-pkgs master (5a49a3f), without #713:

$ OMARCHY_PKGS_PATH=../omarchy-pkgs bash test/shell.d/config-test.sh >/dev/null; echo "exit: $?"
PKGBUILD does not explicitly install default/systemd/user/omarchy-thunderbolt-notify.service -> /usr/lib/systemd/user/omarchy-thunderbolt-notify.service
PKGBUILD does not explicitly install default/systemd/user/omarchy-thunderbolt-notify.path -> /usr/lib/systemd/user/omarchy-thunderbolt-notify.path
PKGBUILD does not explicitly install default/systemd/user/omarchy-thunderbolt-notify.timer -> /usr/lib/systemd/user/omarchy-thunderbolt-notify.timer
exit: 1
$ bash first-run-offline.sh ../omarchy-pkgs
package() exit: 0
no Thunderbolt units packaged
systemctl enable exit: 1
Failed to enable unit: Unit omarchy-thunderbolt-notify.service does not exist
links created: 0

omarchy-pkgs#713 head (80db2be):

$ OMARCHY_PKGS_PATH=../omarchy-pkgs bash test/shell.d/config-test.sh >/dev/null; echo "exit: $?"
exit: 0
$ bash first-run-offline.sh ../omarchy-pkgs
package() exit: 0
omarchy-thunderbolt-notify.path
omarchy-thunderbolt-notify.service
omarchy-thunderbolt-notify.timer
systemctl enable exit: 0
links created: 10

systemd 261.2 (systemctl --root). The build at master succeeds, so nothing in the package build flags the missing units.

Details

Merge order: the dev lane repins the quattro tip and merges green builds automatically; a build without #713 succeeds (its package() exits 0 with no Thunderbolt units). config-test.sh also fails against omarchy-pkgs master until #713 merges (reproducer above).

Still present

Related open PR #13312 (source-only): it clears the click action of toasts restored after a shell restart. The Thunderbolt toast is critical, so if it is still on screen it is restored, and a click then only dismisses it. Because the device stays in the notified state, no fresh toast follows while it stays connected. Keeping the state in $XDG_RUNTIME_DIR, or re-sending when the shell session changes, would limit this.

Corrections to the second review

  • Dropping the timer condition brings back 30 s polling on every install, aarch64 included, not only x86_64.
  • "boltctl runs only after a menu choice" is too broad: a read-only boltctl list runs when nothing is waiting. Accurate: the scripts make no authorizing boltctl call without a choice. Separately, with its default configuration, stock bolt auto-enrolls new devices on hosts with IOMMU DMA protection (bolt-manager.c:1653-1687), so there the prompt appears only in narrower cases, such as a device stored with policy manual, AuthMode disabled or a failed auto-enroll (source-only).

Verified (sandbox; real gum 2.0.0 style, gum choose stubbed)

  • Devices named -x or --help enroll, print the name and send the success toast. A later device in the same run is still offered.
  • An empty device_name shows the unique id in the toast and menu.
  • Esc, Ctrl-C and Skip make no boltctl call.
  • Pruning, replug toasts, authorized=2 and the missing-name fallback behave as at 2ae0d797.
  • ./test/shell and ./test/cli show the same results at 2ae0d797 and 6caba24c (with Same Font dimension everywhere #713 as the omarchy-pkgs checkout).
Reproducer: #11874 conflict

In an omarchy clone with both PR heads fetched (git fetch origin pull/12510/head pull/11874/head), a trial merge of this PR's head 6caba24c with #11874's head c3cdcb1e (merge base 8f324c90):

$ git merge-tree --write-tree --name-only 6caba24cac4bf017c9349fe50ab10773415396ce c3cdcb1e826a5655f87eeb787a5b28ad6e5c892b; echo "exit: $?"
a470179a88c1cb579a0f4fd9b912d7f467f61669
install/user/first-run/enable-user-units.sh

Auto-merging default/omarchy/omarchy-menu.jsonc
Auto-merging install/user/first-run/enable-user-units.sh
CONFLICT (content): Merge conflict in install/user/first-run/enable-user-units.sh
exit: 1

Control, the same #11874 head against quattro at c3e67f5d (this PR's base):

$ git merge-tree --write-tree --name-only c3e67f5d405047765d47a9336a3df115a7beecfb c3cdcb1e826a5655f87eeb787a5b28ad6e5c892b; echo "exit: $?"
c9618910f9aa8126707ccf0b7a215e329cdbcf07
exit: 0

Both PRs replace the same omarchy-crash-watch.service line at the end of the systemctl --user enable --now list and append their own units after it.


Review information

Test scope: Source and sandbox tests at 6caba24c with omarchy-pkgs#713 and master; not tested on hardware or a live session.

AI process: Opus 5.5 Medium coordination and synthesis, Opus 5.5 Xhigh technical review and final fact check, GPT 6 Sol Xhigh search for related issues, Opus 5.5 Medium editorial check.

Opt out: To stop receiving these reviews, reply to this comment saying so.

The notified-state file persists in ~/.local/state and is only pruned when a
device leaves or is authorized, so the pending list is not scoped to the
current session. The old comment wrongly said "this session" and contradicted
the pruning block below it.
@s-gato

s-gato commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

Updated in response to the last review:

  • Added the required merge-order dependency on omarchy-pkgs#713 to the description — the systemd units ship through the package, so first-run enable --now fails until it lands.
  • Corrected two stale claims: the reviewer verifies authorized != 0 (not == 1), and the PathChanged unit is a best-effort trigger, not an instant on-connect check (kernfs emits no fsnotify create event; the timer guarantees detection).
  • Fixed the stale "this session" comment in bin/omarchy-thunderbolt-check; the notified state persists across sessions by design.

On the overlap with #11874: it edits the same enable-user-units.sh unit list, so the two conflict textually and whichever merges second needs a rebase. Both also prompt for the same Thunderbolt device. I've documented that here rather than touching #11874 — the sequencing and any double-prompt resolution are a maintainer call.

@greptile-apps

greptile-apps Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge with the required package change; the remaining notification-state race is non-blocking.

Findings

  1. P2 Saved notification changes get lost ▶

Summary

This PR adds notifications for waiting Thunderbolt devices and a menu for session-only or permanent authorization.

  • A timer checks for devices every 30 seconds.
  • First-run setup and a migration enable the user units.
  • The latest changes add shared file locks, tolerate unplugged devices, and add hardware-free shell tests.
  • Merge together with, or after, the companion package change that supplies the user units.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Timer or path trigger] --> B[Scan waiting devices]
  B --> C[Send notification]
  C --> D[User clicks notification]
  D --> E[Show authorization menu]
  E --> F{User choice}
  F -->|Session| G[boltctl authorize]
  F -->|Permanent| H[boltctl enroll]
  F -->|Skip or cancel| I[No authorization]
  G --> J[Check sysfs authorization]
  H --> J
Loading

Reviews (2) · Last reviewed commit: "test(thunderbolt): cover scanner notific..." · Reviewed by Greptile

Comment thread bin/omarchy-thunderbolt Outdated
Comment on lines +113 to +118
if [[ -f $state_file ]]; then
while IFS= read -r known_id; do
[[ -n $known_id && $known_id != $unique_id ]] || continue
printf '%s\n' "$known_id"
done <"$state_file" >"$state_file.tmp"
mv "$state_file.tmp" "$state_file"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Saved notification changes get lost

omarchy-thunderbolt-check and omarchy-thunderbolt both rewrite notified without a shared lock. A scan can save an old copy after the menu removes an authorized device, restoring its ID. If that device reconnects before another scan sees it authorized or absent, its next prompt is suppressed.

Two open menus also share notified.tmp, so one can overwrite the other's changes or fail at mv. Use one shared lock for both read-and-write paths and a separate temporary file for each writer.

Comment thread bin/omarchy-thunderbolt-check Outdated
Comment thread test/shell.d/config-test.sh
s-gato added 2 commits October 9, 2026 17:01
The scanner (omarchy-thunderbolt-check) and the reviewer (omarchy-thunderbolt) both rewrite the notified-state file, so a scan's write could restore an id a menu had just removed for an authorized device, and two open menus shared one notified.tmp. Serialize the read-modify-write on a shared flock and merge the on-disk set at write time; the menu removes through a unique mktemp file.

A device can also be unplugged mid-scan; the [[ -r ]] checks did not protect the read that followed, so a failed read aborted the scan under set -e or closed the menu before other devices were reviewed. Read every attribute tolerantly and skip a device that left.

The sysfs root honours OMARCHY_THUNDERBOLT_DEVICES_PATH, matching the other hw-* overrides, so behavior can be tested without hardware.
Adds shell coverage with a synthetic sysfs tree and stubbed boltctl, gum and notification helpers: detection, duplicate suppression, prune on unplug, replug, nameless/empty device names, retry after a failed toast, state merge, and the menu's Skip/cancel/authorize/enroll/failure paths.
@s-gato

s-gato commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the three Greptile findings at feae654:

  1. Shared state race — the scanner and the reviewer now serialize their read-modify-write on a shared flock, and the scanner merges the on-disk set at write time instead of replacing it, so a scan cannot resurrect an id a menu just removed. The menu removes through a unique mktemp file.
  2. Unplug mid-scan — every sysfs attribute is read tolerantly; a device that vanished is skipped instead of aborting the scan under set -e, and an unreadable post-action authorization read counts as an unsuccessful action.
  3. Missing tests — test/shell.d/thunderbolt-test.sh now covers detection, duplicate suppression, prune on unplug, replug, nameless/empty names, retry after a failed toast, state merge, and the menu's Skip/cancel/authorize/enroll/failure paths, using a synthetic sysfs tree and stubbed boltctl/gum/notification helpers.

Verified: the new suite (17 checks) passes. ./test/shell and ./test/cli show only pre-existing environment failures (the config/snapper/unowned checks need an omarchy-pkgs checkout; the vscode symlink assertion fails on clean HEAD too).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants