diff --git a/bin/omarchy-channel-set b/bin/omarchy-channel-set index 86130903..a00c9d92 100755 --- a/bin/omarchy-channel-set +++ b/bin/omarchy-channel-set @@ -61,6 +61,25 @@ link_dev_checkout() { omarchy-dev-link "$checkout" --no-reboot } +# A packaged destination cannot be inspected before its package is installed, +# and a package transaction can replace the running tree with a release that +# predates the command-scoped wrapper. After that, a bare sudo would resolve to +# /usr/bin/sudo and publish a timestamp, and the destination's own updater +# authenticates the same way. Neither may run from a flow that has just run +# user hooks: stop at a consistent point and say how to finish from a fresh +# session. +stop_for_older_destination() { + cat >&2 < 0 )) || { usage; exit 1; } dev_checkout="" @@ -109,23 +128,21 @@ if [[ -n $dev_checkout ]]; then omarchy-state set reboot-required fi -omarchy-refresh-pacman "$pacman_channel" defer-hook +OMARCHY_UPDATE_USER_PATH="$user_path" omarchy-refresh-pacman "$pacman_channel" +# Each transaction can have replaced this tree; check before the next sudo. +wrapper_present || stop_for_older_destination # --ask 4 accepts omarchy <-> omarchy-dev replacement prompts without file overwrites. omarchy-update-pacman -S --needed --noconfirm --ask 4 "${packages[@]}" +wrapper_present || stop_for_older_destination if [[ -z $dev_checkout ]]; then omarchy-dev-unlink --no-reboot export OMARCHY_PATH=/usr/share/omarchy - omarchy_security_enable_no_update_sudo if (( leaving_dev )); then omarchy-state set reboot-required fi + omarchy_security_enable_no_update_sudo 2>/dev/null || stop_for_older_destination fi OMARCHY_UPDATE_USER_PATH="$user_path" "$OMARCHY_PATH/bin/omarchy-update" -y - -# No channel-owned privileged work follows the historical refresh hook. -omarchy_security_revoke_sudo_timestamp -PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$user_path" \ - "$OMARCHY_PATH/bin/omarchy-refresh-pacman" "$pacman_channel" run-deferred diff --git a/bin/omarchy-refresh-pacman b/bin/omarchy-refresh-pacman index f09a0925..cd9eab11 100755 --- a/bin/omarchy-refresh-pacman +++ b/bin/omarchy-refresh-pacman @@ -14,36 +14,35 @@ omarchy_security_require_privileged_bash_startup || exit 126 set -e omarchy_security_sanitize_bash_environment "$0" "$@" omarchy_security_require_source_root "$0" -user_path=$PATH +# Channel switching calls this behind its own sanitized PATH and hands the +# caller's original path over the same way the updater receives it. +user_path=${OMARCHY_UPDATE_USER_PATH:-$PATH} +unset OMARCHY_UPDATE_USER_PATH omarchy_security_revoke_sudo_timestamp || exit 1 omarchy_security_install_sudo_cleanup_traps omarchy_security_enable_no_update_sudo channel="${1:-stable}" -hook_mode="${2:-normal}" if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then echo "Invalid channel: $channel" >&2 exit 2 fi -if [[ $hook_mode != "normal" && $hook_mode != "defer-hook" && $hook_mode != "run-deferred" ]]; then - echo "Invalid refresh hook mode: $hook_mode" >&2 - exit 2 -fi -if [[ $hook_mode != "run-deferred" ]]; then - sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak - sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak - echo "Setting channel to $channel" - sudo cp -f "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" /etc/pacman.conf - sudo cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" /etc/pacman.d/mirrorlist - # Reset all package DBs and then update - omarchy-update-pacman -Syyuu --noconfirm -fi +sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak +sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak +echo "Setting channel to $channel" +sudo cp -f "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" /etc/pacman.conf +sudo cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" /etc/pacman.d/mirrorlist -# Keep the historical hook name, but finish every privileged refresh operation -# before running user code. Callers with later root work can defer the hook. -if [[ $hook_mode != "defer-hook" ]]; then - omarchy_security_revoke_sudo_timestamp - PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$user_path" \ - "$OMARCHY_PATH/bin/omarchy-hook" pre-refresh-pacman -fi +# Allow user customization of /etc/pacman.conf before the upgrade runs, so +# custom repositories and IgnorePkg entries shape the downgrade-capable +# transaction below. The hook is user code: it runs cold behind the no-update +# wrapper, and the timestamp is revoked again afterwards so any authorization +# the hook obtained for itself cannot carry into the transaction. +omarchy_security_revoke_sudo_timestamp +PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$user_path" \ + "$OMARCHY_PATH/bin/omarchy-hook" pre-refresh-pacman +omarchy_security_revoke_sudo_timestamp + +# Reset all package DBs and then update +omarchy-update-pacman -Syyuu --noconfirm diff --git a/bin/omarchy-update b/bin/omarchy-update index 3873a6bc..2d5ad474 100755 --- a/bin/omarchy-update +++ b/bin/omarchy-update @@ -94,15 +94,9 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then # update tooling; the reboot-only phase below performs no privileged work. omarchy-update-restart --services-only - # Release update-owned inhibitors before offering a reboot. A confirmed - # reboot can terminate this process before its EXIT trap gets a chance to - # remove the persistent Stay Awake marker. - omarchy_security_revoke_sudo_timestamp - omarchy-update-stay-awake stop - update_stay_awake_stopped=1 - # AUR package installation must also use the no-update wrapper. Finish # update-owned system work before build code, hooks, or mise can run. + omarchy_security_revoke_sudo_timestamp omarchy-update-aur-pkgs omarchy_security_revoke_sudo_timestamp @@ -114,5 +108,13 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$user_path" "$OMARCHY_PATH/bin/omarchy-update-mise" omarchy_security_revoke_sudo_timestamp + # The sleep inhibitor covers AUR builds, hooks and mise as well; releasing it + # needs no privilege because the held command already dropped to this user. + # Release it before offering a reboot: a confirmed reboot can terminate this + # process before its EXIT trap gets a chance to remove the persistent Stay + # Awake marker. + omarchy-update-stay-awake stop + update_stay_awake_stopped=1 + "$OMARCHY_PATH/bin/omarchy-update-restart" --reboot-only fi diff --git a/config/omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample b/config/omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample index 0c8ba7c4..fdc3a871 100644 --- a/config/omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample +++ b/config/omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample @@ -1,20 +1,17 @@ #!/bin/bash -# This legacy-named hook is called by `omarchy refresh pacman` AFTER the -# channel template is copied and the privileged package transaction finishes. -# During `omarchy channel set`, it is deferred until the package switch and -# complete update have also finished. -# Use it to layer customizations onto the freshly-written pacman.conf for -# subsequent package operations — common cases are adding a custom repository -# (e.g. CachyOS, Chaotic-AUR, an internal company repo) or extra IgnorePkg -# lines. +# This hook is called by `omarchy refresh pacman` AFTER the channel template +# is copied and BEFORE the package transaction runs, so your changes shape +# that transaction. Use it to layer customizations onto the freshly-written +# pacman.conf — common cases are adding a custom repository (e.g. CachyOS, +# Chaotic-AUR, an internal company repo) or extra IgnorePkg lines. # # The hook runs as the invoking user after Omarchy invalidates its sudo cache. # A sudo command here therefore requires its own explicit authorization. # # To put it into use, remove .sample from this file name. -# Example: add an Include line above [core] for future package operations. +# Example: add an Include line above [core]. # Maintain the repo entries in /etc/pacman.d/custom-repos.conf yourself. CONF=/etc/pacman.conf diff --git a/default/agents/skills/omarchy/hooks.md b/default/agents/skills/omarchy/hooks.md index 4878c7e1..5da16fcc 100644 --- a/default/agents/skills/omarchy/hooks.md +++ b/default/agents/skills/omarchy/hooks.md @@ -15,7 +15,7 @@ file first, if one exists. ├── font-set.d/ # After font change (font name in $1) ├── post-boot.d/ # After the desktop starts ├── post-update.d/ # At the end of `omarchy update`, after privileged work -├── pre-refresh-pacman.d/ # After `omarchy refresh pacman` finishes (legacy name) +├── pre-refresh-pacman.d/ # After `omarchy refresh pacman` re-syncs the package config, before it updates packages └── theme-set.d/ # After theme change (theme slug in $1) ``` @@ -27,4 +27,4 @@ echo "Theme changed to: $THEME_NAME" # Add custom actions here ``` -Update-related hooks run only after the workflow's privileged work and after Omarchy invalidates its sudo timestamp. A hook that invokes `sudo` must therefore request its own explicit authorization. The legacy-named `pre-refresh-pacman` hook runs after the refresh transaction; during `omarchy channel set`, it is deferred further until the package switch and complete update finish. Executable user code cannot safely run before a later sudo authentication because a detached child could wait for the new timestamp. +Update-related hooks run as your user after Omarchy invalidates its sudo timestamp, behind the no-update wrapper. A hook that invokes `sudo` must therefore request its own explicit authorization, and Omarchy revokes the timestamp again before continuing. `post-update` runs after every sudo-capable update stage. `pre-refresh-pacman` runs after `omarchy refresh pacman` re-syncs the package config and before the package transaction, so custom repositories and `IgnorePkg` entries shape that transaction; every later privileged command still authenticates without publishing a reusable timestamp, so a detached child left behind by the hook has nothing to wait for. diff --git a/docs/update-process.md b/docs/update-process.md index 97511a5c..f96b28c4 100644 --- a/docs/update-process.md +++ b/docs/update-process.md @@ -139,11 +139,10 @@ omarchy-update ├─ omarchy-update-status │ └─ refresh or clear the shell update indicator ├─ restart marked services and the shell + ├─ invalidate sudo credentials, then update AUR packages + ├─ invalidate again, run the post-update hook, invalidate again, then update mise tools ├─ omarchy-update-stay-awake stop │ └─ release the sleep inhibitor and restore shell idle state, if changed - ├─ update AUR packages - ├─ invalidate sudo credentials - ├─ run the post-update hook, invalidate again, then update mise tools └─ offer the unprivileged reboot prompt ``` @@ -155,7 +154,7 @@ Important behavior: - Migrations remain in chronological order even though historical entries mix user-controlled code with later privileged repairs. Before entering that mixed-trust tail, Omarchy invalidates its timestamp and forces every later sudo call—including AUR's configurable sudo command—to use `--no-update`; prompts authorize one command without publishing a reusable timestamp. Yay's credential loop is disabled for the update. - User-controlled post-update hooks and mise tools run only after every sudo-capable update stage. Omarchy invalidates its sudo timestamp before each boundary and on every exit; detached children therefore have no later reusable update authorization to wait for. - This lifecycle controls authorization created by the protected workflow. `sudo -N` prevents cache updates but can use an existing valid credential, and `sudo -k` revokes the current session's timestamp. It does not isolate the account from unrelated concurrent authentication in another workflow. -- Channel switching establishes the same boundary before dev link/unlink, refresh and package operations. It keeps the wrapper first when changing source roots, carries the original user PATH into update hooks and mise, and runs the deferred refresh hook only after the full update succeeds and authorization is revoked again. Failed and interrupted channel switches revoke on exit. +- Channel switching establishes the same boundary before dev link/unlink, refresh and package operations. It keeps the wrapper first when changing source roots, carries the original user PATH into update hooks and mise, and checks after each package transaction that the wrapper still exists before any further privileged step, since a transaction can replace the running tree with a release that predates it; when it is gone, or the destination otherwise lacks it, the switch stops after the package switch with instructions to run that release's update from a fresh session rather than letting a bare `sudo` or an updater that authenticates without `--no-update` publish a timestamp. Failed and interrupted channel switches revoke on exit. - `-y` exports `OMARCHY_UPDATE_UNATTENDED=1` and suppresses Omarchy confirmation prompts. Interactive review steps (orphan removal, conflict handoff) report and skip instead of blocking. Privileged commands still require sudo authorization, and command-scoped authentication can prompt separately for each command. - The free-space requirement uses a 10 GiB threshold and stops the update before confirmation when it is not met. If free space cannot be determined, the @@ -268,9 +267,9 @@ which pacman repo the mirrorlist points at (and swap between the `omarchy` and `omarchy-dev` packages through a guard-allowed pacman run), while `dev` links the runtime to a git checkout via the dev-link mechanism, after which `omarchy update` fast-forwards that checkout instead of upgrading a package. -Channel switching defers the legacy `pre-refresh-pacman` hook across the package -swap and the complete update. The hook runs exactly once at the final cold -credential boundary; it is skipped if the composite operation fails earlier. +Channel switching runs the `pre-refresh-pacman` hook once, during its refresh +step: cold, behind the no-update wrapper, after the package config is re-synced +and before the refresh transaction. It does not run if the switch fails earlier. There is no version file at runtime. `omarchy-version` derives the version from `pacman -Q` on whichever package is installed, or reports `dev ()` for a diff --git a/manual/31-dotfiles.md b/manual/31-dotfiles.md index a81a6596..420350fe 100644 --- a/manual/31-dotfiles.md +++ b/manual/31-dotfiles.md @@ -38,12 +38,12 @@ Omarchy fires hooks at a handful of moments, and you can hang your own scripts o | ----- | ------------ | | `post-boot` | Right after the desktop has started | | `post-update` | Near the end of `omarchy update`, after packages, migrations, and service restarts, before mise tools are updated | -| `pre-refresh-pacman` | After `omarchy refresh pacman` re-syncs the package config and finishes updating packages; during a channel switch, after the package switch and complete update finish | +| `pre-refresh-pacman` | After `omarchy refresh pacman` re-syncs the package config, before it updates packages; a channel switch runs it during that same refresh step | | `theme-set` | After a theme change (theme name in `$1`) | | `font-set` | After a font change (font name in `$1`) | | `battery-low` | When the battery gets low (percentage in `$1`) | -The `pre-refresh-pacman` name is kept for compatibility. Its edits to the refreshed package config apply to future package operations, after the current transaction has finished. Both update-related hooks run as your user after Omarchy clears its cached sudo authorization, so a hook that uses `sudo` needs its own authorization and may ask for your password. +The `pre-refresh-pacman` hook is where custom repositories or `IgnorePkg` lines belong, since it runs before the package transaction. Both update-related hooks run as your user after Omarchy clears its cached sudo authorization, so a hook that uses `sudo` needs its own authorization and may ask for your password. Each of those directories already holds a `.sample` file showing the shape of a hook — drop the `.sample` from the name to put it to work. To install a script you've written elsewhere, use `omarchy hook install post-boot ~/my-hook`, which copies it in and makes it executable. diff --git a/test/shell.d/channel-sudo-boundary-test.sh b/test/shell.d/channel-sudo-boundary-test.sh index b7fabf02..336ed26c 100755 --- a/test/shell.d/channel-sudo-boundary-test.sh +++ b/test/shell.d/channel-sudo-boundary-test.sh @@ -47,8 +47,12 @@ sudo = [event for event in events if event.startswith('sudo ')] assert all(event in ('sudo -h', 'sudo -k') or event.startswith('sudo -N ') for event in sudo), events hooks = [i for i, event in enumerate(events) if event.startswith('step:omarchy-hook ')] assert len(hooks) == 2, events -assert events[hooks[-1]] == 'step:omarchy-hook pre-refresh-pacman', events -assert not any(event.startswith('sudo -N ') for event in events[hooks[0]:]), events +assert events[hooks[0]] == 'step:omarchy-hook pre-refresh-pacman', events +assert events[hooks[1]] == 'step:omarchy-hook post-update', events +assert events[hooks[0] - 1] == 'sudo -k' and events[hooks[0] + 1] == 'sudo -k', events +transaction = next(i for i, event in enumerate(events) if event.startswith('step:pacman ')) +assert hooks[0] < transaction, events +assert not any(event.startswith('sudo -N ') for event in events[hooks[1]:]), events PY } @@ -59,7 +63,7 @@ for channel in stable rc edge dev; do reset_boundary run_channel "$channel" || fail "$channel failed" "$(<"$boundary_tmp/output")" assert_scoped_channel "$channel" - pass "$channel starts cold, authorizes only individual commands, defers hooks and exits cold" + pass "$channel starts cold, authorizes only individual commands, runs the refresh hook cold before its transaction and exits cold" done reset_boundary @@ -79,6 +83,59 @@ OMARCHY_PATH="$SUDO_TEST_HOME/omarchy" run_channel stable || fail "leaving dev f assert_scoped_channel "dev to stable" pass "leaving dev preserves no-update sudo through unlink and the packaged update" +# A packaged destination that predates the wrapper cannot be checked before its +# package is installed. Its updater authenticates without --no-update, so the +# switch must not launch it: it stops at a consistent point with instructions. +reset_boundary +mv "$SUDO_TEST_ROOT/default/omarchy/sudo-no-update/sudo" "$boundary_tmp/saved-package-wrapper" +mv "$SUDO_TEST_ROOT/bin/omarchy-update" "$boundary_tmp/saved-package-update" +ln -s test-step "$SUDO_TEST_ROOT/bin/omarchy-update" +if OMARCHY_PATH="$SUDO_TEST_HOME/omarchy" run_channel stable; then fail "an older packaged destination was updated with ordinary sudo" "$(<"$SUDO_TEST_LOG")"; fi +grep -q "predates command-scoped sudo" "$boundary_tmp/output" || fail "an older packaged destination was not reported" "$(<"$boundary_tmp/output")" +grep -q "Run 'omarchy update' from a new terminal to finish" "$boundary_tmp/output" || fail "an older packaged destination lacks recovery guidance" "$(<"$boundary_tmp/output")" +grep -Fxq 'step:omarchy-dev-unlink --no-reboot' "$SUDO_TEST_LOG" || fail "the package switch was not completed before stopping" "$(<"$SUDO_TEST_LOG")" +grep -Fxq 'step:omarchy-state set reboot-required' "$SUDO_TEST_LOG" || fail "leaving dev for an older release did not mark the reboot" "$(<"$SUDO_TEST_LOG")" +if grep -q '^step:omarchy-update ' "$SUDO_TEST_LOG"; then fail "an older packaged updater was launched from the hardened switch" "$(<"$SUDO_TEST_LOG")"; fi +grep -q 'The channel switch did not complete' "$boundary_tmp/output" && fail "the stop was reported as an error needing a rerun" "$(<"$boundary_tmp/output")" +assert_boundary_cold "older packaged destination" +rm "$SUDO_TEST_ROOT/bin/omarchy-update" +mv "$boundary_tmp/saved-package-update" "$SUDO_TEST_ROOT/bin/omarchy-update" +mv "$boundary_tmp/saved-package-wrapper" "$SUDO_TEST_ROOT/default/omarchy/sudo-no-update/sudo" +pass "an older packaged destination stops the switch cold with instructions instead of running its updater" + +# A package-backed source can be downgraded by its own transaction to a release +# without the wrapper. From then on a bare sudo would be the real one, so no +# privileged step may follow either transaction without checking first. A decoy +# sudo in the package bin catches any such call instead of reaching the host. +cat >"$SUDO_TEST_ROOT/bin/sudo" <<'STUB' +#!/bin/bash +printf 'unwrapped-sudo %s\n' "$*" >>"$SUDO_TEST_LOG" +exit 97 +STUB +chmod +x "$SUDO_TEST_ROOT/bin/sudo" +cp "$SUDO_TEST_ROOT/default/omarchy/sudo-no-update/sudo" "$boundary_tmp/saved-package-wrapper" +for pattern in 'pacman -Syyuu*' 'pacman -S --needed*'; do + reset_boundary + export SUDO_TEST_REMOVE_WRAPPER_STEP=$pattern + if run_channel rc; then fail "a downgrade during '$pattern' was not detected" "$(<"$SUDO_TEST_LOG")"; fi + unset SUDO_TEST_REMOVE_WRAPPER_STEP + grep -q "predates command-scoped sudo" "$boundary_tmp/output" || fail "downgrade during '$pattern' was not reported" "$(<"$boundary_tmp/output")" + if grep -q '^unwrapped-sudo ' "$SUDO_TEST_LOG"; then fail "downgrade during '$pattern' reached ordinary sudo" "$(<"$SUDO_TEST_LOG")"; fi + if grep -q '^step:omarchy-dev-unlink' "$SUDO_TEST_LOG"; then fail "downgrade during '$pattern' still unlinked" "$(<"$SUDO_TEST_LOG")"; fi + if grep -q '^step:omarchy-update ' "$SUDO_TEST_LOG"; then fail "downgrade during '$pattern' still updated" "$(<"$SUDO_TEST_LOG")"; fi + python3 - "$SUDO_TEST_LOG" "$pattern" <<'PY' +import sys +events = open(sys.argv[1]).read().splitlines() +transactions = [e for e in events if e.startswith('step:pacman ')] +assert len(transactions) == (1 if sys.argv[2].startswith('pacman -Syyuu') else 2), events +assert all(e in ('sudo -h', 'sudo -k') or e.startswith('sudo -N ') for e in events if e.startswith('sudo ')), events +PY + assert_boundary_cold "downgrade during $pattern" + cp "$boundary_tmp/saved-package-wrapper" "$SUDO_TEST_ROOT/default/omarchy/sudo-no-update/sudo" + pass "a transaction that removes the wrapper stops the switch before any further sudo ($pattern)" +done +rm "$SUDO_TEST_ROOT/bin/sudo" + mkdir "$boundary_tmp/user tools" cat >"$boundary_tmp/user tools/channel-user-tool" <<'STUB' #!/bin/bash @@ -110,8 +167,8 @@ for step in pacman omarchy-update-system-pkgs omarchy-hook; do reset_boundary if SUDO_TEST_FAIL_STEP="$step" run_channel stable; then fail "$step failure was ignored"; fi assert_boundary_cold "$step failure" - if grep -q '^step:omarchy-hook pre-refresh-pacman$' "$SUDO_TEST_LOG"; then fail "$step failure reached the deferred hook"; fi - pass "$step failure exits cold without the deferred hook" + if grep -q '^step:omarchy-hook post-update$' "$SUDO_TEST_LOG"; then fail "$step failure reached the post-update hook"; fi + pass "$step failure exits cold without the post-update hook" done for signal in HUP INT TERM; do @@ -123,7 +180,7 @@ kill -s "$SUDO_TEST_CHANNEL_SIGNAL" "$PPID" STUB if SUDO_TEST_CHANNEL_SIGNAL="$signal" run_channel stable; then fail "$signal was ignored"; fi assert_boundary_cold "$signal" - if grep -q '^step:omarchy-hook ' "$SUDO_TEST_LOG"; then fail "$signal reached an update hook"; fi + if grep -q '^step:omarchy-hook post-update$' "$SUDO_TEST_LOG"; then fail "$signal reached the post-update hook"; fi pass "$signal stops the channel transition and revokes authorization" done diff --git a/test/shell.d/channel-test.sh b/test/shell.d/channel-test.sh index 5640e292..cbbfc6e8 100644 --- a/test/shell.d/channel-test.sh +++ b/test/shell.d/channel-test.sh @@ -127,7 +127,7 @@ assert_log_line() { } run_channel stable -assert_log_line $'refresh\tstable\tdefer-hook' "stable refreshes the stable pacman channel" +assert_log_line $'refresh\tstable' "stable refreshes the stable pacman channel" assert_log_line $'update-pacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "stable installs stable Omarchy packages" assert_log_line $'unlink\t--no-reboot' "stable restores the package-backed Omarchy path without an early reboot prompt" assert_log_line $'update\t-y\tOMARCHY_PATH='"$package_root" "stable runs the normal update pipeline from the package-backed path" @@ -137,7 +137,7 @@ fi pass "stable does not require reboot when already package-backed" run_channel rc -assert_log_line $'refresh\trc\tdefer-hook' "rc refreshes the rc pacman channel" +assert_log_line $'refresh\trc' "rc refreshes the rc pacman channel" assert_log_line $'update-pacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "rc installs rc Omarchy packages" assert_log_line $'unlink\t--no-reboot' "rc restores the package-backed Omarchy path without an early reboot prompt" assert_log_line $'update\t-y\tOMARCHY_PATH='"$package_root" "rc runs the normal update pipeline from the package-backed path" @@ -145,7 +145,7 @@ assert_log_line $'update\t-y\tOMARCHY_PATH='"$package_root" "rc runs the normal active_checkout="$test_tmp/active-checkout" cp -a "$package_root" "$active_checkout" OMARCHY_TEST_PATH="$active_checkout" run_channel edge -assert_log_line $'refresh\tedge\tdefer-hook' "edge refreshes the edge pacman channel" +assert_log_line $'refresh\tedge' "edge refreshes the edge pacman channel" assert_log_line $'update-pacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "edge installs development Omarchy packages" assert_log_line $'unlink\t--no-reboot' "edge unlinks dev without an early reboot prompt" assert_log_line $'state\tset\treboot-required' "edge marks reboot required when leaving dev" @@ -161,7 +161,7 @@ if run_channel dev >"$test_tmp/occupied.out" 2>"$test_tmp/occupied.err"; then fi grep -q "already exists and is not a git checkout" "$test_tmp/occupied.err" || fail "dev explains occupied checkout paths" "$(cat "$test_tmp/occupied.err")" -if grep -Fx $'refresh\tedge\tdefer-hook' "$log_file" >/dev/null; then +if grep -Fx $'refresh\tedge' "$log_file" >/dev/null; then fail "dev validates checkout path before changing packages" "$(cat "$log_file")" fi pass "dev refuses occupied non-checkout paths before package changes" @@ -169,17 +169,15 @@ pass "dev refuses occupied non-checkout paths before package changes" rmdir "$checkout" run_channel dev assert_log_line $'gum\tconfirm\t--default=false\tSwitch to dev channel?' "dev asks for confirmation" -assert_log_line $'refresh\tedge\tdefer-hook' "dev refreshes the edge pacman channel" +assert_log_line $'refresh\tedge' "dev refreshes the edge pacman channel" assert_log_line $'update-pacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "dev installs development Omarchy packages" assert_log_line $'git\tclone\thttps://github.com/omacom/omarchy.git\t'"$checkout" "dev clones the source checkout to ~/omarchy" assert_log_line $'link\t'"$checkout"$'\t--no-reboot' "dev links ~/omarchy without an early reboot prompt" assert_log_line $'state\tset\treboot-required' "dev defers the reboot prompt to the update pipeline" assert_log_line $'update\t-y\tOMARCHY_PATH='"$checkout" "dev runs the normal update pipeline from the source checkout" -[[ $(grep -E '^(git|link|state|refresh|sudo|update)' "$log_file" | sed '/run-deferred/d') == $'git\tclone\thttps://github.com/omacom/omarchy.git\t'"$checkout"$'\nlink\t'"$checkout"$'\t--no-reboot\nstate\tset\treboot-required\nrefresh\tedge\tdefer-hook\nupdate-pacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev\nupdate\t-y\tOMARCHY_PATH='"$checkout" ]] || +[[ $(grep -E '^(git|link|state|refresh|sudo|update)' "$log_file") == $'git\tclone\thttps://github.com/omacom/omarchy.git\t'"$checkout"$'\nlink\t'"$checkout"$'\t--no-reboot\nstate\tset\treboot-required\nrefresh\tedge\nupdate-pacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev\nupdate\t-y\tOMARCHY_PATH='"$checkout" ]] || fail "dev activates the checkout before changing or updating packages" "$(cat "$log_file")" pass "dev activates the checkout before changing or updating packages" -[[ $(tail -1 "$log_file") == $'refresh\tedge\trun-deferred' ]] || fail "channel refresh hook must run after the complete update" -pass "channel changes defer the refresh hook until all update work finishes" OMARCHY_TEST_PATH="$checkout" run_channel stable assert_log_line $'unlink\t--no-reboot' "switching from dev to stable unlinks without an early reboot prompt" diff --git a/test/shell.d/fixtures/sudo-boundary-test.sh b/test/shell.d/fixtures/sudo-boundary-test.sh index a8c05d45..43b0a66f 100644 --- a/test/shell.d/fixtures/sudo-boundary-test.sh +++ b/test/shell.d/fixtures/sudo-boundary-test.sh @@ -92,6 +92,11 @@ fi if [[ $step == "omarchy-hook" || $step == "omarchy-update-mise" ]]; then [[ ! -e $SUDO_TEST_CACHE ]] || exit 91 fi +if [[ -n ${SUDO_TEST_REMOVE_WRAPPER_STEP:-} && "$step $*" == $SUDO_TEST_REMOVE_WRAPPER_STEP ]]; then + # Model a package transaction replacing the running tree with a release + # that predates the wrapper. + /usr/bin/rm -f "$OMARCHY_PATH/default/omarchy/sudo-no-update/sudo" +fi if [[ ${SUDO_TEST_FAIL_STEP:-} == "$step" ]]; then # Model a misbehaving child leaving state behind, then failing. Cleanup must # still revoke it. This never invokes real sudo or exercises a privilege flaw. @@ -123,7 +128,7 @@ ln -s ../bin/test-step "$SUDO_TEST_ROOT/mock/pacman" reset_boundary() { : >"$SUDO_TEST_LOG" /usr/bin/rm -f "$SUDO_TEST_CACHE" - unset SUDO_TEST_FAIL_STEP SUDO_TEST_SIGNAL_STEP SUDO_TEST_SUDO_FAIL SUDO_TEST_REVOKE_FAIL SUDO_TEST_UNSUPPORTED + unset SUDO_TEST_FAIL_STEP SUDO_TEST_SIGNAL_STEP SUDO_TEST_SUDO_FAIL SUDO_TEST_REVOKE_FAIL SUDO_TEST_UNSUPPORTED SUDO_TEST_REMOVE_WRAPPER_STEP } assert_boundary_cold() { [[ ! -e $SUDO_TEST_CACHE ]] || fail "$1 left cached authorization" diff --git a/test/shell.d/update-hook-security-test.sh b/test/shell.d/update-hook-security-test.sh index 0181b800..ed18c756 100755 --- a/test/shell.d/update-hook-security-test.sh +++ b/test/shell.d/update-hook-security-test.sh @@ -24,9 +24,9 @@ for args in '-y' ''; do python3 - "$SUDO_TEST_LOG" <<'PY' import sys s=open(sys.argv[1]).read().splitlines() -positions=[next(i for i,line in enumerate(s) if line.startswith(prefix)) for prefix in ['step:omarchy-update-restart --services-only','step:omarchy-update-stay-awake stop','step:yay','step:omarchy-hook post-update','step:omarchy-update-mise','step:omarchy-update-restart --reboot-only']] +positions=[next(i for i,line in enumerate(s) if line.startswith(prefix)) for prefix in ['step:omarchy-update-restart --services-only','step:yay','step:omarchy-hook post-update','step:omarchy-update-mise','step:omarchy-update-stay-awake stop','step:omarchy-update-restart --reboot-only']] assert positions==sorted(positions), s -assert not any(line.startswith('sudo -N ') for line in s[positions[3]:]), s +assert not any(line.startswith('sudo -N ') for line in s[positions[2]:]), s PY pass "update $args runs privileged phases before hooks and exits cold" done @@ -63,21 +63,20 @@ if run_update -y; then fail "unsupported sudo must prevent mixed-trust work"; fi assert_boundary_cold "unsupported sudo" pass "unsupported sudo fails without running update steps" -for mode in normal defer-hook run-deferred; do - reset_boundary - "$SUDO_TEST_ROOT/bin/omarchy-refresh-pacman" stable "$mode" >"$boundary_tmp/output" 2>&1 || fail "refresh $mode failed" "$(<"$boundary_tmp/output")" - assert_boundary_cold "refresh $mode" - python3 - "$SUDO_TEST_LOG" "$mode" <<'PY' +reset_boundary +"$SUDO_TEST_ROOT/bin/omarchy-refresh-pacman" stable >"$boundary_tmp/output" 2>&1 || fail "refresh failed" "$(<"$boundary_tmp/output")" +assert_boundary_cold "refresh" +python3 - "$SUDO_TEST_LOG" <<'PY' import sys -s=open(sys.argv[1]).read().splitlines();mode=sys.argv[2] -hooks=[i for i,l in enumerate(s) if l.startswith('step:omarchy-hook')] -priv=[i for i,l in enumerate(s) if l.startswith('sudo -N ')] -assert bool(hooks)==(mode!='defer-hook'), s -assert bool(priv)==(mode!='run-deferred'), s -if hooks: assert not any(i>hooks[0] for i in priv),s +s=open(sys.argv[1]).read().splitlines() +hook=next(i for i,l in enumerate(s) if l=='step:omarchy-hook pre-refresh-pacman') +copies=[i for i,l in enumerate(s) if l.startswith('sudo -N cp ')] +transaction=next(i for i,l in enumerate(s) if l.startswith('step:pacman ')) +assert len(copies)==4 and max(copies) < hook < transaction, s +assert s[hook-1]=='sudo -k' and s[hook+1]=='sudo -k', s +assert all(l in ('sudo -h','sudo -k') or l.startswith('sudo -N ') for l in s if l.startswith('sudo ')), s PY - pass "refresh $mode preserves the final cold hook boundary" -done +pass "refresh runs the hook cold between the config re-sync and the transaction" for step in pacman omarchy-hook; do reset_boundary diff --git a/test/shell.d/update-sequence-test.sh b/test/shell.d/update-sequence-test.sh index 994dc493..a1013308 100755 --- a/test/shell.d/update-sequence-test.sh +++ b/test/shell.d/update-sequence-test.sh @@ -73,10 +73,10 @@ expected_steps() { omarchy-update-analyze-logs \ omarchy-update-status \ omarchy-update-restart \ - omarchy-update-stay-awake \ omarchy-update-aur-pkgs \ omarchy-hook \ omarchy-update-mise \ + omarchy-update-stay-awake \ omarchy-update-restart }