Files
omarchy/bin/omarchy-sudo-passwordless
T
David Heinemeier Hansson d9b970dd62 Let users choose passwordless sudo duration (#14435)
* Let users choose passwordless sudo duration

* Warn in the menu bar when passwordless sudo is active

* Fix sudo indicator hover behavior and repeated authentication

* Disable passwordless sudo from the bar without a terminal

* Shorten passwordless sudo indicator tooltip

* Recover interrupted passwordless sudo duration switches

* Allow sudo grant changes when listings require authentication

* Start passwordless sudo setup with the duration question
2026-10-07 14:41:27 +02:00

597 lines
22 KiB
Bash
Executable File

#!/bin/bash -p
# omarchy:summary=Toggle passwordless sudo for the current user.
# omarchy:args=[MINUTES|permanent|--active|--disable]
# omarchy:requires-sudo=true
if [[ $- != *p* && ${BASH_SOURCE[0]} == "$0" ]]; then
echo "Refusing an unsafe Bash startup for passwordless sudo." >&2
exit 126
fi
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 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|permanent|--active|--disable]" >&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_duration() {
[[ $1 == "permanent" ]] || valid_minutes "$1"
}
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() {
local uid=$((10#$1))
if [[ ${2:-} == "permanent" ]]; then
printf '/etc/sudoers.d/99-omarchy-permanent-nopasswd-%s' "$uid"
else
printf '/etc/sudoers.d/99-omarchy-nopasswd-%s' "$uid"
fi
}
# 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 permanent contents
file=$(rule_file "$1")
permanent=$(rule_file "$1" permanent)
if [[ -e $permanent || -L $permanent ]]; then
verify_root_path "$permanent" && [[ -f $permanent ]] || return 2
contents=$(/usr/bin/cat -- "$permanent") || return 2
[[ $contents =~ ^([a-z_][a-z0-9_-]*\$?)\ ALL=\(ALL\)\ NOPASSWD:\ ALL$ ]] || return 2
GRANT_NAME=${BASH_REMATCH[1]}
GRANT_DEADLINE=""
valid_account_name "$GRANT_NAME" || return 2
if [[ -e $file || -L $file ]]; then
# A SIGKILL between publication and removal can leave both policies.
# Only accept a matching generated timed rule. Permanent access already
# exists, so old expiry callbacks must not revoke it; disable removes
# both files, and the next duration change finishes their replacement.
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
[[ ${BASH_REMATCH[1]} == "$GRANT_NAME" ]] || return 2
fi
return 0
fi
[[ -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-}
if [[ $file == */99-omarchy-permanent-nopasswd-* ]]; then
suffix=${file##*/99-omarchy-permanent-nopasswd-}
fi
# 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
[[ $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
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_rule_file() {
local 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 ]]
}
cleanup_uid_locked() {
local file duration
for duration in temporary permanent; do
file=$(rule_file "$1" "$duration")
cleanup_rule_file "$file" || return 1
done
}
# 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
[[ -n $GRANT_DEADLINE ]] || return 0
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
[[ -n $GRANT_DEADLINE ]] || return 0
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
if [[ -n $timer ]]; then
echo "Could not revoke passwordless sudo; expiry remains armed. Administrator cleanup is required." >&2
else
echo "Could not revoke passwordless sudo. Administrator cleanup is required." >&2
fi
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_duration "$minutes" || return 1
verify_root_path /etc/sudoers.d || return 1
if [[ $minutes != "permanent" ]]; then
verify_boot_cleanup || {
echo "Temporary sudo cleanup is unavailable or a settings package transaction is incomplete. Check the omarchy-settings installation." >&2
return 1
}
fi
file=$(rule_file "$uid" "$minutes")
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
pending=$(/usr/bin/mktemp /etc/sudoers.d/.omarchy-nopasswd.XXXXXX) || return 1
if [[ $minutes == "permanent" ]]; then
/usr/bin/printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$ACCOUNT_NAME" >"$pending" || return 1
else
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
/usr/bin/printf '%s ALL=(ALL) NOTAFTER=%s NOPASSWD: ALL\n' "$ACCOUNT_NAME" "$deadline" >"$pending" || return 1
fi
/usr/bin/chown root:root "$pending" && /usr/bin/chmod 0440 "$pending" || return 1
/usr/sbin/visudo -cf "$pending" >/dev/null || return 1
if [[ $minutes != "permanent" ]]; then
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
fi
# 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=""
# Keep access uninterrupted during renewal. When changing duration type,
# remove the other rule only after its replacement is published. The lock
# keeps status and expiry callbacks from observing both rules together.
if [[ $minutes == "permanent" ]]; then
cleanup_rule_file "$(rule_file "$uid")" || return 1
else
cleanup_rule_file "$(rule_file "$uid" permanent)" || return 1
fi
if [[ $minutes != "permanent" ]]; then
now=$(/usr/bin/date +%s) || return 1
(( now < expires )) && verify_boot_cleanup && /usr/bin/systemctl is-active --quiet "$timer.timer"
fi
)
root_dispatch() {
local action="$1" status
shift
case "$action" in
__status)
(($# == 1)) && verify_sudo_caller "$1" || return 2
if with_root_lock status_locked "$1"; then
return 0
else
status=$?
# Reserve status 1 for sudo refusing noninteractive authentication.
# Every internal inspection failure, including lock failures, is 2.
(( status == STATUS_INACTIVE )) || status=2
return "$status"
fi
;;
__enable)
(($# == 2)) && verify_sudo_caller "$1" && valid_duration "$2" || return 1
if with_root_lock enable_locked "$1" "$2"; then
return 0
else
status=$?
echo "Could not enable passwordless sudo. Check existing sudoers policy and cleanup support." >&2
return "$status"
fi
;;
__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
}
show_enabled() {
if [[ $minutes == "permanent" ]]; then
echo "Passwordless sudo has been ENABLED permanently. Run this command again to disable it."
else
echo "Passwordless sudo has been ENABLED. It will automatically disable in ${minutes} minutes."
fi
}
case "${1:-}" in
__status|__enable|__disable|__expire|__cleanup-all|__package-removing|__migrate|__migration-complete)
action=$1
shift
root_dispatch "$action" "$@"
exit
;;
esac
# Prefer matched policy tags to executing a privileged command, avoiding
# authentication/session logs under the default listing policy. Restrictive
# listpw settings require the root-status fallback. Neither path prompts or
# trusts cached credentials as evidence of passwordless access.
if [[ ${1:-} == "--active" ]]; then
(($# == 1)) || usage
uid=$(/usr/bin/id -u)
if policy=$(/usr/bin/sudo -n -N -l -l -- "$INSTALLED_SELF" __status "$uid" 2>/dev/null); then
/usr/bin/grep -q '!authenticate' <<<"$policy"
else
# listpw=always can require authentication to list a passwordless grant.
# With a command, -k ignores cached credentials without invalidating them.
status=0
/usr/bin/sudo -kn -- "$INSTALLED_SELF" __status "$uid" 2>/dev/null || status=$?
# An internal inspection error still proves that sudo ran the helper
# without authentication. Keep the warning visible for unsafe policy.
(( status == 0 || status == 2 )) || exit 1
fi
exit
fi
(($# <= 1)) || usage
minutes=${1:-}
(($# == 0)) || [[ $minutes == "--disable" ]] || valid_duration "$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
}
# Inspect policy without authenticating. Only inspect the root-owned grant
# when policy already permits it without a password; otherwise the eventual
# enable action is the sole authentication boundary. enable_locked validates
# any existing policy before publishing, including expired or modified rules.
status=$STATUS_INACTIVE
if policy=$(/usr/bin/sudo -n -N -l -l -- "$INSTALLED_SELF" __status "$uid" 2>/dev/null); then
if /usr/bin/grep -q '!authenticate' <<<"$policy"; then
status=0
/usr/bin/sudo -n -N -- "$INSTALLED_SELF" __status "$uid" || status=$?
fi
else
# A listing password is independent of command authorization. An active
# grant can still be inspected/revoked without prompting under listpw=always.
status=0
/usr/bin/sudo -n -N -- "$INSTALLED_SELF" __status "$uid" 2>/dev/null || status=$?
if (( status == 1 )) && [[ $minutes != "--disable" ]]; then
# No noninteractive inspection is available. Offer the confirmed enable
# flow; its single authenticated action validates existing policy itself.
status=$STATUS_INACTIVE
fi
fi
if (( status == 0 )) && { (($# == 0)) || [[ $minutes == "--disable" ]]; }; then
/usr/bin/sudo -n -N -- "$INSTALLED_SELF" __disable "$uid"
echo "Passwordless sudo has been DISABLED. Sudo will require a password again."
elif (( status == STATUS_INACTIVE )) && [[ $minutes == "--disable" ]]; then
echo "Passwordless sudo is already DISABLED."
elif (( status == 0 )) && [[ $minutes != "permanent" ]]; then
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
show_enabled
else
if (( status != 0 && 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
if [[ -z $minutes ]]; then
choice=$(/usr/bin/gum choose --header "How long should passwordless sudo stay enabled?" "15 minutes" "1 Hour" "1 Day" "Permanently") || exit 130
case "$choice" in
"15 minutes") minutes=15 ;;
"1 Hour") minutes=60 ;;
"1 Day") minutes=1440 ;;
"Permanently") minutes=permanent ;;
*) exit 130 ;;
esac
fi
if [[ $minutes == "permanent" ]]; then
duration="permanently"
else
duration="for ${minutes} minutes"
fi
echo ""
echo "⚠️ WARNING: This will allow ANY process running as your user to"
echo "execute ANY command as root WITHOUT a password $duration."
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 ""
if [[ $minutes == "permanent" ]]; then
echo "Passwordless sudo will remain enabled across reboots until you disable it."
else
echo "Passwordless sudo will automatically disable after ${minutes} minutes,"
echo "including if the machine reboots before the deadline."
fi
echo "Run this command again to disable it."
echo ""
if /usr/bin/gum confirm "Enable passwordless sudo $duration? This is a significant security risk!"; then
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
echo ""
show_enabled
else
echo "Aborted. No changes made."
fi
fi