Skip to content

Commit 6eb3748

Browse files
authored
chore(code): harden installer ripgrep gating and prompt semantics (#5344)
Hardens the `dcode` install script (`curl ... | bash`) in two ways, adapted from patterns in Prime's installer. --- ## Gate system ripgrep on a minimum version The `system` ripgrep path (brew/apt/dnf/pacman/zypper/apk/nix/cargo) used to count any `command -v rg` after install as success — an ancient distro package would satisfy it and then behave differently at runtime. Each branch now checks the resulting `rg --version` against a floor (12.0.0, below the 14.1.1 the managed installer pins) and only succeeds when it clears it; a too-old or unprobeable pre-existing `rg` warns instead of silently passing, and a failed version probe no longer aborts the installer. ## Three-valued `prompt_yn` `prompt_yn` returned the same code for "user said no" and "no terminal exists to ask on," forcing callers to guess. It now returns 0 (yes), 1 (no), or 2 (no usable terminal). The upgrade prompt completes the update on the no-terminal case (cron/CI/systemd) rather than conflating it with a decline, the PATH-setup prompt declines only on an explicit "no," and the extras-removal prompts continue (with a warning) when nobody could be asked.
1 parent 889f217 commit 6eb3748

3 files changed

Lines changed: 276 additions & 33 deletions

File tree

libs/code/scripts/install.sh

Lines changed: 107 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,14 @@
6767
# DEEPAGENTS_CODE_RIPGREP_INSTALLER — how to provision ripgrep:
6868
# "managed" (default) eagerly installs the pinned, SHA-256-verified binary
6969
# into ~/.deepagents/bin (no sudo) via `dcode tools install`; "system"
70-
# keeps the interactive package-manager install (brew/apt/cargo/...). Set
71-
# DEEPAGENTS_CODE_OFFLINE=1 to skip the managed download entirely.
70+
# keeps the interactive package-manager install (brew/apt/cargo/...),
71+
# which only counts as success when the resulting `rg` meets the minimum
72+
# supported version. Set DEEPAGENTS_CODE_OFFLINE=1 to skip the managed
73+
# download entirely.
74+
#
75+
# Before execution, the downloaded uv bootstrap script is checked for a shell
76+
# shebang and valid shell syntax. These structural checks help catch error
77+
# pages and some truncated downloads, but do not verify its integrity.
7278
# DEEPAGENTS_CODE_SKIP_XCODE_CHECK — set to 1 to bypass the macOS Xcode
7379
# Command Line Tools preflight check
7480
# DEEPAGENTS_CODE_NO_MODIFY_PATH — set to 1 to skip PATH setup entirely
@@ -461,10 +467,18 @@ fi
461467
# ---------------------------------------------------------------------------
462468
# Prompt helper — reads from /dev/tty when stdin is piped
463469
# ---------------------------------------------------------------------------
470+
# prompt_yn QUESTION — three outcomes, so callers can tell a declined prompt
471+
# apart from one that could never be shown:
472+
# 0 — answered yes
473+
# 1 — answered no (or empty)
474+
# 2 — no terminal exists to ask on (non-interactive, or /dev/tty unusable)
475+
# Plain `if prompt_yn ...` keeps working unchanged: 0 is yes, anything else is
476+
# not-yes. Only callers that act differently on "no terminal" vs "no" check
477+
# for 2 explicitly.
464478
prompt_yn() {
465479
local question="$1"
466480
if [ "$IS_INTERACTIVE" = false ]; then
467-
return 1
481+
return 2
468482
fi
469483
local reply=""
470484
if [ -t 0 ]; then
@@ -1296,6 +1310,14 @@ install_uv() {
12961310
exit "$uv_install_rc"
12971311
fi
12981312

1313+
# Astral does not publish a checksum that covers uv-installer.sh itself:
1314+
# the per-release sha256.sum and dist-manifest.json only list the platform
1315+
# archives, and the archives' digests are embedded in this script (which
1316+
# pins APP_VERSION and verifies them after download). The meaningful checks
1317+
# for the script fetch are therefore the shebang and parse checks below:
1318+
# they catch a captive-portal HTML page and a truncated body, which are the
1319+
# realistic failure modes for this URL.
1320+
#
12991321
# Verify the downloaded script starts with a shell shebang before executing
13001322
# it. This catches a non-shell response — an HTML error page or JSON from a
13011323
# proxy or captive portal that returned 200 — that would otherwise fail
@@ -2686,7 +2708,12 @@ ensure_path_setup() {
26862708
done
26872709
fi
26882710
if [ "$IS_INTERACTIVE" = true ] && can_prompt; then
2689-
if ! prompt_yn "Add ~/.local/bin to your PATH in ${prompt_targets}?"; then
2711+
# Only an explicit "no" (rc=1) declines; rc=2 (no usable terminal) leaves
2712+
# the auto-add default in place, matching the non-interactive behavior.
2713+
# `|| prompt_rc=$?` keeps "no" from tripping `set -e`.
2714+
prompt_rc=0
2715+
prompt_yn "Add ~/.local/bin to your PATH in ${prompt_targets}?" || prompt_rc=$?
2716+
if [ "$prompt_rc" -eq 1 ]; then
26902717
should_add=false
26912718
fi
26922719
fi
@@ -2951,6 +2978,46 @@ fi
29512978
# ---------------------------------------------------------------------------
29522979
# Optional tools — ripgrep
29532980
# ---------------------------------------------------------------------------
2981+
# Oldest ripgrep the agent's grep tool is expected to work with. The managed
2982+
# installer pins a newer upstream release; this floor only guards the system
2983+
# path (brew/apt/...) so an ancient distro package doesn't install
2984+
# "successfully" and then behave differently at runtime.
2985+
MIN_RIPGREP_VERSION="12.0.0"
2986+
2987+
# version_at_least HAVE WANT — dotted numeric compare (12.0.0 >= 11.0.0). The
2988+
# installer runs before Python exists, so this stays in pure shell.
2989+
version_at_least() {
2990+
local have="$1" want="$2" IFS_save="$IFS"
2991+
case "$have" in ''|*[!0-9.]*) return 1 ;; esac
2992+
IFS=.
2993+
# shellcheck disable=SC2086 # word-splitting on '.' is the point
2994+
set -- $have
2995+
IFS="$IFS_save"
2996+
local have_major="${1:-0}" have_minor="${2:-0}" have_patch="${3:-0}"
2997+
IFS=.
2998+
# shellcheck disable=SC2086
2999+
set -- $want
3000+
IFS="$IFS_save"
3001+
local want_major="${1:-0}" want_minor="${2:-0}" want_patch="${3:-0}"
3002+
[ "$have_major" -gt "$want_major" ] && return 0
3003+
[ "$have_major" -lt "$want_major" ] && return 1
3004+
[ "$have_minor" -gt "$want_minor" ] && return 0
3005+
[ "$have_minor" -lt "$want_minor" ] && return 1
3006+
[ "$have_patch" -ge "$want_patch" ]
3007+
}
3008+
3009+
# version_older_than_have IS MIN — true when IS is present but below MIN.
3010+
version_too_old() {
3011+
local is="$1" min="$2"
3012+
[ -n "$is" ] || return 1
3013+
! version_at_least "$is" "$min"
3014+
}
3015+
3016+
# Print the version of `rg` on PATH, or nothing when it can't be determined.
3017+
installed_rg_version() {
3018+
rg --version 2>/dev/null | head -1 | awk '{print $2}'
3019+
}
3020+
29543021

29553022
# Pre-check: verify sudo is usable before running sudo commands.
29563023
# Returns 0 if sudo is available (cached or passwordless), 1 otherwise.
@@ -2970,52 +3037,69 @@ check_sudo() {
29703037
return 1
29713038
}
29723039

3040+
# True when the `rg` on PATH is present and at least MIN_RIPGREP_VERSION.
3041+
# An install that produced a too-old binary does not count as success: an
3042+
# ancient distro package would otherwise satisfy `command -v rg` while behaving
3043+
# differently from what the grep tool expects.
3044+
installed_rg_is_acceptable() {
3045+
local ver
3046+
command -v rg >/dev/null 2>&1 || return 1
3047+
if ! ver=$(installed_rg_version) || [ -z "$ver" ]; then
3048+
return 1
3049+
fi
3050+
if version_too_old "$ver" "$MIN_RIPGREP_VERSION"; then
3051+
log_warn "ripgrep ${ver} is older than the supported minimum (${MIN_RIPGREP_VERSION})."
3052+
return 1
3053+
fi
3054+
return 0
3055+
}
3056+
29733057
install_ripgrep_via_pkg() {
29743058
case "$OS" in
29753059
macos)
29763060
if command -v brew >/dev/null 2>&1; then
29773061
log_info "Installing ripgrep via Homebrew (this may take a moment)..."
29783062
if HOMEBREW_NO_AUTO_UPDATE=1 brew install ripgrep; then
2979-
command -v rg >/dev/null 2>&1 && return 0
3063+
installed_rg_is_acceptable && return 0
29803064
fi
29813065
fi
29823066
if command -v port >/dev/null 2>&1 && check_sudo; then
29833067
log_info "Installing ripgrep via MacPorts..."
29843068
if sudo port install ripgrep; then
2985-
command -v rg >/dev/null 2>&1 && return 0
3069+
installed_rg_is_acceptable && return 0
29863070
fi
29873071
fi
29883072
;;
29893073
linux)
29903074
if command -v apt-get >/dev/null 2>&1 && check_sudo; then
29913075
log_info "Installing ripgrep via apt-get..."
29923076
if sudo apt-get install -y ripgrep; then
2993-
command -v rg >/dev/null 2>&1 && return 0
3077+
installed_rg_is_acceptable && return 0
29943078
fi
29953079
elif command -v dnf >/dev/null 2>&1 && check_sudo; then
29963080
log_info "Installing ripgrep via dnf..."
29973081
if sudo dnf install -y ripgrep; then
2998-
command -v rg >/dev/null 2>&1 && return 0
3082+
installed_rg_is_acceptable && return 0
29993083
fi
30003084
elif command -v pacman >/dev/null 2>&1 && check_sudo; then
30013085
log_info "Installing ripgrep via pacman..."
30023086
if sudo pacman -S --noconfirm ripgrep; then
3003-
command -v rg >/dev/null 2>&1 && return 0
3087+
installed_rg_is_acceptable && return 0
30043088
fi
30053089
elif command -v zypper >/dev/null 2>&1 && check_sudo; then
30063090
log_info "Installing ripgrep via zypper..."
30073091
if sudo zypper install -y ripgrep; then
3008-
command -v rg >/dev/null 2>&1 && return 0
3092+
installed_rg_is_acceptable && return 0
30093093
fi
30103094
elif command -v apk >/dev/null 2>&1 && check_sudo; then
30113095
log_info "Installing ripgrep via apk..."
30123096
if sudo apk add ripgrep; then
3013-
command -v rg >/dev/null 2>&1 && return 0
3097+
installed_rg_is_acceptable && return 0
30143098
fi
30153099
elif command -v nix-env >/dev/null 2>&1; then
30163100
log_info "Installing ripgrep via nix..."
30173101
if nix-env -iA nixpkgs.ripgrep; then
3018-
command -v rg >/dev/null 2>&1 && return 0
3102+
installed_rg_is_acceptable && return 0
30193103
fi
30203104
fi
30213105
;;
@@ -3028,8 +3112,8 @@ install_ripgrep_via_cargo() {
30283112
log_info "Installing ripgrep via cargo (no sudo needed)..."
30293113
if cargo install ripgrep; then
30303114
fix_owner "${HOME}/.cargo"
3031-
command -v rg >/dev/null 2>&1 && return 0
3032-
log_warn "cargo install succeeded but rg not found in PATH."
3115+
installed_rg_is_acceptable && return 0
3116+
log_warn "cargo install succeeded but rg not found in PATH or too old."
30333117
fi
30343118
fi
30353119
return 1
@@ -3084,10 +3168,17 @@ if [ "$SKIP_OPTIONAL" != "1" ]; then
30843168
fi
30853169
fi
30863170
elif command -v rg >/dev/null 2>&1; then
3087-
if [ "$VERBOSE" = "1" ]; then
3171+
if ! rg_version=$(installed_rg_version) || [ -z "$rg_version" ]; then
3172+
echo ""
3173+
log_warn "Could not determine the version of ripgrep on PATH."
3174+
ripgrep_manual_hint
3175+
elif version_too_old "$rg_version" "$MIN_RIPGREP_VERSION"; then
3176+
echo ""
3177+
log_warn "ripgrep ${rg_version} found, but the minimum supported is ${MIN_RIPGREP_VERSION} — the grep tool may misbehave."
3178+
ripgrep_manual_hint
3179+
elif [ "$VERBOSE" = "1" ]; then
30883180
echo ""
30893181
log_info "Checking optional tools..."
3090-
rg_version=$(rg --version 2>/dev/null | head -1 | awk '{print $2}') || rg_version="(version unknown)"
30913182
log_success "ripgrep ${rg_version} found"
30923183
fi
30933184
else

0 commit comments

Comments
 (0)