diff --git a/Makefile b/Makefile index 846a18be5..d480c9199 100644 --- a/Makefile +++ b/Makefile @@ -113,6 +113,7 @@ help: @printf ' %-18s %s\n' "PACKAGE_WITH_UPDATER=0" "Build packages without codex-update-manager or the updater service" @printf ' %-18s %s\n' "CODEX_CLI_BUNDLE_SOURCE=..." "Embed an installed Codex CLI package in a local AppImage" @printf ' %-18s %s\n' "MAX_BUILD_THREADS=8" "Set supported build jobs/compression threads (default: 0, tool/user defaults)" + @printf ' %-18s %s\n' "CODEX_SUDO_ALERT=1" "Play a best-effort alert before an interactive sudo password prompt" @printf ' %-18s %s\n' "RPM_BINARY_PAYLOAD=..." "Advanced RPM payload flags override (default follows MAX_BUILD_THREADS)" @printf ' %-18s %s\n' "APPIMAGETOOL=..." "Override the appimagetool executable for make appimage" @printf ' %-18s %s\n' "DEB=/path/file.deb" "Override the .deb used by make install" @@ -127,6 +128,7 @@ help: @printf ' %s\n' "make setup-native" @printf ' %s\n' "make bootstrap-native" @printf ' %s\n' "make install-native" + @printf ' %s\n' "CODEX_SUDO_ALERT=1 make install-native" @printf ' %s\n' "PACKAGE_WITH_UPDATER=0 make update-native" @printf ' %s\n' "CODEX_CLI_BUNDLE_SOURCE=/path/to/node_modules/@openai/codex make appimage" @printf ' %s\n' "make inspect-upstream DMG=/tmp/Codex.dmg" @@ -317,35 +319,35 @@ install: echo "[make] No pacman package found. Run 'make pacman' first." >&2; exit 1; \ fi; \ echo "[make] Installing $$pkg"; \ - sudo pacman -U --noconfirm "$$pkg"; \ + "$(CURDIR)/scripts/sudo-with-alert.sh" pacman -U --noconfirm "$$pkg"; \ elif [ "$$format" = "rpm" ] && command -v dnf >/dev/null 2>&1; then \ rpm="$${RPM:-$$(latest_matching_file "$(RPM_GLOB)")}"; \ if [ -z "$$rpm" ]; then \ echo "[make] No RPM package found. Run 'make rpm' first." >&2; exit 1; \ fi; \ echo "[make] Installing $$rpm"; \ - sudo dnf install -y "$$rpm"; \ + "$(CURDIR)/scripts/sudo-with-alert.sh" dnf install -y "$$rpm"; \ elif [ "$$format" = "rpm" ] && command -v zypper >/dev/null 2>&1; then \ rpm="$${RPM:-$$(latest_matching_file "$(RPM_GLOB)")}"; \ if [ -z "$$rpm" ]; then \ echo "[make] No RPM package found. Run 'make rpm' first." >&2; exit 1; \ fi; \ echo "[make] Installing $$rpm"; \ - sudo zypper --non-interactive --no-gpg-checks install -y "$$rpm"; \ + "$(CURDIR)/scripts/sudo-with-alert.sh" zypper --non-interactive --no-gpg-checks install -y "$$rpm"; \ elif [ "$$format" = "rpm" ]; then \ rpm="$${RPM:-$$(latest_matching_file "$(RPM_GLOB)")}"; \ if [ -z "$$rpm" ]; then \ echo "[make] No RPM package found. Run 'make rpm' first." >&2; exit 1; \ fi; \ echo "[make] Installing $$rpm"; \ - sudo rpm -Uvh "$$rpm"; \ + "$(CURDIR)/scripts/sudo-with-alert.sh" rpm -Uvh "$$rpm"; \ elif [ "$$format" = "deb" ]; then \ deb="$${DEB:-$$(latest_matching_file "$(DEB_GLOB)")}"; \ if [ -z "$$deb" ]; then \ echo "[make] No Debian package found. Run 'make deb' first." >&2; exit 1; \ fi; \ echo "[make] Installing $$deb"; \ - sudo dpkg -i "$$deb"; \ + "$(CURDIR)/scripts/sudo-with-alert.sh" dpkg -i "$$deb"; \ else \ echo "[make] No supported package manager found (dpkg, rpm, zypper, or pacman)." >&2; exit 1; \ fi diff --git a/docs/native-setup.md b/docs/native-setup.md index 32c8c70be..b161e287f 100644 --- a/docs/native-setup.md +++ b/docs/native-setup.md @@ -24,6 +24,19 @@ If dependencies are already installed: make install-native ``` +To play a best-effort sound immediately before an interactive `sudo` password +prompt, opt in with `CODEX_SUDO_ALERT=1`: + +```bash +CODEX_SUDO_ALERT=1 make install-native +CODEX_SUDO_ALERT=1 make bootstrap-native +CODEX_SUDO_ALERT=1 make update-native +``` + +The alert is skipped while the existing `sudo` credential timestamp is valid. +It tries the desktop sound system first and falls back to the terminal bell. +Missing audio tools, sound files, or audio services never block installation. + ## Guided Setup ```bash diff --git a/scripts/install-deps.sh b/scripts/install-deps.sh index a2f637eb3..2fe802582 100755 --- a/scripts/install-deps.sh +++ b/scripts/install-deps.sh @@ -8,6 +8,10 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # shellcheck disable=SC1091 source "$SCRIPT_DIR/lib/linux-target-detect.sh" +sudo() { + "$SCRIPT_DIR/sudo-with-alert.sh" "$@" +} + RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' diff --git a/scripts/sudo-with-alert.sh b/scripts/sudo-with-alert.sh new file mode 100755 index 000000000..cd7691fca --- /dev/null +++ b/scripts/sudo-with-alert.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -eu + +play_sudo_alert_command() { + if command -v timeout >/dev/null 2>&1; then + timeout 1s "$@" + else + "$@" + fi +} + +play_sudo_alert() { + local sound_file + + sound_file="/usr/share/sounds/freedesktop/stereo/dialog-warning.oga" + if command -v pw-play >/dev/null 2>&1 \ + && [ -r "$sound_file" ] \ + && play_sudo_alert_command pw-play "$sound_file" >/dev/null 2>&1; then + return 0 + fi + + if command -v paplay >/dev/null 2>&1 \ + && [ -r "$sound_file" ] \ + && play_sudo_alert_command paplay "$sound_file" >/dev/null 2>&1; then + return 0 + fi + + if command -v canberra-gtk-play >/dev/null 2>&1 \ + && play_sudo_alert_command canberra-gtk-play -i dialog-warning >/dev/null 2>&1; then + return 0 + fi + + for sound_file in \ + /usr/share/sounds/alsa/Front_Center.wav \ + /usr/share/sounds/sound-icons/prompt.wav; do + if command -v aplay >/dev/null 2>&1 \ + && [ -r "$sound_file" ] \ + && play_sudo_alert_command aplay -q "$sound_file" >/dev/null 2>&1; then + return 0 + fi + done + + printf '\a' >&2 || true +} + +if [ "${CODEX_SUDO_ALERT:-0}" != "1" ]; then + exec sudo "$@" +fi + +if sudo -n -v 2>/dev/null; then + exec sudo "$@" +fi + +play_sudo_alert || true +sudo -v +exec sudo "$@" diff --git a/tests/scripts_smoke.sh b/tests/scripts_smoke.sh index 7ad2d098c..06999bc0a 100755 --- a/tests/scripts_smoke.sh +++ b/tests/scripts_smoke.sh @@ -2777,6 +2777,104 @@ test_native_shortcut_targets_compose_existing_flows() { assert_contains "$setup_log" 'bash scripts/bootstrap-wizard.sh' } +test_sudo_alert_wrapper() { + info "Checking opt-in sudo password alerts" + local workspace="$TMP_DIR/sudo-alert" + local bin_dir="$workspace/bin" + local log_file="$workspace/events.log" + local wrapper="$REPO_DIR/scripts/sudo-with-alert.sh" + mkdir -p "$bin_dir" + + cat > "$bin_dir/sudo" <<'EOF' +#!/usr/bin/env bash +set -eu +printf 'sudo:%s\n' "$*" >> "$SUDO_ALERT_TEST_LOG" +if [ "${1-}" = "-n" ] && [ "${2-}" = "-v" ]; then + exit "${SUDO_ALERT_TEST_CACHE_STATUS:-0}" +fi +if [ "${1-}" = "-v" ]; then + exit "${SUDO_ALERT_TEST_AUTH_STATUS:-0}" +fi +exit "${SUDO_ALERT_TEST_COMMAND_STATUS:-0}" +EOF + chmod +x "$bin_dir/sudo" + + cat > "$bin_dir/pw-play" <<'EOF' +#!/usr/bin/env bash +set -eu +printf 'alert:%s\n' "$*" >> "$SUDO_ALERT_TEST_LOG" +exit "${SUDO_ALERT_TEST_SOUND_STATUS:-0}" +EOF + chmod +x "$bin_dir/pw-play" + + : > "$log_file" + PATH="$bin_dir:$HOST_TOOL_PATH" SUDO_ALERT_TEST_LOG="$log_file" \ + "$wrapper" true + assert_occurrence_count "$log_file" '^sudo:true$' 1 + assert_not_contains "$log_file" 'sudo:-n -v' + assert_not_contains "$log_file" 'alert:' + + : > "$log_file" + CODEX_SUDO_ALERT=1 PATH="$bin_dir:$HOST_TOOL_PATH" SUDO_ALERT_TEST_LOG="$log_file" \ + "$wrapper" true + assert_contains "$log_file" 'sudo:-n -v' + assert_not_contains "$log_file" 'alert:' + assert_contains "$log_file" 'sudo:true' + + : > "$log_file" + CODEX_SUDO_ALERT=1 SUDO_ALERT_TEST_CACHE_STATUS=1 \ + PATH="$bin_dir:$HOST_TOOL_PATH" SUDO_ALERT_TEST_LOG="$log_file" \ + "$wrapper" true + [ "$(sed -n '1p' "$log_file")" = 'sudo:-n -v' ] || fail "Expected cached sudo check first" + [ "$(sed -n '2p' "$log_file")" = 'alert:/usr/share/sounds/freedesktop/stereo/dialog-warning.oga' ] \ + || fail "Expected alert before authentication" + [ "$(sed -n '3p' "$log_file")" = 'sudo:-v' ] || fail "Expected sudo authentication after alert" + [ "$(sed -n '4p' "$log_file")" = 'sudo:true' ] || fail "Expected command after authentication" + + : > "$log_file" + CODEX_SUDO_ALERT=1 SUDO_ALERT_TEST_CACHE_STATUS=1 SUDO_ALERT_TEST_SOUND_STATUS=1 \ + PATH="$bin_dir:$HOST_TOOL_PATH" SUDO_ALERT_TEST_LOG="$log_file" \ + "$wrapper" true 2>/dev/null + assert_contains "$log_file" 'sudo:-v' + assert_contains "$log_file" 'sudo:true' + + : > "$log_file" + local status=0 + CODEX_SUDO_ALERT=1 SUDO_ALERT_TEST_CACHE_STATUS=1 SUDO_ALERT_TEST_AUTH_STATUS=23 \ + PATH="$bin_dir:$HOST_TOOL_PATH" SUDO_ALERT_TEST_LOG="$log_file" \ + "$wrapper" true 2>/dev/null || status=$? + [ "$status" -eq 23 ] || fail "Expected sudo authentication failure status, got $status" + assert_not_contains "$log_file" 'sudo:true' + + : > "$log_file" + status=0 + CODEX_SUDO_ALERT=1 SUDO_ALERT_TEST_COMMAND_STATUS=17 \ + PATH="$bin_dir:$HOST_TOOL_PATH" SUDO_ALERT_TEST_LOG="$log_file" \ + "$wrapper" true || status=$? + [ "$status" -eq 17 ] || fail "Expected privileged command status, got $status" +} + +test_native_sudo_alert_wiring() { + info "Checking native targets route sudo through the alert wrapper" + local install_log="$TMP_DIR/make-install-sudo-alert.log" + local bootstrap_log="$TMP_DIR/make-bootstrap-sudo-alert.log" + local update_log="$TMP_DIR/make-update-sudo-alert.log" + + CODEX_SUDO_ALERT=1 make -n -C "$REPO_DIR" install >"$install_log" + assert_occurrence_count "$install_log" 'scripts/sudo-with-alert.sh' 5 + + CODEX_SUDO_ALERT=1 make -n -C "$REPO_DIR" bootstrap-native >"$bootstrap_log" + assert_contains "$bootstrap_log" 'bash scripts/install-deps.sh' + assert_contains "$bootstrap_log" 'install-native' + + CODEX_SUDO_ALERT=1 make -n -C "$REPO_DIR" update-native >"$update_log" + assert_contains "$update_log" 'git pull --ff-only' + assert_contains "$update_log" 'install-native' + + assert_contains "$REPO_DIR/scripts/install-deps.sh" 'sudo-with-alert.sh' + assert_contains "$REPO_DIR/Makefile" 'CODEX_SUDO_ALERT=1' +} + test_fedora_dependency_bootstrap_installs_rpmbuild() { info "Checking Fedora dependency bootstrap includes rpmbuild and C++ build tools" local install_deps="$REPO_DIR/scripts/install-deps.sh" @@ -9082,6 +9180,8 @@ main() { test_transactional_install_uses_managed_node_and_isolated_reports test_installer_cleanup_handles_readonly_trees test_native_shortcut_targets_compose_existing_flows + test_sudo_alert_wrapper + test_native_sudo_alert_wiring test_fedora_dependency_bootstrap_installs_rpmbuild test_fedora_atomic_rpm_ostree_target_detection test_setup_native_wizard_noninteractive_feature_writer