diff --git a/setup.sh b/setup.sh index b66c99517..ae87d0761 100755 --- a/setup.sh +++ b/setup.sh @@ -85,6 +85,30 @@ if [[ -f "$_SHARED_CONSTANTS" ]]; then fi unset _SHARED_CONSTANTS +# Escape a string for safe embedding in XML (plist heredocs). +# Prevents XML injection if paths contain &, <, >, ", or ' characters. +_xml_escape() { + local str="$1" + str="${str//&/&}" + str="${str///>}" + str="${str//\"/"}" + str="${str//\'/'}" + printf '%s' "$str" + return 0 +} + +# Escape a string for safe embedding in crontab entries. +# Wraps value in single quotes (prevents $(…), backtick, and variable expansion +# by cron's /bin/sh). Embedded single quotes are escaped via the '\'' idiom. +_cron_escape() { + local str="$1" + # Replace each ' with '\'' (end quote, escaped quote, start quote) + str="${str//\'/\'\\\'\'}" + printf "'%s'" "$str" + return 0 +} + # Check if a launchd agent is loaded (SIGPIPE-safe for pipefail, t1265) _launchd_has_agent() { local label="$1" @@ -839,7 +863,7 @@ main() { _pulse_installed=true fi fi - if [[ "$_pulse_installed" == "false" ]] && crontab -l 2>/dev/null | grep -qF "pulse-wrapper" 2>/dev/null; then + if [[ "$_pulse_installed" == "false" ]] && crontab -l 2>/dev/null | grep -qF "pulse-wrapper"; then _pulse_installed=true fi @@ -856,17 +880,26 @@ main() { # Unload old plist if upgrading if _launchd_has_agent "$pulse_label"; then - launchctl unload "$pulse_plist" 2>/dev/null || true + launchctl unload "$pulse_plist" || true pkill -f 'Supervisor Pulse' 2>/dev/null || true fi # Also clean up old label if present local old_plist="$HOME/Library/LaunchAgents/com.aidevops.supervisor-pulse.plist" if [[ -f "$old_plist" ]]; then - launchctl unload "$old_plist" 2>/dev/null || true + launchctl unload "$old_plist" || true rm -f "$old_plist" fi + # XML-escape paths for safe plist embedding (prevents injection + # if $HOME or paths contain &, <, > characters) + local _xml_wrapper_script _xml_home _xml_opencode_bin _xml_aidevops_dir _xml_path + _xml_wrapper_script=$(_xml_escape "$wrapper_script") + _xml_home=$(_xml_escape "$HOME") + _xml_opencode_bin=$(_xml_escape "$opencode_bin") + _xml_aidevops_dir=$(_xml_escape "$_aidevops_dir") + _xml_path=$(_xml_escape "$PATH") + # Write the plist (always regenerated to pick up config changes) cat >"$pulse_plist" < @@ -878,24 +911,24 @@ main() { ProgramArguments /bin/bash - ${wrapper_script} + ${_xml_wrapper_script} StartInterval 120 StandardOutPath - ${HOME}/.aidevops/logs/pulse-wrapper.log + ${_xml_home}/.aidevops/logs/pulse-wrapper.log StandardErrorPath - ${HOME}/.aidevops/logs/pulse-wrapper.log + ${_xml_home}/.aidevops/logs/pulse-wrapper.log EnvironmentVariables PATH - ${PATH} + ${_xml_path} HOME - ${HOME} + ${_xml_home} OPENCODE_BIN - ${opencode_bin} + ${_xml_opencode_bin} PULSE_DIR - ${_aidevops_dir} + ${_xml_aidevops_dir} PULSE_STALE_THRESHOLD 1800 @@ -907,7 +940,7 @@ main() { PLIST - if launchctl load "$pulse_plist" 2>/dev/null; then + if launchctl load "$pulse_plist"; then if [[ "$_pulse_installed" == "true" ]]; then print_info "Supervisor pulse updated (launchd config regenerated)" else @@ -919,10 +952,16 @@ PLIST else # Linux: use cron entry with wrapper # Remove old-style cron entries (direct opencode invocation) + # Shell-escape all interpolated paths to prevent command injection + # via $(…) or backticks if paths contain shell metacharacters + local _cron_opencode_bin _cron_aidevops_dir _cron_wrapper_script + _cron_opencode_bin=$(_cron_escape "$opencode_bin") + _cron_aidevops_dir=$(_cron_escape "$_aidevops_dir") + _cron_wrapper_script=$(_cron_escape "$wrapper_script") ( crontab -l 2>/dev/null | grep -v 'aidevops: supervisor-pulse' - echo "*/2 * * * * OPENCODE_BIN=${opencode_bin} PULSE_DIR=${_aidevops_dir} /bin/bash ${wrapper_script} >> $HOME/.aidevops/logs/pulse-wrapper.log 2>&1 # aidevops: supervisor-pulse" - ) | crontab - 2>/dev/null || true + echo "*/2 * * * * PATH=\"/usr/local/bin:/usr/bin:/bin\" OPENCODE_BIN=${_cron_opencode_bin} PULSE_DIR=${_cron_aidevops_dir} /bin/bash ${_cron_wrapper_script} >> \"\$HOME/.aidevops/logs/pulse-wrapper.log\" 2>&1 # aidevops: supervisor-pulse" + ) | crontab - || true if crontab -l 2>/dev/null | grep -qF "aidevops: supervisor-pulse"; then print_info "Supervisor pulse enabled (cron, every 2 min). Disable: crontab -e and remove the supervisor-pulse line" else @@ -934,14 +973,14 @@ PLIST if [[ "$(uname -s)" == "Darwin" ]]; then local pulse_plist="$HOME/Library/LaunchAgents/${pulse_label}.plist" if _launchd_has_agent "$pulse_label"; then - launchctl unload "$pulse_plist" 2>/dev/null || true + launchctl unload "$pulse_plist" || true rm -f "$pulse_plist" pkill -f 'Supervisor Pulse' 2>/dev/null || true print_info "Supervisor pulse disabled (launchd agent removed per config)" fi else - if crontab -l 2>/dev/null | grep -qF "pulse-wrapper" 2>/dev/null; then - crontab -l 2>/dev/null | grep -v 'aidevops: supervisor-pulse' | crontab - 2>/dev/null || true + if crontab -l 2>/dev/null | grep -qF "pulse-wrapper"; then + crontab -l 2>/dev/null | grep -v 'aidevops: supervisor-pulse' | crontab - || true print_info "Supervisor pulse disabled (cron entry removed per config)" fi fi @@ -994,6 +1033,13 @@ PLIST launchctl unload "$guard_plist" || true fi + # XML-escape paths for safe plist embedding (prevents injection + # if $HOME or paths contain &, <, > characters) + local _xml_guard_script _xml_guard_home _xml_guard_path + _xml_guard_script=$(_xml_escape "$guard_script") + _xml_guard_home=$(_xml_escape "$HOME") + _xml_guard_path=$(_xml_escape "$PATH") + cat >"$guard_plist" < @@ -1003,21 +1049,22 @@ PLIST ${guard_label} ProgramArguments - ${guard_script} + /bin/bash + ${_xml_guard_script} kill-runaways StartInterval 30 StandardOutPath - ${HOME}/.aidevops/logs/process-guard.log + ${_xml_guard_home}/.aidevops/logs/process-guard.log StandardErrorPath - ${HOME}/.aidevops/logs/process-guard.log + ${_xml_guard_home}/.aidevops/logs/process-guard.log EnvironmentVariables PATH - ${PATH} + ${_xml_guard_path} HOME - ${HOME} + ${_xml_guard_home} SHELLCHECK_RSS_LIMIT_KB 524288 SHELLCHECK_RUNTIME_LIMIT @@ -1043,11 +1090,14 @@ GUARD_PLIST else # Linux: cron entry (every minute — cron minimum granularity) # Always regenerate to pick up config changes (matches macOS behavior) + # Shell-escape path to prevent command injection via metacharacters + local _cron_guard_script + _cron_guard_script=$(_cron_escape "$guard_script") ( crontab -l 2>/dev/null | grep -v 'aidevops: process-guard' - echo "* * * * * SHELLCHECK_RSS_LIMIT_KB=524288 SHELLCHECK_RUNTIME_LIMIT=120 CHILD_RSS_LIMIT_KB=8388608 CHILD_RUNTIME_LIMIT=7200 /bin/bash \"${guard_script}\" kill-runaways >> \"\$HOME/.aidevops/logs/process-guard.log\" 2>&1 # aidevops: process-guard" - ) | crontab - 2>/dev/null || true - if crontab -l 2>/dev/null | grep -qF "aidevops: process-guard" 2>/dev/null; then + echo "* * * * * PATH=\"/usr/local/bin:/usr/bin:/bin\" SHELLCHECK_RSS_LIMIT_KB=524288 SHELLCHECK_RUNTIME_LIMIT=120 CHILD_RSS_LIMIT_KB=8388608 CHILD_RUNTIME_LIMIT=7200 /bin/bash ${_cron_guard_script} kill-runaways >> \"\$HOME/.aidevops/logs/process-guard.log\" 2>&1 # aidevops: process-guard" + ) | crontab - || true + if crontab -l 2>/dev/null | grep -qF "aidevops: process-guard"; then print_info "Process guard enabled (cron, every minute)" else print_warning "Failed to install process guard cron entry"