Merge pull request #9467 from AFOliveira/codex/om-sec-12-update-inhibitor-identity

[codex] OM-SEC-12: Bind update inhibitor cleanup to process identity

Reported-by: Afonso "AFOliveira" Oliveira
This commit is contained in:
Erik Melton authored and GitHub committed 2026-09-25 15:56:39 +02:00
commit d201fb9564
38 files changed
+3548 -422

No files matched your search

+52 -5
View File
@@ -1,10 +1,25 @@
#!/bin/bash
#!/bin/bash -p
# omarchy:summary=Set the Omarchy package channel.
# omarchy:args=<stable|rc|edge|dev>
# omarchy:requires-sudo=true
if [[ $- != *p* ]]; then
echo "Refusing an unsafe Bash startup for channel switching." >&2
exit 126
fi
security_entrypoint=$(/usr/bin/readlink -e -- "${BASH_SOURCE[0]}") || exit 126
source "${security_entrypoint%/*}/omarchy-security-functions" || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
set -euo pipefail
omarchy_security_sanitize_bash_environment "$0" "$@"
omarchy_security_require_source_root "$0"
user_path=$PATH
# Traps first, so a signal or failure during the entry revocation still
# exits through the cleanup path.
omarchy_security_install_sudo_cleanup_traps
omarchy_security_revoke_sudo_timestamp || exit 1
omarchy_security_enable_no_update_sudo
usage() { echo "Usage: omarchy-channel-set [stable|rc|edge|dev]"; }
fail() { echo "Error: $*" >&2; exit 1; }
@@ -33,12 +48,40 @@ validate_dev_checkout() {
}
link_dev_checkout() {
local checkout="$1"
local checkout="$1" required
[[ -d $checkout/.git ]] || git clone https://github.com/omacom/omarchy.git "$checkout"
# Check the destination before changing /etc/omarchy.conf or sudo's path.
# An existing checkout is not pulled automatically and may predate this policy.
for required in bin/omarchy-security-functions bin/omarchy-update bin/omarchy-refresh-pacman default/omarchy/sudo-no-update/sudo; do
if [[ ! -f $checkout/$required || ! -r $checkout/$required ||
( $required != "bin/omarchy-security-functions" && ! -x $checkout/$required ) ]]; then
fail "Update the checkout before switching to dev; missing required update support in $required."
fi
done
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 <<EOF
The destination predates command-scoped sudo, so this switch stops before its update.
Packages are switched. Run 'omarchy update' from a new terminal to finish, then reboot if prompted.
EOF
exit 3
}
wrapper_present() {
[[ -f $OMARCHY_PATH/default/omarchy/sudo-no-update/sudo && -x $OMARCHY_PATH/default/omarchy/sudo-no-update/sudo ]]
}
(( $# > 0 )) || { usage; exit 1; }
dev_checkout=""
@@ -83,13 +126,16 @@ fi
if [[ -n $dev_checkout ]]; then
link_dev_checkout "$dev_checkout"
export OMARCHY_PATH="$dev_checkout"
export PATH="$OMARCHY_PATH/bin:$PATH"
omarchy_security_enable_no_update_sudo
omarchy-state set reboot-required
fi
omarchy-refresh-pacman "$pacman_channel"
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
@@ -98,6 +144,7 @@ if [[ -z $dev_checkout ]]; then
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 -y
OMARCHY_UPDATE_USER_PATH="$user_path" "$OMARCHY_PATH/bin/omarchy-update" -y
+37 -13
View File
@@ -1,26 +1,50 @@
#!/bin/bash
#!/bin/bash -p
# omarchy:summary=Overwrite the package configuration for /etc/pacman with the Omarchy default of using its dedicated mirrors and repositories, then update all packages.
# omarchy:requires-sudo=true
sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak
sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
channel="${1:-stable}"
if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
echo "Error: Invalid channel '$channel'. Must be one of: stable, rc, edge"
exit 1
if [[ $- != *p* ]]; then
echo "Refusing an unsafe Bash startup." >&2
exit 126
fi
echo "Setting channel to $channel"
echo
security_entrypoint=$(/usr/bin/readlink -e -- "${BASH_SOURCE[0]}") || exit 126
source "${security_entrypoint%/*}/omarchy-security-functions" || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
set -e
omarchy_security_sanitize_bash_environment "$0" "$@"
omarchy_security_require_source_root "$0"
# 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
# Traps first, so a signal or failure during the entry revocation still
# exits through the cleanup path.
omarchy_security_install_sudo_cleanup_traps
omarchy_security_revoke_sudo_timestamp || exit 1
omarchy_security_enable_no_update_sudo
channel="${1:-stable}"
if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
echo "Invalid channel: $channel" >&2
exit 2
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
# Allow user customization of /etc/pacman.conf before the upgrade runs
omarchy-hook pre-refresh-pacman
# 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
+1 -1
View File
@@ -220,7 +220,7 @@ fi
# never owned, a yes takes that with it, and saying so is the prompt's job.
# Without a terminal to ask in, keeping everything is the answer.
data_removed=false
if [[ -d $HOME/.hermes || -d $HOME/.config/Hermes ]] && [[ -t 0 ]] && omarchy-cmd-present gum; then
if [[ -d $HOME/.hermes || -d $HOME/.config/Hermes ]] && [[ -t 0 ]]; then
# du answers non-zero when either directory is missing, and pipefail would
# turn that into an aborted removal; the size is worth no such thing.
size=$(du -shc "$HOME/.hermes" "$HOME/.config/Hermes" 2>/dev/null | tail -1 | cut -f1 || true)
+1 -1
View File
@@ -61,7 +61,7 @@ gtk-update-icon-cache "$HOME/.local/share/icons/hicolor" &>/dev/null || true
# front of the user with the size rather than left silent. Without a terminal
# to ask in, keeping it is the answer.
state_removed=false
if [[ -d $HOME/.openclaw && -t 0 ]] && omarchy-cmd-present gum; then
if [[ -d $HOME/.openclaw && -t 0 ]]; then
size=$(du -sh "$HOME/.openclaw" 2>/dev/null | cut -f1)
if gum confirm --default=false "Also delete ~/.openclaw ($size: chats, memories, credentials, and downloaded plugins)?"; then
rm -rf "$HOME/.openclaw"
+56 -16
View File
@@ -61,6 +61,21 @@ relock_session() {
return 1
}
# A short timeout keeps an unresponsive user bus from stalling the restart;
# busctl would otherwise wait 25 seconds per probe.
notifications_ready() {
[[ $(busctl --user --timeout=1s call org.freedesktop.DBus /org/freedesktop/DBus \
org.freedesktop.DBus NameHasOwner s org.freedesktop.Notifications 2>/dev/null) == "b true" ]]
}
# Core IPC can answer before the notification plugin has registered its bus
# name. Restore an existing notification service before update hooks or setup
# invitations send their one-time toasts; a disabled service need not appear.
notifications_were_running=0
if notifications_ready; then
notifications_were_running=1
fi
# Each kill stops the oldest matching instance and only returns once it has
# fully exited, so the no-duplicate launch below can't race a dying shell.
while timeout 5 quickshell kill -p "$CONFIG_DIR" --any-display >/dev/null 2>&1; do :; done
@@ -69,25 +84,50 @@ while timeout 5 quickshell kill -p "$CONFIG_DIR" --any-display >/dev/null 2>&1;
# not transient variables from a terminal, SSH connection, or development tool.
hyprctl dispatch 'hl.dsp.exec_cmd("omarchy-launch-shell")' >/dev/null
shell_ready=0
for (( attempt = 0; attempt < 20; attempt++ )); do
if OMARCHY_PATH="$session_omarchy_path" OMARCHY_SHELL_IPC_TIMEOUT=0.5s omarchy-shell shell ping >/dev/null 2>&1; then
# The session stays compositor-locked after the old lock client died, so
# re-acquire the lock and let the user authenticate out of it.
if (( relock )) && ! relock_session; then
echo "Omarchy shell restarted, but the session lock was not re-secured." >&2
exit 1
fi
# Invitation toasts (like Voxtype/fingerprint setup) die with the old
# shell, and their notify-send waiters hang forever: the dying server
# never emits NotificationClosed. A still-running omarchy-*-invitation
# unit is therefore an unanswered invitation — re-run it so its toast
# reappears on the new shell. Answered invitations have already exited
# and been collected, so the glob no longer matches them.
systemctl --user try-restart 'omarchy-*-invitation.service' 2>/dev/null || true
exit 0
shell_ready=1
break
fi
sleep 0.1
done
if (( shell_ready == 0 )); then
echo "Omarchy shell did not become ready after restart." >&2
exit 1
fi
echo "Omarchy shell did not become ready after restart." >&2
exit 1
# The session stays compositor-locked after the old lock client died, so
# re-acquire the lock and let the user authenticate out of it. This comes
# first and depends on nothing else: a user stranded behind the failsafe must
# not wait on the notification plugin, which may be slow or absent.
if (( relock )) && ! relock_session; then
echo "Omarchy shell restarted, but the session lock was not re-secured." >&2
exit 1
fi
# Core IPC answers before the notification plugin has registered its bus
# name, so wait for it separately before one-time toasts are sent.
if (( notifications_were_running )); then
notifications_restored=0
for (( attempt = 0; attempt < 20; attempt++ )); do
if notifications_ready; then
notifications_restored=1
break
fi
sleep 0.1
done
if (( notifications_restored == 0 )); then
echo "Omarchy shell restarted, but its notification service did not become ready." >&2
exit 1
fi
fi
# Invitation toasts (like Voxtype/fingerprint setup) die with the old
# shell, and their notify-send waiters hang forever: the dying server
# never emits NotificationClosed. A still-running omarchy-*-invitation
# unit is therefore an unanswered invitation — re-run it so its toast
# reappears on the new shell. Answered invitations have already exited
# and been collected, so the glob no longer matches them.
systemctl --user try-restart 'omarchy-*-invitation.service' 2>/dev/null || true
exit 0
+137
View File
@@ -0,0 +1,137 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Provide internal helpers for command-scoped sudo authentication
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
echo "omarchy-security-functions is an internal function library." >&2
exit 64
fi
omarchy_security_require_privileged_bash_startup() {
[[ $- == *p* ]] || return 1
/usr/bin/env -i /usr/bin/bash -p -c '
mapfile -d "" -t argv <"/proc/$1/cmdline" || exit 1
executable=$(/usr/bin/readlink -e -- "/proc/$1/exe") || exit 1
[[ $executable == "/usr/bin/bash" &&
( ${argv[0]:-} == "/bin/bash" || ${argv[0]:-} == "/usr/bin/bash" ) &&
${argv[1]:-} == "-p" ]]
' omarchy-bash-startup "$$"
}
omarchy_security_sanitize_bash_environment() {
local script=$1
shift
local entry name environment_fd environment_pid
local -a unsets=()
# Read the raw environment: privileged Bash ignores exported functions, but
# leaves their records for ordinary child interpreters to import later.
exec {environment_fd}< <(/usr/bin/env -0)
environment_pid=$!
while IFS= read -r -d '' entry <&"$environment_fd"; do
name=${entry%%=*}
case "$name" in
BASH_ENV|ENV|SHELLOPTS|BASHOPTS|PS4|CDPATH|GLOBIGNORE|BASH_FUNC_*%%)
unsets+=(-u "$name")
;;
esac
done
exec {environment_fd}<&-
wait "$environment_pid" || return 1
if (( ${#unsets[@]} > 0 )); then
exec /usr/bin/env "${unsets[@]}" /usr/bin/bash -p -- "$script" "$@"
fi
}
omarchy_security_require_source_root() {
local command_source command_name=${1##*/}
command_source=$(/usr/bin/readlink -e -- "$1") || return 1
# A runtime root selects the code used by this invocation. Accept the
# canonical checkout containing the entrypoint or the package's bin links.
if [[ ${OMARCHY_PATH:-} != /* || $(/usr/bin/realpath -e -- "$OMARCHY_PATH") != "$OMARCHY_PATH" ]] ||
! { [[ $command_source == "$OMARCHY_PATH/bin/$command_name" ]] ||
[[ $OMARCHY_PATH == "/usr/share/omarchy" && $command_source == "/usr/bin/$command_name" ]]; }; then
echo "OMARCHY_PATH does not match this Omarchy command." >&2
return 1
fi
}
omarchy_security_sudo_supports_no_update() {
local help
help=$(LC_ALL=C /usr/bin/sudo -h 2>&1) || return 1
/usr/bin/grep -Eq '^usage: sudo .*\[[^]]*N[^]]*\]' <<< "$help"
}
omarchy_security_revoke_sudo_timestamp() {
/usr/bin/sudo -k
}
omarchy_security_exit_with_revoked_sudo() {
local status=$1
local message=${2:-Could not invalidate cached sudo authorization.}
trap - EXIT HUP INT TERM
if ! omarchy_security_revoke_sudo_timestamp; then
echo "$message" >&2
(( status != 0 )) || status=1
fi
exit "$status"
}
omarchy_security_install_signal_exit_traps() {
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
}
omarchy_security_install_sudo_cleanup_traps() {
OMARCHY_SECURITY_SUDO_CLEANUP_MESSAGE=${1:-Could not invalidate cached sudo authorization.}
trap omarchy_security_run_sudo_cleanup_trap EXIT
omarchy_security_install_signal_exit_traps
}
omarchy_security_enable_no_update_sudo() {
local wrapper_dir="$OMARCHY_PATH/default/omarchy/sudo-no-update"
if ! omarchy_security_sudo_supports_no_update; then
echo "This sudo does not support --no-update; refusing mixed-trust work." >&2
return 1
fi
if [[ ! -f $wrapper_dir/sudo || ! -x $wrapper_dir/sudo ]]; then
echo "The command-scoped sudo wrapper is missing." >&2
return 1
fi
PATH="$wrapper_dir:$OMARCHY_PATH/bin:/usr/bin:/usr/sbin:/bin:/sbin"
OMARCHY_SUDO_NO_UPDATE=1
export PATH OMARCHY_SUDO_NO_UPDATE
}
omarchy_security_run_sudo_cleanup_trap() {
local status=$?
omarchy_security_exit_with_revoked_sudo "$status" \
"${OMARCHY_SECURITY_SUDO_CLEANUP_MESSAGE:-Could not invalidate cached sudo authorization.}"
}
omarchy_security_assert_root_directory() {
local path=$1 expected_mode=$2 canonical owner actual_mode
[[ $path == /* && -d $path && ! -L $path ]] || return 1
canonical=$(/usr/bin/realpath -e -- "$path") || return 1
[[ $canonical == "$path" ]] || return 1
read -r owner actual_mode < <(/usr/bin/stat -Lc '%u %a' -- "$path") || return 1
[[ $owner == "0" && $actual_mode == "$expected_mode" ]]
}
omarchy_security_prepare_private_root_directory() {
local path=$1 parent=$2
omarchy_security_assert_root_directory "$parent" 755 || return 1
if [[ -e $path || -L $path ]]; then
omarchy_security_assert_root_directory "$path" 700
else
/usr/bin/install -d -o root -g root -m 0700 -- "$path" || return 1
omarchy_security_assert_root_directory "$path" 700
fi
}
+417 -39
View File
@@ -1,70 +1,448 @@
#!/bin/bash
#!/bin/bash -p
# omarchy:summary=Toggle passwordless sudo for the current user.
# omarchy:args=[MINUTES]
# omarchy:requires-sudo=true
NOPASSWD_FILE="/etc/sudoers.d/99-omarchy-nopasswd-${USER}"
TIMER_NAME="omarchy-nopasswd-expire-${USER}"
MINUTES=${1:-15}
if [[ $1 && ! $1 =~ ^[0-9]+$ ]]; then
echo "Usage: omarchy-sudo-passwordless [MINUTES]" >&2
exit 1
if [[ $- != *p* && ${BASH_SOURCE[0]} == "$0" ]]; then
echo "Refusing an unsafe Bash startup for passwordless sudo." >&2
exit 126
fi
arm_expiry() {
if sudo systemd-run --on-active=${MINUTES}m --timer-property=AccuracySec=1s --unit="$TIMER_NAME" \
rm -f -- "$NOPASSWD_FILE"; then
security_entrypoint=$(/usr/bin/readlink -e -- "${BASH_SOURCE[0]}") || exit 126
source "${security_entrypoint%/*}/omarchy-security-functions" || exit 126
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
omarchy_security_require_privileged_bash_startup || {
echo "Refusing an unsafe Bash startup for passwordless sudo." >&2
exit 126
}
omarchy_security_sanitize_bash_environment "$0" "$@" || exit 126
fi
set -euo pipefail
readonly DEFAULT_MINUTES=15
readonly MAX_MINUTES=1440
readonly LOCK_FILE=/run/lock/omarchy-sudo-passwordless.lock
readonly BOOT_CLEANUP_FILE=/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf
readonly PACKAGE_HOOK=/usr/share/libalpm/hooks/05-omarchy-passwordless-revoke.hook
readonly REMOVAL_BLOCKER=/run/omarchy-sudo-passwordless-package-removing
readonly MIGRATION_MARKER=/var/lib/omarchy/migrations/1788163635
readonly QUARANTINE_DIR=/var/lib/omarchy/sudoers-quarantine
readonly INSTALLED_SELF=/usr/bin/omarchy-sudo-passwordless
readonly STATUS_INACTIVE=3
usage() {
echo "Usage: omarchy-sudo-passwordless [MINUTES]" >&2
echo "MINUTES must be between 1 and $MAX_MINUTES." >&2
exit 1
}
valid_minutes() {
[[ $1 =~ ^0*[1-9][0-9]{0,3}$ ]] && ((10#$1 <= MAX_MINUTES))
}
valid_uid() {
[[ $1 =~ ^0*[1-9][0-9]{0,9}$ ]] && ((10#$1 <= 4294967294))
}
valid_account_name() {
[[ $1 =~ ^[a-z_][a-z0-9_-]{0,31}\$?$ ]] && (( ${#1} <= 32 ))
}
resolve_account() {
local uid="$1" entry
valid_uid "$uid" || return 1
entry=$(/usr/bin/getent passwd "$((10#$uid))") || return 1
IFS=: read -r ACCOUNT_NAME _ ACCOUNT_UID _ _ _ _ <<<"$entry"
[[ $ACCOUNT_UID == "$((10#$uid))" ]] || return 1
# Sudoers has metacharacters, and it reads an upper-case word such as ALICE
# as an alias reference rather than a user. Accounts use this portable
# lower-case subset; refusing anything else is safer than attempting to
# quote privileged policy syntax.
valid_account_name "$ACCOUNT_NAME" || return 1
ACCOUNT_UID=$((10#$uid))
}
verify_sudo_caller() {
local requested_uid="$1"
((EUID == 0)) || return 1
valid_uid "$requested_uid" || return 1
[[ ${SUDO_UID:-} =~ ^[0-9]+$ ]] || return 1
((10#$SUDO_UID == 10#$requested_uid)) || return 1
resolve_account "$requested_uid"
}
with_root_lock() {
local fd rc=0
# The boot cleanup cannot depend on STATE_DIR or RUNTIME_DIR being healthy:
# those are exactly the kinds of partial-install state it must fail closed
# through. /run/lock is established by the OS before sysinit services run.
omarchy_security_assert_root_directory /run 755 || return 1
[[ -d /run/lock && ! -L /run/lock ]] || return 1
[[ $(/usr/bin/stat -Lc '%u' /run/lock) == 0 ]] || return 1
! ((8#$(/usr/bin/stat -Lc '%a' /run/lock) & 022)) || return 1
exec {fd}>"$LOCK_FILE" || return 1
/usr/bin/chown root:root "$LOCK_FILE" || return 1
/usr/bin/chmod 0600 "$LOCK_FILE" || return 1
# Grant operations are short. A stalled holder must not hang a caller
# indefinitely, least of all pacman's pre-transaction hook.
/usr/bin/flock -x -w 60 "$fd" || return 1
"$@" || rc=$?
/usr/bin/flock -u "$fd" || rc=1
exec {fd}>&-
return "$rc"
}
rule_file() {
printf '/etc/sudoers.d/99-omarchy-nopasswd-%s' "$1"
}
# The sudoers rule is the only grant record. A missing file is distinct from
# an unreadable, unsafe, or administrator-modified file.
read_grant() {
local file contents
file=$(rule_file "$1")
[[ -e $file || -L $file ]] || return "$STATUS_INACTIVE"
verify_root_path "$file" && [[ -f $file ]] || return 2
contents=$(/usr/bin/cat -- "$file") || return 2
[[ $contents =~ ^([a-z_][a-z0-9_-]*\$?)\ ALL=\(ALL\)\ NOTAFTER=([0-9]{14}Z)\ NOPASSWD:\ ALL$ ]] || return 2
GRANT_NAME=${BASH_REMATCH[1]}
GRANT_DEADLINE=${BASH_REMATCH[2]}
valid_account_name "$GRANT_NAME" || return 2
}
# Returns 0 for a rule this command generated, 1 for anything else under the
# owned prefix (preserved as administrator policy), and 2 when unreadable.
classify_generated_rule() {
local file=$1 suffix contents name
[[ -f $file && ! -L $file ]] || return 1
contents=$(/usr/bin/cat -- "$file") || return 2
suffix=${file##*/99-omarchy-nopasswd-}
# The legacy command wrote the caller's unvalidated name into both the
# filename and the rule. That exact relationship is its fingerprint, so an
# account the current policy would reject still has its old grant removed.
if [[ $contents == "$suffix ALL=(ALL) NOPASSWD: ALL" ]]; then
return 0
fi
echo "Failed to schedule passwordless sudo expiry. Revoking access now." >&2
if ! sudo rm -f -- "$NOPASSWD_FILE"; then
echo "CRITICAL: Could not remove $NOPASSWD_FILE. Remove it as root immediately." >&2
[[ $suffix =~ ^[0-9]+$ ]] || return 1
name=${contents%' ALL=(ALL) NOPASSWD: ALL'}
if valid_account_name "$name" && [[ $contents == "$name ALL=(ALL) NOPASSWD: ALL" ]]; then
return 0
fi
return 1
name=${contents%%' ALL=(ALL) NOTAFTER='*}
valid_account_name "$name" && [[ $contents =~ ^[a-z_][a-z0-9_-]*\$?\ ALL=\(ALL\)\ NOTAFTER=[0-9]{14}Z\ NOPASSWD:\ ALL$ ]]
}
cleanup_uid_locked() {
local file
file=$(rule_file "$1")
[[ -e $file || -L $file ]] || return 0
verify_root_path "$file" && classify_generated_rule "$file" || return 1
/usr/bin/rm -f -- "$file" && [[ ! -e $file && ! -L $file ]]
}
# The generated prefix is reserved: boot cleanup and the package hook already
# remove everything in it, and the legacy writer could produce a rule whose
# body differs from its filename. Nothing unrecognized may stay live there, but
# its content is kept for the administrator instead of being deleted.
quarantine_foreign_rule() {
local file=$1 target
if [[ ! -e /var/lib/omarchy && ! -L /var/lib/omarchy ]]; then
/usr/bin/install -d -o root -g root -m 0755 -- /var/lib/omarchy || return 1
fi
omarchy_security_prepare_private_root_directory "$QUARANTINE_DIR" /var/lib/omarchy || return 1
# A legacy filename can already be close to NAME_MAX, so the destination
# name is fixed and the original name travels beside it.
target=$(/usr/bin/mktemp -d "$QUARANTINE_DIR/XXXXXXXXXX") || return 1
/usr/bin/printf '%s\n' "${file##*/}" >"$target/name" || return 1
/usr/bin/mv -fT -- "$file" "$target/policy" && [[ ! -e $file && ! -L $file ]] || return 1
echo "Moved unrecognized sudoers policy $file to $target/policy" >&2
}
cleanup_all_locked() {
local file classification failed=0
verify_root_path /etc/sudoers.d || return 1
for file in /etc/sudoers.d/99-omarchy-nopasswd-*; do
[[ -e $file || -L $file ]] || continue
if classify_generated_rule "$file"; then
if ! /usr/bin/rm -f -- "$file" || [[ -e $file || -L $file ]]; then
failed=1
fi
else
classification=$?
if (( classification != 1 )) || ! quarantine_foreign_rule "$file"; then
failed=1
fi
fi
done
return "$failed"
}
verify_root_path() {
local file=$1 owner mode canonical current
[[ ( -f $file || -d $file ) && ! -L $file ]] || return 1
canonical=$(/usr/bin/realpath -e -- "$file") || return 1
[[ $canonical == "$file" ]] || return 1
owner=$(/usr/bin/stat -Lc '%u' -- "$file") || return 1
mode=$(/usr/bin/stat -Lc '%a' -- "$file") || return 1
[[ $owner == 0 && $mode =~ ^[0-7]+$ ]] && ! ((8#$mode & 022)) || return 1
current=${file%/*}
while :; do
[[ -d $current && ! -L $current ]] || return 1
canonical=$(/usr/bin/realpath -e -- "$current") || return 1
[[ $canonical == "$current" ]] || return 1
read -r owner mode < <(/usr/bin/stat -Lc '%u %a' -- "$current") || return 1
[[ $owner == 0 && $mode =~ ^[0-7]+$ ]] && ! ((8#$mode & 022)) || return 1
[[ $current == / ]] && break
current=${current%/*}
[[ -n $current ]] || current=/
done
}
verify_boot_cleanup() {
local active_rules hook
[[ ! -e $REMOVAL_BLOCKER && ! -L $REMOVAL_BLOCKER ]] || return 1
verify_root_path "$BOOT_CLEANUP_FILE" || return 1
active_rules=$(/usr/bin/awk '!/^[[:space:]]*(#|$)/ { print }' "$BOOT_CLEANUP_FILE") || return 1
[[ $active_rules == 'r! /etc/sudoers.d/99-omarchy-nopasswd-*' ]] || return 1
verify_root_path "$PACKAGE_HOOK" || return 1
hook=$(/usr/bin/cat -- "$PACKAGE_HOOK") || return 1
[[ $hook == '[Trigger]
Operation = Upgrade
Operation = Remove
Type = Package
Target = omarchy-settings
Target = omarchy-settings-dev
[Action]
Description = Revoking temporary Omarchy sudo grants before settings changes...
When = PreTransaction
Exec = /usr/bin/omarchy-sudo-passwordless __package-removing
AbortOnFail' ]]
}
package_removing_locked() {
# ALPM must abort before removing the helper or boot cleanup if revocation
# fails. The marker also blocks publication after this lock is released.
(umask 077; : >"$REMOVAL_BLOCKER") || return 1
/usr/bin/rm -f -- /etc/sudoers.d/99-omarchy-nopasswd-* || return 1
cleanup_all_locked
}
migration_complete() {
[[ -f $MIGRATION_MARKER && ! -s $MIGRATION_MARKER ]] && verify_root_path "$MIGRATION_MARKER"
}
migrate_locked() {
local directory
if migration_complete; then
return 0
fi
[[ ! -e $MIGRATION_MARKER && ! -L $MIGRATION_MARKER ]] || return 1
verify_root_path /var/lib || return 1
for directory in /var/lib/omarchy /var/lib/omarchy/migrations; do
if [[ ! -e $directory && ! -L $directory ]]; then
/usr/bin/install -d -o root -g root -m 0755 -- "$directory" || return 1
fi
verify_root_path "$directory" || return 1
done
cleanup_all_locked || return 1
# The empty marker is written only after cleanup succeeds, under the same
# machine lock. Later accounts need no sudo and cannot revoke newer grants.
/usr/bin/install -o root -g root -m 0644 /dev/null "$MIGRATION_MARKER"
}
# Old callbacks only remove an expired current rule. Renewing a grant never
# needs a second state file or a stored timer generation to identify it.
expire_locked() {
local status now
if read_grant "$1"; then
now=$(/usr/bin/date -u +%Y%m%d%H%M%SZ) || return 2
[[ $now < $GRANT_DEADLINE ]] && return 0
cleanup_uid_locked "$1"
else
status=$?
if (( status == STATUS_INACTIVE )); then
return 0
else
cleanup_uid_locked "$1"
fi
fi
}
status_locked() {
local status now
resolve_account "$1" || return 2
if read_grant "$1"; then
[[ $GRANT_NAME == "$ACCOUNT_NAME" ]] || return 2
now=$(/usr/bin/date -u +%Y%m%d%H%M%SZ) || return 2
if [[ $now < $GRANT_DEADLINE ]]; then
return 0
fi
cleanup_uid_locked "$1" || return 2
return "$STATUS_INACTIVE"
else
status=$?
return "$status"
fi
}
finish_enable() {
local status=$?
trap - EXIT HUP INT TERM
if (( status != 0 )); then
if cleanup_uid_locked "$uid"; then
[[ -z $timer ]] || /usr/bin/systemctl stop "$timer.timer" "$timer.service" >/dev/null 2>&1 || true
else
echo "Could not revoke passwordless sudo; expiry remains armed. Administrator cleanup is required." >&2
fi
fi
[[ -z $pending ]] || /usr/bin/rm -f -- "$pending"
exit "$status"
}
enable_locked() (
local uid=$1 minutes=$2 now expires deadline token timer="" pending="" file status
resolve_account "$uid" && valid_minutes "$minutes" || return 1
verify_boot_cleanup && verify_root_path /etc/sudoers.d || return 1
file=$(rule_file "$uid")
if read_grant "$uid"; then
[[ $GRANT_NAME == "$ACCOUNT_NAME" ]] || return 1
else
status=$?
(( status == STATUS_INACTIVE )) || return 1
fi
trap finish_enable EXIT
omarchy_security_install_signal_exit_traps
now=$(/usr/bin/date +%s) || return 1
expires=$((now + 10#$minutes * 60))
deadline=$(/usr/bin/date -u -d "@$expires" +%Y%m%d%H%M%SZ) || return 1
pending=$(/usr/bin/mktemp /etc/sudoers.d/.omarchy-nopasswd.XXXXXX) || return 1
/usr/bin/printf '%s ALL=(ALL) NOTAFTER=%s NOPASSWD: ALL\n' "$ACCOUNT_NAME" "$deadline" >"$pending" || return 1
/usr/bin/chown root:root "$pending" && /usr/bin/chmod 0440 "$pending" || return 1
/usr/sbin/visudo -cf "$pending" >/dev/null || return 1
token=$(/usr/bin/tr -d '-' </proc/sys/kernel/random/uuid) || return 1
[[ $token =~ ^[0-9a-f]{32}$ ]] || return 1
timer="omarchy-nopasswd-expire-$uid-$token"
/usr/bin/systemd-run --quiet --collect --on-calendar="@$expires" \
--timer-property=AccuracySec=1s --unit="$timer" \
-- "$INSTALLED_SELF" __expire "$uid" || return 1
/usr/bin/systemctl is-active --quiet "$timer.timer" || return 1
# The temporary filename contains a dot, so sudo ignores it. Rename within
# sudoers.d publishes the complete validated policy in one operation.
/usr/bin/mv -fT -- "$pending" "$file" || return 1
pending=""
now=$(/usr/bin/date +%s) || return 1
(( now < expires )) && verify_boot_cleanup && /usr/bin/systemctl is-active --quiet "$timer.timer"
)
root_dispatch() {
local action="$1"
shift
case "$action" in
__status)
(($# == 1)) && verify_sudo_caller "$1" || return 2
with_root_lock status_locked "$1"
;;
__enable)
(($# == 2)) && verify_sudo_caller "$1" && valid_minutes "$2" || return 1
with_root_lock enable_locked "$1" "$2"
;;
__disable)
(($# == 1)) && verify_sudo_caller "$1" || return 1
with_root_lock cleanup_uid_locked "$1"
;;
__expire)
(($# == 1 || $# == 2)) && ((EUID == 0)) && valid_uid "$1" || return 1
[[ -z ${2:-} || $2 =~ ^omarchy-nopasswd-expire-${1}-[0-9a-f]{32}$ ]] || return 1
with_root_lock expire_locked "$@"
;;
__migration-complete)
(($# == 0)) && migration_complete
;;
__migrate)
(($# == 0)) && ((EUID == 0)) || return 1
with_root_lock migrate_locked
;;
__cleanup-all)
(($# == 0)) && ((EUID == 0)) || return 1
with_root_lock cleanup_all_locked
;;
__package-removing)
(($# == 0)) && ((EUID == 0)) || return 1
with_root_lock package_removing_locked
;;
*) return 1 ;;
esac
}
case "${1:-}" in
__status|__enable|__disable|__expire|__cleanup-all|__package-removing|__migrate|__migration-complete)
action=$1
shift
root_dispatch "$action" "$@"
exit
;;
esac
(($# <= 1)) || usage
minutes=${1:-$DEFAULT_MINUTES}
valid_minutes "$minutes" || usage
uid=$(/usr/bin/id -u)
valid_uid "$uid" || {
echo "omarchy-sudo-passwordless: cannot grant passwordless sudo to this account" >&2
exit 1
}
omarchy_security_sudo_supports_no_update || {
echo "This sudo does not support --no-update; refusing the passwordless-sudo workflow." >&2
exit 1
}
omarchy_security_install_sudo_cleanup_traps
/usr/bin/sudo -k >/dev/null 2>&1 || {
echo "Could not start from a cold sudo credential state." >&2
exit 1
}
echo "Toggle passwordless sudo..."
# Safety: if the file exists but the timer doesn't (e.g. after reboot), clean up
if sudo test -f "$NOPASSWD_FILE" && ! systemctl is-active "${TIMER_NAME}.timer" &>/dev/null; then
sudo rm "$NOPASSWD_FILE"
fi
# Check for the file directly — sudo -n can stay cached or be granted by other rules
if sudo test -f "$NOPASSWD_FILE"; then
if [[ $1 ]]; then
sudo systemctl stop "${TIMER_NAME}.timer" 2>/dev/null
arm_expiry || exit 1
echo "Passwordless sudo timer updated. It will now automatically disable in ${MINUTES} minutes."
else
sudo rm "$NOPASSWD_FILE"
sudo systemctl stop "${TIMER_NAME}.timer" 2>/dev/null
if /usr/bin/sudo -N -- "$INSTALLED_SELF" __status "$uid"; then
if (($# == 0)); then
/usr/bin/sudo -N -- "$INSTALLED_SELF" __disable "$uid"
echo "Passwordless sudo has been DISABLED. Sudo will require a password again."
else
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
echo "Passwordless sudo expiry updated. It will automatically disable in ${minutes} minutes."
fi
else
status=$?
if (( status != STATUS_INACTIVE )); then
echo "Could not safely inspect passwordless sudo; no grant will be enabled. Resolve the reported authorization or cleanup error first." >&2
exit 1
fi
echo ""
echo "⚠️ WARNING: This will allow ANY process running as your user to"
echo "execute ANY command as root WITHOUT a password for ${MINUTES} minutes."
echo "execute ANY command as root WITHOUT a password for ${minutes} minutes."
echo ""
echo "This is useful for AI agents that need to run sudo commands,"
echo "but it significantly weakens the security of your system."
echo "Anyone or anything with access to your user account gets full root."
echo ""
echo "Passwordless sudo will automatically disable after ${MINUTES} minutes."
echo "Passwordless sudo will automatically disable after ${minutes} minutes,"
echo "including if the machine reboots before the deadline."
echo "Run this command again to disable it early."
echo ""
if gum confirm "Enable passwordless sudo for ${MINUTES} minutes? This is a significant security risk!"; then
echo "${USER} ALL=(ALL) NOPASSWD: ALL" | sudo tee "$NOPASSWD_FILE" > /dev/null
sudo chmod 440 "$NOPASSWD_FILE"
arm_expiry || exit 1
if /usr/bin/gum confirm "Enable passwordless sudo for ${minutes} minutes? This is a significant security risk!"; then
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
echo ""
echo "Passwordless sudo has been ENABLED. It will automatically disable in ${MINUTES} minutes."
echo "A restart removes the passwordless sudo rule as well."
echo "Passwordless sudo has been ENABLED. It will automatically disable in ${minutes} minutes."
else
echo "Aborted. No changes made."
fi
+73 -15
View File
@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/bash -p
# omarchy:summary=Update Omarchy and system packages
# omarchy:alias=omarchy up
@@ -6,24 +6,58 @@
# omarchy:examples=omarchy update | omarchy update -y
# omarchy:requires-sudo=true
if [[ $- != *p* ]]; then
echo "Refusing an unsafe Bash startup." >&2
exit 126
fi
security_entrypoint=$(/usr/bin/readlink -e -- "${BASH_SOURCE[0]}") || exit 126
source "${security_entrypoint%/*}/omarchy-security-functions" || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
set -e
omarchy_security_sanitize_bash_environment "$0" "$@"
omarchy_security_require_source_root "$0"
# Logging and lock acquisition re-exec this command with a sanitized PATH.
# Preserve the caller's path only for the later unprivileged hook/mise phases.
user_path=${OMARCHY_UPDATE_USER_PATH:-$PATH}
unset OMARCHY_UPDATE_USER_PATH
# Traps first, so a signal or failure during the entry revocation still
# exits through the cleanup path.
omarchy_security_install_sudo_cleanup_traps
omarchy_security_revoke_sudo_timestamp || exit 1
omarchy_security_enable_no_update_sudo
update_stay_awake_stopped=0
cleanup_update() {
local status=$?
trap - EXIT HUP INT TERM
if ! omarchy_security_revoke_sudo_timestamp; then
echo "Could not invalidate sudo before update cleanup." >&2
omarchy_security_exit_with_revoked_sudo 1
fi
if (( update_stay_awake_stopped == 0 )); then
omarchy-update-stay-awake stop || status=1
fi
omarchy_security_exit_with_revoked_sudo "$status"
}
if [[ -z ${OMARCHY_UPDATE_LOGGED:-} ]]; then
script_command=$(printf '%q ' "$0" "$@")
exec env OMARCHY_UPDATE_LOGGED=1 script -qefc "$script_command" "/tmp/omarchy-update.log"
exec env OMARCHY_UPDATE_LOGGED=1 OMARCHY_UPDATE_USER_PATH="$user_path" script -qefc "$script_command" "/tmp/omarchy-update.log"
fi
if ! omarchy-update-lock held; then
exec omarchy-update-lock run "$0" "$@"
exec env OMARCHY_UPDATE_USER_PATH="$user_path" omarchy-update-lock run "$0" "$@"
fi
trap 'echo ""; echo -e "\033[0;31mSomething went wrong during the update!\n\nPlease review the output above carefully, correct the error, and retry the update.\n\nIf you need assistance, get help from the community at https://omarchy.org/discord\033[0m"' ERR
trap 'omarchy-update-stay-awake stop' EXIT
trap cleanup_update EXIT
omarchy_security_install_signal_exit_traps
omarchy-update-requires-free-space
# -y is a promise not to ask anything. Steps that would need an answer report
# and move on instead of waiting on a prompt nobody is here to give.
# -y suppresses Omarchy confirmation prompts; sudo authorization is still
# required. Interactive review steps report and move on instead of waiting.
[[ ${1:-} != "-y" ]] || export OMARCHY_UPDATE_UNATTENDED=1
if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
@@ -39,6 +73,7 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
omarchy-update-stay-awake start
# Preserve the established development-checkout update ordering.
omarchy-update-dev
omarchy-update-keyring
@@ -46,20 +81,43 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
# them, so everything below waits on this finishing. An upgrade that stopped
# takes the update with it rather than migrating against what is still on disk.
omarchy-update-system-pkgs
# Historical migrations are strictly ordered and mix user hooks/downloaded
# tooling with privileged repairs. The no-update sudo wrapper has covered the
# whole update, so neither the package transaction nor a later repair can
# publish a timestamp to a detached migration child.
omarchy_security_revoke_sudo_timestamp
omarchy-migrate
omarchy-hook post-update
omarchy-update-aur-pkgs
omarchy-update-mise
omarchy-update-orphan-pkgs
omarchy-update-analyze-logs
omarchy-update-status
# 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-update-stay-awake stop
trap - EXIT
# Service restart helpers can need sudo. Run them before any user-controlled
# update tooling; the reboot-only phase below performs no privileged work.
omarchy-update-restart --services-only
omarchy-update-restart
# 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
# Hooks and mise execute user-controlled code. Give each a cold credential
# boundary and run mise last so it cannot wait for a legitimate hook sudo.
# Only the unprivileged reboot prompt follows them.
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$user_path" "$OMARCHY_PATH/bin/omarchy-hook" post-update
omarchy_security_revoke_sudo_timestamp
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
+8 -1
View File
@@ -2,10 +2,17 @@
# omarchy:summary=Update AUR packages if any are installed
sudo_options=()
if [[ ${OMARCHY_SUDO_NO_UPDATE:-0} == "1" ]]; then
sudo_wrapper="$OMARCHY_PATH/default/omarchy/sudo-no-update/sudo"
[[ -x $sudo_wrapper ]] || exit 1
sudo_options=(--sudo "$sudo_wrapper" --sudoloop=false)
fi
if pacman -Qem >/dev/null; then
if omarchy-pkg-aur-accessible; then
echo -e "\e[32m\nUpdate AUR packages\e[0m"
yay -Sua --noconfirm --cleanafter --ignore gcc14,gcc14-libs
yay "${sudo_options[@]}" -Sua --noconfirm --cleanafter --ignore gcc14,gcc14-libs || exit 1
echo
else
echo -e "\e[31m\nAUR is unavailable (so skipping updates)\e[0m"
+51 -36
View File
@@ -1,51 +1,66 @@
#!/bin/bash
# omarchy:summary=Prompt for required reboot or service restarts after updates
# omarchy:args=[--services-only|--reboot-only]
mode="${1:-all}"
case "$mode" in
all|--services-only|--reboot-only) ;;
*) echo "Unknown restart phase: $mode" >&2; exit 2 ;;
esac
echo
confirm_reboot() {
gum confirm "$1" && { omarchy-system-reboot; exit 0; }
if [[ ${OMARCHY_UPDATE_UNATTENDED:-0} == "1" ]]; then
echo "$1 Run omarchy-system-reboot when ready."
elif gum confirm "$1"; then
omarchy-system-reboot
exit 0
fi
}
running_kernel=$(uname -r)
kernel_updated=true
if [[ $mode != "--services-only" ]]; then
running_kernel=$(uname -r)
kernel_updated=true
for kernel in /usr/lib/modules/*/vmlinuz; do
if [[ -f $kernel ]] && pacman -Qo "$kernel" &>/dev/null; then
installed_kernel=$(basename "$(dirname "$kernel")")
for kernel in /usr/lib/modules/*/vmlinuz; do
if [[ -f $kernel ]] && pacman -Qo "$kernel" &>/dev/null; then
installed_kernel=$(basename "$(dirname "$kernel")")
if [[ $installed_kernel == $running_kernel ]]; then
kernel_updated=false
break
if [[ $installed_kernel == $running_kernel ]]; then
kernel_updated=false
break
fi
fi
fi
done
done
if [[ $kernel_updated == "true" ]]; then
confirm_reboot "Linux kernel has been updated. Reboot?"
elif [[ -f $HOME/.local/state/omarchy/reboot-required ]]; then
confirm_reboot "Updates require reboot. Ready?"
if [[ $kernel_updated == "true" ]]; then
confirm_reboot "Linux kernel has been updated. Reboot?"
elif [[ -f $HOME/.local/state/omarchy/reboot-required ]]; then
confirm_reboot "Updates require reboot. Ready?"
fi
running_hyprland=$(readlink /proc/$(pgrep -x Hyprland)/exe 2>/dev/null)
if [[ $running_hyprland == *"(deleted)"* ]]; then
confirm_reboot "Hyprland has been updated. Reboot?"
fi
fi
running_hyprland=$(readlink /proc/$(pgrep -x Hyprland)/exe 2>/dev/null)
if [[ $running_hyprland == *"(deleted)"* ]]; then
confirm_reboot "Hyprland has been updated. Reboot?"
if [[ $mode != "--reboot-only" ]]; then
for file in "$HOME"/.local/state/omarchy/restart-*-required; do
if [[ -f $file ]]; then
filename=$(basename "$file")
service=$(echo "$filename" | sed 's/restart-\(.*\)-required/\1/')
echo "Restarting $service"
omarchy-state clear "$filename"
omarchy-restart-"$service"
fi
done
# Updates routinely replace the shell's QML, and a stale process can lazy-load
# new files into old code. A restart failure (locked session, ssh, TTY) only
# prints its reason: the next update or login gets a fresh shell anyway.
echo -e "\e[32m\nRestarting shell\e[0m"
echo "All plugins have been reloaded"
omarchy-restart-shell || true
fi
for file in "$HOME"/.local/state/omarchy/restart-*-required; do
if [[ -f $file ]]; then
filename=$(basename "$file")
service=$(echo "$filename" | sed 's/restart-\(.*\)-required/\1/')
echo "Restarting $service"
omarchy-state clear "$filename"
omarchy-restart-"$service"
fi
done
# Updates routinely replace the shell's QML, and a stale process can lazy-load
# new files into old code. A restart failure (locked session, ssh, TTY) only
# prints its reason: the next update or login gets a fresh shell anyway.
echo -e "\e[32m\nRestarting shell\e[0m"
echo "All plugins have been reloaded"
omarchy-restart-shell || true
+520 -83
View File
@@ -1,15 +1,231 @@
#!/bin/bash
#!/bin/bash -p
# omarchy:summary=Manage sleep and idle inhibition during an update
# omarchy:args=<start|stop>
# omarchy:hidden=true
if [[ $- != *p* ]]; then
echo "Refusing an unsafe Bash startup." >&2
exit 126
fi
security_entrypoint=$(/usr/bin/readlink -e -- "${BASH_SOURCE[0]}") || exit 126
source "${security_entrypoint%/*}/omarchy-security-functions" || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
set -e
omarchy_security_sanitize_bash_environment "$0" "$@"
omarchy_security_require_source_root "$0"
# Traps first, so a signal or failure during the entry revocation still
# exits through the cleanup path.
omarchy_security_install_sudo_cleanup_traps
omarchy_security_revoke_sudo_timestamp || exit 1
omarchy_security_enable_no_update_sudo
state_dir="${XDG_RUNTIME_DIR:-/tmp/omarchy-$UID}/omarchy-update-stay-awake"
idle_owner_file="$state_dir/idle-owner"
inhibit_pid_file="$state_dir/inhibit-pid"
stay_awake_state="$HOME/.local/state/omarchy/indicators/stay-awake"
caller_uid=""
state_base=""
state_dir=""
idle_owner_file=""
inhibit_pid_file=""
launch_control_file=""
launch_pending=0
launch_token=""
fail_state_boundary() {
echo "Refusing to use an unsafe Omarchy update inhibitor state path." >&2
return 1
}
directory_is_private() {
local directory="$1"
local expected_owner="$2"
local legacy="${3:-0}"
local canonical=""
local owner=""
local mode=""
[[ -d $directory && ! -L $directory ]] || return 1
canonical=$(readlink -e -- "$directory") || return 1
[[ $canonical == "$directory" ]] || return 1
read -r owner mode < <(stat -Lc '%u %a' -- "$directory") || return 1
[[ $owner == "$expected_owner" ]] || return 1
[[ $mode == "700" ]] || (( legacy == 1 && (8#$mode & 022) == 0 ))
}
root_owned_parent_chain() {
local directory="$1"
local parent owner mode type canonical
parent=$(/usr/bin/dirname -- "$directory") || return 1
while :; do
[[ -d $parent && ! -L $parent ]] || return 1
canonical=$(/usr/bin/readlink -e -- "$parent") || return 1
[[ $canonical == "$parent" ]] || return 1
read -r owner mode type < <(/usr/bin/stat -Lc '%u %a %F' -- "$parent") || return 1
[[ $owner == 0 && $type == "directory" ]] || return 1
! ((8#$mode & 022)) || return 1
[[ $parent == / ]] && break
parent=$(/usr/bin/dirname -- "$parent") || return 1
done
}
runtime_directory_is_private() {
local directory="$1" expected_owner="$2"
directory_is_private "$directory" "$expected_owner" &&
root_owned_parent_chain "$directory"
}
ensure_private_directory() {
local directory="$1"
if [[ ! -e $directory && ! -L $directory ]]; then
mkdir -m 700 -- "$directory" 2>/dev/null || true
fi
# Older helpers used mkdir -p with the caller's umask. Tighten only a
# canonical, caller-owned directory that other accounts cannot write.
directory_is_private "$directory" "$caller_uid" 1 || return 1
chmod 700 -- "$directory" || return 1
directory_is_private "$directory" "$caller_uid"
}
initialize_state_boundary() {
local canonical_tmp=""
local tmp_owner=""
local tmp_mode=""
caller_uid="$EUID"
[[ $caller_uid =~ ^[0-9]+$ ]] || return 1
if [[ -n ${XDG_RUNTIME_DIR:-} ]]; then
runtime_directory_is_private "$XDG_RUNTIME_DIR" "$caller_uid" || fail_state_boundary
state_base="$XDG_RUNTIME_DIR"
else
[[ -d /tmp && ! -L /tmp ]] || fail_state_boundary
canonical_tmp=$(readlink -e -- /tmp) || fail_state_boundary
read -r tmp_owner tmp_mode < <(stat -Lc '%u %a' -- /tmp) || fail_state_boundary
[[ $canonical_tmp == "/tmp" && $tmp_owner == "0" && $tmp_mode == "1777" ]] || fail_state_boundary
state_base="/tmp/omarchy-$caller_uid"
ensure_private_directory "$state_base" || fail_state_boundary
fi
state_dir="$state_base/omarchy-update-stay-awake"
idle_owner_file="$state_dir/idle-owner"
inhibit_pid_file="$state_dir/inhibit-pid"
launch_control_file="$state_dir/launch-control"
}
state_file_is_private() {
local state_file="$1"
local legacy="${2:-0}"
local owner=""
local mode=""
local links=""
[[ -f $state_file && ! -L $state_file ]] || return 1
read -r owner mode links < <(stat -Lc '%u %a %h' -- "$state_file") || return 1
[[ $owner == "$caller_uid" && $links == "1" ]] || return 1
[[ $mode == "600" ]] || (( legacy == 1 && (8#$mode & 022) == 0 ))
}
read_state_record() {
local state_file="$1"
local legacy="${2:-0}"
local records=()
local file_size=""
local LC_ALL=C
state_file_is_private "$state_file" "$legacy" || return 1
mapfile -t records <"$state_file" || return 1
(( ${#records[@]} == 1 )) || return 1
file_size=$(stat -Lc '%s' -- "$state_file") || return 1
(( file_size == ${#records[0]} + 1 )) || return 1
state_file_is_private "$state_file" "$legacy" || return 1
printf '%s\n' "${records[0]}"
}
atomic_write_state() {
local state_file="$1"
local record="$2"
local temporary=""
[[ $record != *$'\n'* ]] || return 1
temporary=$(mktemp "$state_dir/.${state_file##*/}.XXXXXXXX") || return 1
chmod 600 "$temporary" || {
rm -f -- "$temporary"
return 1
}
if ! printf '%s\n' "$record" >"$temporary" || ! state_file_is_private "$temporary"; then
rm -f -- "$temporary"
return 1
fi
if [[ -e $state_file || -L $state_file ]]; then
state_file_is_private "$state_file" || {
rm -f -- "$temporary"
return 1
}
fi
mv -fT -- "$temporary" "$state_file" || {
rm -f -- "$temporary"
return 1
}
state_file_is_private "$state_file"
}
rollback_pending_launch() {
local control_fd=""
local inhibit_record=""
local inhibit_pid=""
local recorded_start_time=""
local recorded_owner=""
local token=""
(( launch_pending == 1 )) || return 0
if [[ -e $launch_control_file || -L $launch_control_file ]]; then
state_file_is_private "$launch_control_file" || return 1
exec {control_fd}<>"$launch_control_file" || return 1
/usr/bin/flock -x "$control_fd" || {
exec {control_fd}>&-
return 1
}
: >"/proc/self/fd/$control_fd"
printf 'cancelled %s\n' "$launch_token" >&"$control_fd"
rm -f -- "$launch_control_file"
/usr/bin/flock -u "$control_fd"
exec {control_fd}>&-
fi
inhibit_record=$(read_state_record "$inhibit_pid_file" 2>/dev/null || true)
if [[ $inhibit_record =~ ^1\ ([1-9][0-9]{0,18})\ ([1-9][0-9]{0,18})\ ([0-9]{1,10})\ ($launch_token)$ ]]; then
inhibit_pid="${BASH_REMATCH[1]}"
recorded_start_time="${BASH_REMATCH[2]}"
recorded_owner="${BASH_REMATCH[3]}"
token="${BASH_REMATCH[4]}"
if process_matches "$inhibit_pid" "$recorded_start_time" "$recorded_owner" "$token"; then
discard_launched_inhibitor "$inhibit_pid" "$recorded_start_time" "$recorded_owner" "$token"
fi
[[ $(read_state_record "$inhibit_pid_file" 2>/dev/null || true) != "$inhibit_record" ]] ||
rm -f -- "$inhibit_pid_file"
fi
# A signal may interrupt either parent-side atomic write before its rename.
rm -f -- "$state_dir"/.launch-control.* "$state_dir"/.idle-owner.*
stop_locked || return 1
launch_pending=0
}
cleanup_pending_launch() {
local status=$?
trap - EXIT HUP INT TERM
if ! rollback_pending_launch; then
echo "Failed to roll back the pending Omarchy update sleep inhibitor." >&2
(( status != 0 )) || status=1
fi
omarchy_security_exit_with_revoked_sudo "$status" \
"Failed to invalidate sudo credentials after the update sleep inhibitor."
}
process_start_time() {
local process_pid="$1"
@@ -21,9 +237,67 @@ process_start_time() {
process_stat="${process_stat##*) }"
read -r -a stat_fields <<<"$process_stat"
(( ${#stat_fields[@]} > 19 )) || return 1
[[ ${stat_fields[19]} =~ ^[0-9]+$ ]] || return 1
printf '%s\n' "${stat_fields[19]}"
}
process_owner() {
local process_pid="$1"
local token="$2"
local owner=""
if [[ -n $token ]]; then
owner=$(stat -Lc '%u' -- "/proc/$process_pid") || return 1
else
# Legacy state can name sudo: its real UID remains the invoking account,
# even when /proc ownership reflects its effective root credentials.
owner=$(awk '/^Uid:/ { print $2; exit }' "/proc/$process_pid/status") || return 1
fi
[[ $owner =~ ^[0-9]+$ ]] || return 1
printf '%s\n' "$owner"
}
process_has_token() {
local process_pid="$1"
local token="$2"
local argument=""
local expected="--why=Omarchy update in progress [$token]"
[[ -r /proc/$process_pid/cmdline ]] || return 1
while IFS= read -r -d '' argument; do
[[ $argument == "$expected" ]] && return 0
done <"/proc/$process_pid/cmdline"
return 1
}
process_identity() {
local process_pid="$1"
local token="$2"
local start_before=""
local start_after=""
local owner_before=""
local owner_after=""
start_before=$(process_start_time "$process_pid") || return 1
owner_before=$(process_owner "$process_pid" "$token") || return 1
[[ -z $token ]] || process_has_token "$process_pid" "$token" || return 1
start_after=$(process_start_time "$process_pid") || return 1
owner_after=$(process_owner "$process_pid" "$token") || return 1
[[ $start_before == "$start_after" && $owner_before == "$owner_after" ]] || return 1
printf '%s %s\n' "$start_before" "$owner_before"
}
process_matches() {
local process_pid="$1"
local expected_start="$2"
local expected_owner="$3"
local token="$4"
local identity=""
identity=$(process_identity "$process_pid" "$token" 2>/dev/null) || return 1
[[ $identity == "$expected_start $expected_owner" ]]
}
process_state() {
local process_pid="$1"
local process_stat=""
@@ -34,113 +308,276 @@ process_state() {
printf '%s\n' "${process_stat%% *}"
}
stop() {
local inhibit_pid=""
local recorded_start_time=""
local current_start_time=""
local idle_owner=""
local current_idle_owner=""
discard_launched_inhibitor() {
local inhibit_pid="$1"
local recorded_start_time="$2"
local recorded_owner="$3"
local token="$4"
if [[ -s $idle_owner_file ]]; then
idle_owner=$(<"$idle_owner_file")
if [[ -f $stay_awake_state ]]; then
current_idle_owner=$(<"$stay_awake_state")
fi
if [[ -n $idle_owner && $current_idle_owner == "$idle_owner" ]]; then
omarchy-toggle-idle allow-idle >/dev/null 2>&1 || true
fi
rm -f "$idle_owner_file"
signal_inhibitor "$inhibit_pid" "$recorded_start_time" "$recorded_owner" "$token" TERM || true
for (( attempt = 0; attempt < 25; attempt++ )); do
process_matches "$inhibit_pid" "$recorded_start_time" "$recorded_owner" "$token" || break
[[ $(process_state "$inhibit_pid" 2>/dev/null || true) == "Z" ]] && break
sleep 0.02
done
if process_matches "$inhibit_pid" "$recorded_start_time" "$recorded_owner" "$token" &&
[[ $(process_state "$inhibit_pid" 2>/dev/null || true) != "Z" ]]; then
signal_inhibitor "$inhibit_pid" "$recorded_start_time" "$recorded_owner" "$token" KILL || true
fi
if [[ -s $inhibit_pid_file ]]; then
read -r inhibit_pid recorded_start_time <"$inhibit_pid_file" || true
if [[ $inhibit_pid =~ ^[0-9]+$ ]]; then
current_start_time=$(process_start_time "$inhibit_pid" 2>/dev/null || true)
if [[ ! -e /proc/$inhibit_pid ]] || [[ $(process_state "$inhibit_pid" 2>/dev/null || true) == "Z" ]]; then
wait "$inhibit_pid" 2>/dev/null || true
fi
}
signal_inhibitor() {
local inhibit_pid="$1"
local recorded_start_time="$2"
local recorded_owner="$3"
local token="$4"
local signal="$5"
[[ $recorded_owner == "$caller_uid" ]] || return 1
[[ $signal == "TERM" || $signal == "KILL" ]] || return 1
process_matches "$inhibit_pid" "$recorded_start_time" "$recorded_owner" "$token" || return 1
builtin kill -s "$signal" -- "$inhibit_pid" 2>/dev/null
}
terminate_inhibitor() {
local inhibit_pid="$1"
local recorded_start_time="$2"
local recorded_owner="$3"
local token="$4"
signal_inhibitor "$inhibit_pid" "$recorded_start_time" "$recorded_owner" "$token" TERM || true
for (( attempt = 0; attempt < 50; attempt++ )); do
process_matches "$inhibit_pid" "$recorded_start_time" "$recorded_owner" "$token" || return 0
[[ $(process_state "$inhibit_pid" 2>/dev/null || true) != "Z" ]] || return 0
sleep 0.02
done
process_matches "$inhibit_pid" "$recorded_start_time" "$recorded_owner" "$token" || return 0
[[ $(process_state "$inhibit_pid" 2>/dev/null || true) == "Z" ]] && return 0
return 1
}
stop_locked() {
local inhibit_record=""
local inhibit_pid=""
local recorded_start_time=""
local recorded_owner=""
local token=""
local idle_owner=""
local current_idle_owner=""
local current_start=""
local failed=0
local remove_inhibit_state=1
if [[ ! -e $state_dir && ! -L $state_dir ]]; then
return 0
fi
if ! ensure_private_directory "$state_dir"; then
fail_state_boundary
return 1
fi
if [[ -e $idle_owner_file || -L $idle_owner_file ]]; then
idle_owner=$(read_state_record "$idle_owner_file" 1 2>/dev/null || true)
if [[ $idle_owner =~ ^[0-9]+:[0-9]+:[0-9]+$ ]]; then
if [[ -f $stay_awake_state ]]; then
current_idle_owner=$(<"$stay_awake_state")
fi
if [[ $current_idle_owner == "$idle_owner" ]]; then
omarchy-toggle-idle allow-idle >/dev/null 2>&1 || true
fi
else
echo "Ignoring unsafe Omarchy update idle ownership state." >&2
failed=1
fi
if [[ -n $recorded_start_time && $current_start_time == "$recorded_start_time" ]]; then
kill "$inhibit_pid" >/dev/null 2>&1 || true
rm -f -- "$idle_owner_file"
fi
for (( attempt = 0; attempt < 50; attempt++ )); do
current_start_time=$(process_start_time "$inhibit_pid" 2>/dev/null || true)
[[ $current_start_time == "$recorded_start_time" ]] || break
[[ $(process_state "$inhibit_pid" 2>/dev/null || true) != "Z" ]] || break
sleep 0.02
done
current_start_time=$(process_start_time "$inhibit_pid" 2>/dev/null || true)
if [[ $current_start_time == "$recorded_start_time" ]] &&
[[ $(process_state "$inhibit_pid" 2>/dev/null || true) != "Z" ]]; then
echo "Failed to stop the Omarchy update sleep inhibitor." >&2
return 1
if [[ -e $inhibit_pid_file || -L $inhibit_pid_file ]]; then
inhibit_record=$(read_state_record "$inhibit_pid_file" 1 2>/dev/null || true)
if state_file_is_private "$inhibit_pid_file" &&
[[ $inhibit_record =~ ^1\ ([1-9][0-9]{0,18})\ ([1-9][0-9]{0,18})\ ([0-9]{1,10})\ ([0-9a-f]{32})$ ]]; then
inhibit_pid="${BASH_REMATCH[1]}"
recorded_start_time="${BASH_REMATCH[2]}"
recorded_owner="${BASH_REMATCH[3]}"
token="${BASH_REMATCH[4]}"
elif [[ $inhibit_record =~ ^([1-9][0-9]{0,18})\ ([1-9][0-9]{0,18})$ ]]; then
# The first update starts the old helper and installs this one before
# cleanup. Its protected same-account record predates launch tokens.
inhibit_pid="${BASH_REMATCH[1]}"
recorded_start_time="${BASH_REMATCH[2]}"
recorded_owner="$caller_uid"
if [[ -e /proc/$inhibit_pid && $(process_state "$inhibit_pid" 2>/dev/null || true) != "Z" ]] &&
! process_matches "$inhibit_pid" "$recorded_start_time" "$recorded_owner" ""; then
current_start=$(process_start_time "$inhibit_pid" 2>/dev/null || true)
if [[ -z $current_start || $current_start == "$recorded_start_time" ]]; then
echo "Cannot verify permission to stop the legacy update inhibitor; retained its state for recovery." >&2
failed=1
remove_inhibit_state=0
fi
fi
else
echo "Ignoring unsafe Omarchy update sleep inhibitor state." >&2
failed=1
fi
if [[ -n $inhibit_pid ]] && (( remove_inhibit_state == 1 )); then
if process_matches "$inhibit_pid" "$recorded_start_time" "$recorded_owner" "$token"; then
if ! terminate_inhibitor "$inhibit_pid" "$recorded_start_time" "$recorded_owner" "$token"; then
echo "Failed to stop the Omarchy update sleep inhibitor." >&2
failed=1
remove_inhibit_state=0
fi
fi
fi
rm -f "$inhibit_pid_file"
(( remove_inhibit_state == 0 )) || rm -f -- "$inhibit_pid_file"
fi
rmdir "$state_dir" 2>/dev/null || true
(( failed == 0 ))
}
start() {
start_locked() {
local idle_owner="$$:$RANDOM:$RANDOM"
local token=""
local launcher_pid=""
local inhibit_record=""
local inhibit_pid=""
local inhibit_start_time=""
local inhibit_runner=()
local idle_owner="$$:$RANDOM:$RANDOM"
local inhibit_owner=""
local readiness_attempts=0
stop
mkdir -p "$state_dir"
stop_locked || return 1
ensure_private_directory "$state_dir" || fail_state_boundary
if omarchy-cmd-present systemd-inhibit; then
if (( EUID != 0 )); then
if [[ -t 0 ]]; then
sudo -v
inhibit_runner=(sudo)
else
inhibit_runner=(pkexec)
fi
fi
token=$(LC_ALL=C /usr/bin/od -An -N16 -tx1 /dev/urandom | /usr/bin/tr -d ' \n')
[[ $token =~ ^[0-9a-f]{32}$ ]] || return 1
launch_token="$token"
launch_pending=1
trap cleanup_pending_launch EXIT
atomic_write_state "$launch_control_file" "active $token" || return 1
if [[ -n ${OMARCHY_UPDATE_LOCK_FD:-} ]]; then
"${inhibit_runner[@]}" systemd-inhibit \
--what=sleep:idle \
--who=omarchy-update \
--why="Omarchy update in progress" \
--mode=block \
sleep infinity >/dev/null 2>&1 {OMARCHY_UPDATE_LOCK_FD}>&- &
else
"${inhibit_runner[@]}" systemd-inhibit \
--what=sleep:idle \
--who=omarchy-update \
--why="Omarchy update in progress" \
--mode=block \
sleep infinity >/dev/null 2>&1 &
fi
inhibit_pid=$!
inhibit_start_time=$(process_start_time "$inhibit_pid" 2>/dev/null || true)
if [[ -n $inhibit_start_time ]]; then
printf '%s %s\n' "$inhibit_pid" "$inhibit_start_time" >"$inhibit_pid_file"
# The child is identifiable before it publishes state, including before exec.
# exec -a retains that identity without keeping an extra shell alive.
local hold_command=(
/usr/bin/systemd-inhibit --what=sleep:idle --who=omarchy-update
--why="Omarchy update in progress [$token]" --mode=block
/usr/bin/setpriv --reuid "$caller_uid" --regid "$(/usr/bin/id -g)" --clear-groups
/usr/bin/bash -p -c '
set -e
umask 077
state_dir=$1
token=$2
owner=$3
control=$4
expected="--why=Omarchy update in progress [$token]"
temporary=""
cleanup() { [[ -z $temporary ]] || /usr/bin/rm -f -- "$temporary"; }
trap cleanup EXIT
read -r process_stat <"/proc/$$/stat"
process_stat=${process_stat##*) }
read -r -a fields <<<"$process_stat"
temporary=$(/usr/bin/mktemp "$state_dir/.inhibit-pid.XXXXXXXX")
/usr/bin/chmod 600 "$temporary"
printf "1 %s %s %s %s\n" "$$" "${fields[19]}" "$owner" "$token" >"$temporary"
exec {control_fd}<"$control"
/usr/bin/flock -x "$control_fd"
IFS= read -r control_record <&"$control_fd"
[[ $control_record == "active $token" ]]
/usr/bin/mv -fT -- "$temporary" "$state_dir/inhibit-pid"
temporary=""
/usr/bin/flock -u "$control_fd"
exec {control_fd}>&-
trap - EXIT
exec -a "$expected" /usr/bin/sleep infinity
' "--why=Omarchy update in progress [$token]" "$state_dir" "$token" "$caller_uid" "$launch_control_file"
)
if (( EUID == 0 )); then
(
[[ -z ${OMARCHY_UPDATE_LOCK_FD:-} ]] || exec {OMARCHY_UPDATE_LOCK_FD}>&-
exec {state_lock_fd}>&-
exec "${hold_command[@]}"
) &
launcher_pid=$!
elif [[ -t 0 ]]; then
if ! (
[[ -z ${OMARCHY_UPDATE_LOCK_FD:-} ]] || exec {OMARCHY_UPDATE_LOCK_FD}>&-
exec {state_lock_fd}>&-
exec /usr/bin/sudo -N -b -- "${hold_command[@]}"
); then
return 1
fi
else
(
[[ -z ${OMARCHY_UPDATE_LOCK_FD:-} ]] || exec {OMARCHY_UPDATE_LOCK_FD}>&-
exec {state_lock_fd}>&-
exec /usr/bin/pkexec "${hold_command[@]}"
) &
launcher_pid=$!
fi
while :; do
inhibit_record=$(read_state_record "$inhibit_pid_file" 2>/dev/null || true)
if [[ $inhibit_record =~ ^1\ ([1-9][0-9]{0,18})\ ([1-9][0-9]{0,18})\ ([0-9]{1,10})\ ($token)$ ]]; then
inhibit_pid="${BASH_REMATCH[1]}"
inhibit_start_time="${BASH_REMATCH[2]}"
inhibit_owner="${BASH_REMATCH[3]}"
process_matches "$inhibit_pid" "$inhibit_start_time" "$inhibit_owner" "$token" && break
fi
if [[ -n $launcher_pid ]] && ! kill -0 "$launcher_pid" 2>/dev/null; then
wait "$launcher_pid" || return 1
echo "The update sleep inhibitor did not start." >&2
return 1
fi
(( readiness_attempts += 1 ))
if [[ -t 0 ]] && (( EUID != 0 && readiness_attempts >= 100 )); then
echo "The update sleep inhibitor did not become ready." >&2
return 1
fi
/usr/bin/sleep 0.05
done
if [[ -e $launch_control_file || -L $launch_control_file ]]; then
state_file_is_private "$launch_control_file" || return 1
rm -f -- "$launch_control_file"
fi
if [[ ! -f $stay_awake_state ]]; then
printf '%s\n' "$idle_owner" >"$idle_owner_file"
mkdir -p "$(dirname "$stay_awake_state")"
if omarchy-toggle-idle stay-awake >/dev/null 2>&1; then
printf '%s\n' "$idle_owner" >"$stay_awake_state"
else
rm -f "$idle_owner_file"
atomic_write_state "$idle_owner_file" "$idle_owner" || return 1
mkdir -p "$(dirname "$stay_awake_state")" || return 1
# The idle toggle uses this marker. Publish its owner in the same write so
# cancellation cannot leave an unowned Stay Awake setting behind.
if ! printf '%s\n' "$idle_owner" >"$stay_awake_state"; then
omarchy-toggle-idle allow-idle >/dev/null 2>&1 || true
return 1
fi
fi
launch_pending=0
omarchy_security_install_sudo_cleanup_traps
}
case "${1:-}" in
start)
start
;;
stop)
stop
;;
start | stop) ;;
*)
echo "Usage: omarchy-update-stay-awake <start|stop>" >&2
exit 2
;;
esac
initialize_state_boundary
exec {state_lock_fd}<"$state_base" || fail_state_boundary
flock -x "$state_lock_fd" || fail_state_boundary
directory_is_private "$state_base" "$caller_uid" || fail_state_boundary
case "$1" in
start)
start_locked
;;
stop)
stop_locked
;;
esac