From 86f2407dd1efb116d9c796f139d29a64ca860bb3 Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Sat, 26 Sep 2026 15:01:24 +0800 Subject: [PATCH 1/4] fix(installer): require verified binaries before replacement The release-branch shell installer currently warns and replaces an existing binary when its checksum manifest is unavailable or no hash tool is installed. Require one valid SHA-256 entry, verify downloaded bytes in a destination-local temporary file, and leave the prior binary untouched on every failure path. Keep the three published script copies synchronized, preserve detected-client registration behind the new integration opt-out, and stop the installed hook from pointing at the removed codedb_edit tool for native sed/awk commands. Exercise checksum failures, valid install, hooks, and registration with isolated fixtures. Co-Authored-By: Codegraff --- install/install.sh | 51 +++++++------- scripts/test_installer_safety.py | 114 +++++++++++++++++++++++++++++++ website/app/install_script.sh | 51 +++++++------- website/dist/install.sh | 51 +++++++------- 4 files changed, 198 insertions(+), 69 deletions(-) create mode 100644 scripts/test_installer_safety.py diff --git a/install/install.sh b/install/install.sh index 71853166..970f581f 100644 --- a/install/install.sh +++ b/install/install.sh @@ -639,7 +639,7 @@ STRIPPED=$(echo "$STRIPPED" | sed -E 's/^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*=[^[: FIRST=$(echo "$STRIPPED" | awk '{print $1}') case "$FIRST" in - grep|rg|egrep|fgrep|cat|head|tail|sed|awk|find) ;; + grep|rg|egrep|fgrep|cat|head|tail|find) ;; *) exit 0 ;; esac @@ -677,7 +677,6 @@ case "$FIRST" in grep|rg|egrep|fgrep) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_search \"\" (codedb_word for an exact identifier, codedb_callers for call sites) instead of $FIRST — ranked + fewer tokens. Native $FIRST is allowed outside this repo or with CODEDB_NO_HOOKS=1." >&2; exit 2 ;; cat) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_read path= (codedb_outline first for a map) instead of cat. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;; head|tail) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_read path= line_start=.. line_end=.. instead of $FIRST. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;; - sed|awk) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_edit (op=str_replace) instead of $FIRST for edits. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;; find) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_find (fuzzy names) or codedb_glob (patterns) instead of find. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;; esac exit 0 @@ -808,22 +807,24 @@ main() { local dest="$INSTALL_DIR/codedb${ext}" printf " ${D}│${N} %-12s " "codedb" - local tmp="/tmp/codedb.tmp.$$" + local tmp + tmp="$(mktemp "$INSTALL_DIR/.codedb-download.XXXXXX")" + trap 'rm -f "$tmp"' EXIT if curl -fsSL -A 'codedb-installer' "$url" -o "$tmp" 2>/dev/null; then - # Verify checksum when the release publishes a checksum manifest. - local checksum_text expected_hash checksum_notice="" actual_hash="" + # Keep any existing binary until the downloaded bytes are verified. + local checksum_text expected_hash actual_hash="" checksum_text="$(curl -fsSL -A 'codedb-installer' "$checksum_url" 2>/dev/null || true)" - expected_hash="$(printf '%s\n' "$checksum_text" | awk "/codedb-${platform}${ext}\$/ { print \$1 }")" - if [ -n "$expected_hash" ]; then + expected_hash="$(printf '%s\n' "$checksum_text" | awk -v asset="codedb-${platform}${ext}" '$2 == asset { count++; hash=$1 } END { if (count == 1) print hash }')" + if [[ "$expected_hash" =~ ^[0-9a-f]{64}$ ]]; then if command -v sha256sum >/dev/null 2>&1; then actual_hash="$(sha256sum "$tmp" | awk '{print $1}')" elif command -v shasum >/dev/null 2>&1; then actual_hash="$(shasum -a 256 "$tmp" | awk '{print $1}')" fi if [ -z "$actual_hash" ]; then - # No hashing tool on PATH. Never silently install an unverified - # binary — say so in the same place the skipped-manifest case does. - checksum_notice=" ${Y}warning:${N} checksum NOT verified — neither sha256sum nor shasum is on PATH\n" + printf "${R}failed${N}\n" + printf "\n ${R}error: cannot verify download — install sha256sum or shasum first. Existing binary unchanged.${N}\n" >&2 + exit 1 elif [ "$actual_hash" != "$expected_hash" ]; then rm -f "$tmp" printf "${R}failed${N}\n" @@ -833,10 +834,13 @@ main() { exit 1 fi else - checksum_notice=" ${Y}warning:${N} checksum verification skipped (checksums.sha256 unavailable)\n" + printf "${R}failed${N}\n" + printf "\n ${R}error: no unique valid checksum for this platform. Existing binary unchanged; retry when release checksums are available.${N}\n" >&2 + exit 1 fi xattr -c "$tmp" 2>/dev/null || true mv -f "$tmp" "$dest" + trap - EXIT chmod +x "$dest" printf "${G}✓${N}\n" else @@ -848,24 +852,25 @@ main() { echo "" printf " ${G}installed${N} ${D}→ $dest${N}\n" - if [ -n "$checksum_notice" ]; then - printf "$checksum_notice" - fi printf " ${D}claude hook opt-out: CODEDB_NO_HOOKS=1 skips this run; CODEDB_PERSIST_NO_HOOKS=1 or touch ~/.codedb/no-hooks makes it permanent (rm ~/.codedb/no-hooks re-enables)${N}\n" # Register MCP server in coding tools echo "" printf " ${W}registering integrations${N}\n" echo "" - register_claude "$dest" - register_codex "$dest" - register_codex_policy "$dest" - register_gemini "$dest" - register_cursor "$dest" - register_windsurf_devin "$dest" - register_detected_clients "$dest" - register_deepwiki - register_hooks + if [ -z "${CODEDB_NO_INTEGRATIONS:-}" ]; then + register_claude "$dest" + register_codex "$dest" + register_codex_policy "$dest" + register_gemini "$dest" + register_cursor "$dest" + register_windsurf_devin "$dest" + register_detected_clients "$dest" + register_deepwiki + register_hooks + else + printf " ${D}skipped (CODEDB_NO_INTEGRATIONS)${N}\n" + fi print_hook_notes "$dest" # Check PATH diff --git a/scripts/test_installer_safety.py b/scripts/test_installer_safety.py new file mode 100644 index 00000000..7c1f0c67 --- /dev/null +++ b/scripts/test_installer_safety.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Installer failure paths must preserve the binary and avoid client config writes.""" +import hashlib +import os +import json +import re +from pathlib import Path +import shutil +import shlex +import subprocess +import sys +import tempfile +import tomllib + + +def main(): + installer = Path(__file__).resolve().parents[1] / 'install/install.sh' + payload = b'#!/bin/sh\nexit 0\n' + digest = hashlib.sha256(payload).hexdigest() + with tempfile.TemporaryDirectory(prefix='codedb-installer-') as directory: + root = Path(directory) + commands = root / 'commands' + commands.mkdir() + for command in ('mkdir', 'mktemp', 'awk', 'rm', 'mv', 'chmod'): + (commands / command).symlink_to(shutil.which(command)) + (commands / 'uname').write_text('#!/bin/sh\ncase "$1" in -s) echo Darwin;; -m) echo arm64;; esac\n') + (commands / 'uname').chmod(0o755) + curl = commands / 'curl' + curl.write_text('#!' + sys.executable + '\n' + ''' +import os,sys +from pathlib import Path +args=sys.argv[1:];scenario=os.environ['INSTALLER_TEST_SCENARIO'] +digest=os.environ['INSTALLER_TEST_DIGEST'] +if any(a.endswith('checksums.sha256') for a in args): + if scenario=='unavailable':sys.exit(22) + if scenario=='missing':print(digest+' other-platform');sys.exit(0) + if scenario=='malformed':print('not-a-hash codedb-darwin-arm64');sys.exit(0) + if scenario=='duplicate':print((digest+' codedb-darwin-arm64\\n')*2);sys.exit(0) + print(('0'*64 if scenario=='mismatch' else digest)+' codedb-darwin-arm64') +else: + if scenario=='download-failed':sys.exit(22) + Path(args[args.index('-o')+1]).write_bytes(b'#!/bin/sh\\nexit 0\\n') +''') + curl.chmod(0o755) + for scenario in ('unavailable', 'missing', 'malformed', 'duplicate', 'mismatch', 'no-hash-tool', 'download-failed', 'valid'): + target = root / (scenario + ' install with spaces') + target.mkdir() + binary = target / 'codedb' + binary.write_bytes(b'existing installation\n') + hash_tool = commands / 'shasum' + if hash_tool.exists(): + hash_tool.unlink() + if scenario != 'no-hash-tool': + hash_tool.write_text('#!' + sys.executable + '\nimport hashlib,sys\nfrom pathlib import Path\nprint(hashlib.sha256(Path(sys.argv[-1]).read_bytes()).hexdigest()+" binary")\n') + hash_tool.chmod(0o755) + env = {k: v for k, v in os.environ.items() if not k.startswith('CODEDB_')} + env.update(PATH=str(commands), CODEDB_DIR=str(target), CODEDB_VERSION='0.2.5855', + CODEDB_NO_INTEGRATIONS='1', INSTALLER_TEST_SCENARIO=scenario, + INSTALLER_TEST_DIGEST=digest) + done = subprocess.run(['/bin/bash', str(installer)], env=env, capture_output=True, text=True, timeout=20) + success = scenario == 'valid' + assert (done.returncode == 0) == success, (scenario, done.stdout, done.stderr) + assert binary.read_bytes() == (payload if success else b'existing installation\n'), scenario + assert not list(target.glob('.codedb-download.*')), scenario + assert 'unbound variable' not in done.stderr, done.stderr + print('PASS:', scenario) + + # Extract the installed hook verbatim. Relocate its HOME path reference + # in the fixture only; never change the process HOME or real settings. + hook_source = re.search(r'"codedb-block-legacy.sh": r\x27\x27\x27(.*?)\x27\x27\x27,', installer.read_text(), re.S).group(1) + hook = root / 'hook.sh' + hook.write_text(hook_source.replace('$HOME', '${CODEDB_INSTALL_TEST_ROOT}')) + fixture_home = root / 'fixture-user' + project = root / 'project' + project.mkdir() + project = project.resolve() + registration = fixture_home / '.codedb/projects/test/project.txt' + registration.parent.mkdir(parents=True) + registration.write_text(str(project) + '\n') + fake_codedb = commands / 'codedb' + fake_codedb.write_bytes(payload) + fake_codedb.chmod(0o755) + hook_env = {k: v for k, v in os.environ.items() if not k.startswith('CODEDB_')} + hook_env.update(CODEDB_INSTALL_TEST_ROOT=str(fixture_home), PATH=str(commands) + os.pathsep + os.environ['PATH']) + for command, expected in [('sed -i s/old/new/ code.py', 0), ('awk "{print}" code.py', 0), ('rg handler .', 2), ('python3 edit.py', 0)]: + done = subprocess.run(['/bin/bash', str(hook)], cwd=project, env=hook_env, + input=json.dumps({'tool_input': {'command': command}}), + capture_output=True, text=True, timeout=10) + assert done.returncode == expected, (command, done.stderr) + assert 'codedb_edit' not in done.stderr + print('PASS: hook', command.split()[0]) + + # Exercise fresh client registration against fixture config paths. + # Existing unrelated MCP registrations must survive the additive writes. + (fixture_home / '.claude.json').write_text(json.dumps({'mcpServers': {'other': {'command': 'other-tool'}}})) + (fixture_home / '.codex').mkdir() + (fixture_home / '.codex/config.toml').write_text('[mcp_servers.other]\ncommand = "other-tool"\n') + definitions = installer.read_text().rsplit('\nmain\n', 1)[0] + registration_script = root / 'register.sh' + registration_script.write_text(definitions.replace('$HOME', '${CODEDB_INSTALL_TEST_ROOT}') + + '\nregister_claude ' + shlex.quote(str(fake_codedb)) + + '\nregister_codex ' + shlex.quote(str(fake_codedb)) + '\n') + subprocess.run(['/bin/bash', str(registration_script)], env=hook_env, check=True, capture_output=True, text=True) + claude = json.loads((fixture_home / '.claude.json').read_text())['mcpServers'] + codex = tomllib.loads((fixture_home / '.codex/config.toml').read_text())['mcp_servers'] + for name, config in [('claude', claude), ('codex', codex)]: + assert config['other']['command'] == 'other-tool' + assert config['codedb']['command'] == str(fake_codedb) + assert config['codedb']['args'] == ['mcp'] + print('PASS: fresh', name, 'registration preserves other servers') + + +if __name__ == '__main__': + main() diff --git a/website/app/install_script.sh b/website/app/install_script.sh index 71853166..970f581f 100644 --- a/website/app/install_script.sh +++ b/website/app/install_script.sh @@ -639,7 +639,7 @@ STRIPPED=$(echo "$STRIPPED" | sed -E 's/^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*=[^[: FIRST=$(echo "$STRIPPED" | awk '{print $1}') case "$FIRST" in - grep|rg|egrep|fgrep|cat|head|tail|sed|awk|find) ;; + grep|rg|egrep|fgrep|cat|head|tail|find) ;; *) exit 0 ;; esac @@ -677,7 +677,6 @@ case "$FIRST" in grep|rg|egrep|fgrep) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_search \"\" (codedb_word for an exact identifier, codedb_callers for call sites) instead of $FIRST — ranked + fewer tokens. Native $FIRST is allowed outside this repo or with CODEDB_NO_HOOKS=1." >&2; exit 2 ;; cat) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_read path= (codedb_outline first for a map) instead of cat. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;; head|tail) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_read path= line_start=.. line_end=.. instead of $FIRST. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;; - sed|awk) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_edit (op=str_replace) instead of $FIRST for edits. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;; find) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_find (fuzzy names) or codedb_glob (patterns) instead of find. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;; esac exit 0 @@ -808,22 +807,24 @@ main() { local dest="$INSTALL_DIR/codedb${ext}" printf " ${D}│${N} %-12s " "codedb" - local tmp="/tmp/codedb.tmp.$$" + local tmp + tmp="$(mktemp "$INSTALL_DIR/.codedb-download.XXXXXX")" + trap 'rm -f "$tmp"' EXIT if curl -fsSL -A 'codedb-installer' "$url" -o "$tmp" 2>/dev/null; then - # Verify checksum when the release publishes a checksum manifest. - local checksum_text expected_hash checksum_notice="" actual_hash="" + # Keep any existing binary until the downloaded bytes are verified. + local checksum_text expected_hash actual_hash="" checksum_text="$(curl -fsSL -A 'codedb-installer' "$checksum_url" 2>/dev/null || true)" - expected_hash="$(printf '%s\n' "$checksum_text" | awk "/codedb-${platform}${ext}\$/ { print \$1 }")" - if [ -n "$expected_hash" ]; then + expected_hash="$(printf '%s\n' "$checksum_text" | awk -v asset="codedb-${platform}${ext}" '$2 == asset { count++; hash=$1 } END { if (count == 1) print hash }')" + if [[ "$expected_hash" =~ ^[0-9a-f]{64}$ ]]; then if command -v sha256sum >/dev/null 2>&1; then actual_hash="$(sha256sum "$tmp" | awk '{print $1}')" elif command -v shasum >/dev/null 2>&1; then actual_hash="$(shasum -a 256 "$tmp" | awk '{print $1}')" fi if [ -z "$actual_hash" ]; then - # No hashing tool on PATH. Never silently install an unverified - # binary — say so in the same place the skipped-manifest case does. - checksum_notice=" ${Y}warning:${N} checksum NOT verified — neither sha256sum nor shasum is on PATH\n" + printf "${R}failed${N}\n" + printf "\n ${R}error: cannot verify download — install sha256sum or shasum first. Existing binary unchanged.${N}\n" >&2 + exit 1 elif [ "$actual_hash" != "$expected_hash" ]; then rm -f "$tmp" printf "${R}failed${N}\n" @@ -833,10 +834,13 @@ main() { exit 1 fi else - checksum_notice=" ${Y}warning:${N} checksum verification skipped (checksums.sha256 unavailable)\n" + printf "${R}failed${N}\n" + printf "\n ${R}error: no unique valid checksum for this platform. Existing binary unchanged; retry when release checksums are available.${N}\n" >&2 + exit 1 fi xattr -c "$tmp" 2>/dev/null || true mv -f "$tmp" "$dest" + trap - EXIT chmod +x "$dest" printf "${G}✓${N}\n" else @@ -848,24 +852,25 @@ main() { echo "" printf " ${G}installed${N} ${D}→ $dest${N}\n" - if [ -n "$checksum_notice" ]; then - printf "$checksum_notice" - fi printf " ${D}claude hook opt-out: CODEDB_NO_HOOKS=1 skips this run; CODEDB_PERSIST_NO_HOOKS=1 or touch ~/.codedb/no-hooks makes it permanent (rm ~/.codedb/no-hooks re-enables)${N}\n" # Register MCP server in coding tools echo "" printf " ${W}registering integrations${N}\n" echo "" - register_claude "$dest" - register_codex "$dest" - register_codex_policy "$dest" - register_gemini "$dest" - register_cursor "$dest" - register_windsurf_devin "$dest" - register_detected_clients "$dest" - register_deepwiki - register_hooks + if [ -z "${CODEDB_NO_INTEGRATIONS:-}" ]; then + register_claude "$dest" + register_codex "$dest" + register_codex_policy "$dest" + register_gemini "$dest" + register_cursor "$dest" + register_windsurf_devin "$dest" + register_detected_clients "$dest" + register_deepwiki + register_hooks + else + printf " ${D}skipped (CODEDB_NO_INTEGRATIONS)${N}\n" + fi print_hook_notes "$dest" # Check PATH diff --git a/website/dist/install.sh b/website/dist/install.sh index 71853166..970f581f 100644 --- a/website/dist/install.sh +++ b/website/dist/install.sh @@ -639,7 +639,7 @@ STRIPPED=$(echo "$STRIPPED" | sed -E 's/^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*=[^[: FIRST=$(echo "$STRIPPED" | awk '{print $1}') case "$FIRST" in - grep|rg|egrep|fgrep|cat|head|tail|sed|awk|find) ;; + grep|rg|egrep|fgrep|cat|head|tail|find) ;; *) exit 0 ;; esac @@ -677,7 +677,6 @@ case "$FIRST" in grep|rg|egrep|fgrep) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_search \"\" (codedb_word for an exact identifier, codedb_callers for call sites) instead of $FIRST — ranked + fewer tokens. Native $FIRST is allowed outside this repo or with CODEDB_NO_HOOKS=1." >&2; exit 2 ;; cat) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_read path= (codedb_outline first for a map) instead of cat. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;; head|tail) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_read path= line_start=.. line_end=.. instead of $FIRST. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;; - sed|awk) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_edit (op=str_replace) instead of $FIRST for edits. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;; find) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_find (fuzzy names) or codedb_glob (patterns) instead of find. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;; esac exit 0 @@ -808,22 +807,24 @@ main() { local dest="$INSTALL_DIR/codedb${ext}" printf " ${D}│${N} %-12s " "codedb" - local tmp="/tmp/codedb.tmp.$$" + local tmp + tmp="$(mktemp "$INSTALL_DIR/.codedb-download.XXXXXX")" + trap 'rm -f "$tmp"' EXIT if curl -fsSL -A 'codedb-installer' "$url" -o "$tmp" 2>/dev/null; then - # Verify checksum when the release publishes a checksum manifest. - local checksum_text expected_hash checksum_notice="" actual_hash="" + # Keep any existing binary until the downloaded bytes are verified. + local checksum_text expected_hash actual_hash="" checksum_text="$(curl -fsSL -A 'codedb-installer' "$checksum_url" 2>/dev/null || true)" - expected_hash="$(printf '%s\n' "$checksum_text" | awk "/codedb-${platform}${ext}\$/ { print \$1 }")" - if [ -n "$expected_hash" ]; then + expected_hash="$(printf '%s\n' "$checksum_text" | awk -v asset="codedb-${platform}${ext}" '$2 == asset { count++; hash=$1 } END { if (count == 1) print hash }')" + if [[ "$expected_hash" =~ ^[0-9a-f]{64}$ ]]; then if command -v sha256sum >/dev/null 2>&1; then actual_hash="$(sha256sum "$tmp" | awk '{print $1}')" elif command -v shasum >/dev/null 2>&1; then actual_hash="$(shasum -a 256 "$tmp" | awk '{print $1}')" fi if [ -z "$actual_hash" ]; then - # No hashing tool on PATH. Never silently install an unverified - # binary — say so in the same place the skipped-manifest case does. - checksum_notice=" ${Y}warning:${N} checksum NOT verified — neither sha256sum nor shasum is on PATH\n" + printf "${R}failed${N}\n" + printf "\n ${R}error: cannot verify download — install sha256sum or shasum first. Existing binary unchanged.${N}\n" >&2 + exit 1 elif [ "$actual_hash" != "$expected_hash" ]; then rm -f "$tmp" printf "${R}failed${N}\n" @@ -833,10 +834,13 @@ main() { exit 1 fi else - checksum_notice=" ${Y}warning:${N} checksum verification skipped (checksums.sha256 unavailable)\n" + printf "${R}failed${N}\n" + printf "\n ${R}error: no unique valid checksum for this platform. Existing binary unchanged; retry when release checksums are available.${N}\n" >&2 + exit 1 fi xattr -c "$tmp" 2>/dev/null || true mv -f "$tmp" "$dest" + trap - EXIT chmod +x "$dest" printf "${G}✓${N}\n" else @@ -848,24 +852,25 @@ main() { echo "" printf " ${G}installed${N} ${D}→ $dest${N}\n" - if [ -n "$checksum_notice" ]; then - printf "$checksum_notice" - fi printf " ${D}claude hook opt-out: CODEDB_NO_HOOKS=1 skips this run; CODEDB_PERSIST_NO_HOOKS=1 or touch ~/.codedb/no-hooks makes it permanent (rm ~/.codedb/no-hooks re-enables)${N}\n" # Register MCP server in coding tools echo "" printf " ${W}registering integrations${N}\n" echo "" - register_claude "$dest" - register_codex "$dest" - register_codex_policy "$dest" - register_gemini "$dest" - register_cursor "$dest" - register_windsurf_devin "$dest" - register_detected_clients "$dest" - register_deepwiki - register_hooks + if [ -z "${CODEDB_NO_INTEGRATIONS:-}" ]; then + register_claude "$dest" + register_codex "$dest" + register_codex_policy "$dest" + register_gemini "$dest" + register_cursor "$dest" + register_windsurf_devin "$dest" + register_detected_clients "$dest" + register_deepwiki + register_hooks + else + printf " ${D}skipped (CODEDB_NO_INTEGRATIONS)${N}\n" + fi print_hook_notes "$dest" # Check PATH From 81a2c994d880dcb7e8e1a90958514de2ae71591b Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Sat, 26 Sep 2026 15:17:29 +0800 Subject: [PATCH 2/4] test(installer): cover normal registration and installed hook The failure-path fixture opted out of integrations and exercised an extracted hook, so it did not prove the normal installation dispatch. Run a verified install inside a disposable HOME, assert detected-client registration preserves other servers, and invoke the hook file installed by main. Keep every failure scenario isolated from real client configuration. Co-Authored-By: Codegraff --- scripts/test_installer_safety.py | 89 ++++++++++++++++++++------------ 1 file changed, 55 insertions(+), 34 deletions(-) diff --git a/scripts/test_installer_safety.py b/scripts/test_installer_safety.py index 7c1f0c67..33c51208 100644 --- a/scripts/test_installer_safety.py +++ b/scripts/test_installer_safety.py @@ -3,10 +3,8 @@ import hashlib import os import json -import re from pathlib import Path import shutil -import shlex import subprocess import sys import tempfile @@ -53,61 +51,84 @@ def main(): if scenario != 'no-hash-tool': hash_tool.write_text('#!' + sys.executable + '\nimport hashlib,sys\nfrom pathlib import Path\nprint(hashlib.sha256(Path(sys.argv[-1]).read_bytes()).hexdigest()+" binary")\n') hash_tool.chmod(0o755) + fixture_home = root / (scenario + ' home') + fixture_home.mkdir() env = {k: v for k, v in os.environ.items() if not k.startswith('CODEDB_')} - env.update(PATH=str(commands), CODEDB_DIR=str(target), CODEDB_VERSION='0.2.5855', - CODEDB_NO_INTEGRATIONS='1', INSTALLER_TEST_SCENARIO=scenario, - INSTALLER_TEST_DIGEST=digest) + env.update(HOME=str(fixture_home), PATH=str(commands), CODEDB_DIR=str(target), + CODEDB_VERSION='0.2.5855', CODEDB_NO_INTEGRATIONS='1', + INSTALLER_TEST_SCENARIO=scenario, INSTALLER_TEST_DIGEST=digest) done = subprocess.run(['/bin/bash', str(installer)], env=env, capture_output=True, text=True, timeout=20) success = scenario == 'valid' assert (done.returncode == 0) == success, (scenario, done.stdout, done.stderr) assert binary.read_bytes() == (payload if success else b'existing installation\n'), scenario assert not list(target.glob('.codedb-download.*')), scenario assert 'unbound variable' not in done.stderr, done.stderr + assert not (fixture_home / '.claude.json').exists(), scenario + assert not (fixture_home / '.codex').exists(), scenario + assert not (fixture_home / '.claude').exists(), scenario print('PASS:', scenario) - # Extract the installed hook verbatim. Relocate its HOME path reference - # in the fixture only; never change the process HOME or real settings. - hook_source = re.search(r'"codedb-block-legacy.sh": r\x27\x27\x27(.*?)\x27\x27\x27,', installer.read_text(), re.S).group(1) - hook = root / 'hook.sh' - hook.write_text(hook_source.replace('$HOME', '${CODEDB_INSTALL_TEST_ROOT}')) + # Drive the normal main() path inside a disposable HOME. This must + # reach detected-client registration and install the hook we exercise. fixture_home = root / 'fixture-user' + fixture_home.mkdir() project = root / 'project' project.mkdir() project = project.resolve() registration = fixture_home / '.codedb/projects/test/project.txt' registration.parent.mkdir(parents=True) registration.write_text(str(project) + '\n') + claude_config = fixture_home / '.claude.json' + claude_config.write_text(json.dumps({'mcpServers': {'other': {'command': 'other-tool'}}})) + codex_config = fixture_home / '.codex/config.toml' + codex_config.parent.mkdir() + codex_config.write_text('[mcp_servers.other]\ncommand = "other-tool"\n') + qwen_config = fixture_home / '.qwen/settings.json' + qwen_config.parent.mkdir() + qwen_config.write_text(json.dumps({'mcpServers': {'other': {'command': 'other-tool'}}})) + normal_target = root / 'normal install with spaces' + normal_target.mkdir() + binary = normal_target / 'codedb' + binary.write_bytes(b'existing installation\n') fake_codedb = commands / 'codedb' fake_codedb.write_bytes(payload) fake_codedb.chmod(0o755) - hook_env = {k: v for k, v in os.environ.items() if not k.startswith('CODEDB_')} - hook_env.update(CODEDB_INSTALL_TEST_ROOT=str(fixture_home), PATH=str(commands) + os.pathsep + os.environ['PATH']) - for command, expected in [('sed -i s/old/new/ code.py', 0), ('awk "{print}" code.py', 0), ('rg handler .', 2), ('python3 edit.py', 0)]: - done = subprocess.run(['/bin/bash', str(hook)], cwd=project, env=hook_env, + normal_env = {k: v for k, v in os.environ.items() if not k.startswith('CODEDB_')} + normal_env.update(HOME=str(fixture_home), PATH=str(commands) + os.pathsep + os.environ['PATH'], + CODEDB_DIR=str(normal_target), CODEDB_VERSION='0.2.5855', + CODEDB_INSTALL_DEEPWIKI='0', INSTALLER_TEST_SCENARIO='valid', + INSTALLER_TEST_DIGEST=digest) + done = subprocess.run(['/bin/bash', str(installer)], env=normal_env, + capture_output=True, text=True, timeout=20) + assert done.returncode == 0, (done.stdout, done.stderr) + assert binary.read_bytes() == payload + assert not list(normal_target.glob('.codedb-download.*')) + print('PASS: normal install through main') + + configs = [ + ('claude', json.loads(claude_config.read_text())['mcpServers']), + ('codex', tomllib.loads(codex_config.read_text())['mcp_servers']), + ('qwen detected client', json.loads(qwen_config.read_text())['mcpServers']), + ] + for name, config in configs: + assert config['other']['command'] == 'other-tool' + assert config['codedb']['command'] == str(binary) + assert config['codedb']['args'] == ['mcp'] + print('PASS: normal', name, 'registration preserves other servers') + + hook = fixture_home / '.claude/hooks/codedb-block-legacy.sh' + assert hook.is_file() and os.access(hook, os.X_OK) + settings = json.loads((fixture_home / '.claude/settings.json').read_text()) + assert any('codedb-block-legacy.sh' in entry['hooks'][0]['command'] + for entry in settings['hooks']['PreToolUse']) + for command, expected in [('sed -i s/old/new/ code.py', 0), ('awk "{print}" code.py', 0), + ('rg handler .', 2), ('python3 edit.py', 0)]: + done = subprocess.run(['/bin/bash', str(hook)], cwd=project, env=normal_env, input=json.dumps({'tool_input': {'command': command}}), capture_output=True, text=True, timeout=10) assert done.returncode == expected, (command, done.stderr) assert 'codedb_edit' not in done.stderr - print('PASS: hook', command.split()[0]) - - # Exercise fresh client registration against fixture config paths. - # Existing unrelated MCP registrations must survive the additive writes. - (fixture_home / '.claude.json').write_text(json.dumps({'mcpServers': {'other': {'command': 'other-tool'}}})) - (fixture_home / '.codex').mkdir() - (fixture_home / '.codex/config.toml').write_text('[mcp_servers.other]\ncommand = "other-tool"\n') - definitions = installer.read_text().rsplit('\nmain\n', 1)[0] - registration_script = root / 'register.sh' - registration_script.write_text(definitions.replace('$HOME', '${CODEDB_INSTALL_TEST_ROOT}') + - '\nregister_claude ' + shlex.quote(str(fake_codedb)) + - '\nregister_codex ' + shlex.quote(str(fake_codedb)) + '\n') - subprocess.run(['/bin/bash', str(registration_script)], env=hook_env, check=True, capture_output=True, text=True) - claude = json.loads((fixture_home / '.claude.json').read_text())['mcpServers'] - codex = tomllib.loads((fixture_home / '.codex/config.toml').read_text())['mcp_servers'] - for name, config in [('claude', claude), ('codex', codex)]: - assert config['other']['command'] == 'other-tool' - assert config['codedb']['command'] == str(fake_codedb) - assert config['codedb']['args'] == ['mcp'] - print('PASS: fresh', name, 'registration preserves other servers') + print('PASS: installed hook', command.split()[0]) if __name__ == '__main__': From 07a44bd12f4cc9431dd528c42281f199eb14585e Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Sat, 26 Sep 2026 15:27:27 +0800 Subject: [PATCH 3/4] test(installer): prove checksum failure stops registration The existing failure fixtures opt out of integrations, which cannot prove that an invalid checksum stops before client configuration is touched. Run the mismatched-hash case with integrations enabled in a disposable HOME and assert its preexisting Claude registration remains byte-for-byte unchanged. Co-Authored-By: Codegraff --- scripts/test_installer_safety.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/test_installer_safety.py b/scripts/test_installer_safety.py index 33c51208..84abb8f0 100644 --- a/scripts/test_installer_safety.py +++ b/scripts/test_installer_safety.py @@ -53,17 +53,29 @@ def main(): hash_tool.chmod(0o755) fixture_home = root / (scenario + ' home') fixture_home.mkdir() + claude_config = fixture_home / '.claude.json' + existing_config = json.dumps({'mcpServers': {'other': {'command': 'other-tool'}}}) + if scenario == 'mismatch': + claude_config.write_text(existing_config) env = {k: v for k, v in os.environ.items() if not k.startswith('CODEDB_')} env.update(HOME=str(fixture_home), PATH=str(commands), CODEDB_DIR=str(target), CODEDB_VERSION='0.2.5855', CODEDB_NO_INTEGRATIONS='1', INSTALLER_TEST_SCENARIO=scenario, INSTALLER_TEST_DIGEST=digest) + if scenario == 'mismatch': + # A checksum failure must stop before normal client registration. + env.pop('CODEDB_NO_INTEGRATIONS') + env['CODEDB_INSTALL_DEEPWIKI'] = '0' + env['PATH'] += os.pathsep + os.environ['PATH'] done = subprocess.run(['/bin/bash', str(installer)], env=env, capture_output=True, text=True, timeout=20) success = scenario == 'valid' assert (done.returncode == 0) == success, (scenario, done.stdout, done.stderr) assert binary.read_bytes() == (payload if success else b'existing installation\n'), scenario assert not list(target.glob('.codedb-download.*')), scenario assert 'unbound variable' not in done.stderr, done.stderr - assert not (fixture_home / '.claude.json').exists(), scenario + if scenario == 'mismatch': + assert claude_config.read_text() == existing_config, scenario + else: + assert not claude_config.exists(), scenario assert not (fixture_home / '.codex').exists(), scenario assert not (fixture_home / '.claude').exists(), scenario print('PASS:', scenario) From 2ba90d4518906c4c26c5b819a3f4e43a96b84c02 Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Sat, 26 Sep 2026 16:45:56 +0800 Subject: [PATCH 4/4] fix(installer): reject malformed selected checksum lines The verifier matched the selected asset in the second field but ignored trailing fields, so a malformed manifest line with a correct hash could authorize replacement. Require exactly two fields on each selected-asset entry and count malformed duplicates as conflicts. Exercise the trailing-field case through the installer's main path with integrations enabled; failure must leave the prior binary and client configuration unchanged. Co-Authored-By: Codegraff --- install/install.sh | 2 +- scripts/test_installer_safety.py | 9 +++++---- website/app/install_script.sh | 2 +- website/dist/install.sh | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/install/install.sh b/install/install.sh index 970f581f..8ead80da 100644 --- a/install/install.sh +++ b/install/install.sh @@ -814,7 +814,7 @@ main() { # Keep any existing binary until the downloaded bytes are verified. local checksum_text expected_hash actual_hash="" checksum_text="$(curl -fsSL -A 'codedb-installer' "$checksum_url" 2>/dev/null || true)" - expected_hash="$(printf '%s\n' "$checksum_text" | awk -v asset="codedb-${platform}${ext}" '$2 == asset { count++; hash=$1 } END { if (count == 1) print hash }')" + expected_hash="$(printf '%s\n' "$checksum_text" | awk -v asset="codedb-${platform}${ext}" '$2 == asset { count++; if (NF != 2) malformed=1; hash=$1 } END { if (count == 1 && !malformed) print hash }')" if [[ "$expected_hash" =~ ^[0-9a-f]{64}$ ]]; then if command -v sha256sum >/dev/null 2>&1; then actual_hash="$(sha256sum "$tmp" | awk '{print $1}')" diff --git a/scripts/test_installer_safety.py b/scripts/test_installer_safety.py index 84abb8f0..ddfd4869 100644 --- a/scripts/test_installer_safety.py +++ b/scripts/test_installer_safety.py @@ -33,6 +33,7 @@ def main(): if scenario=='unavailable':sys.exit(22) if scenario=='missing':print(digest+' other-platform');sys.exit(0) if scenario=='malformed':print('not-a-hash codedb-darwin-arm64');sys.exit(0) + if scenario=='trailing-field':print(digest+' codedb-darwin-arm64 extra-field');sys.exit(0) if scenario=='duplicate':print((digest+' codedb-darwin-arm64\\n')*2);sys.exit(0) print(('0'*64 if scenario=='mismatch' else digest)+' codedb-darwin-arm64') else: @@ -40,7 +41,7 @@ def main(): Path(args[args.index('-o')+1]).write_bytes(b'#!/bin/sh\\nexit 0\\n') ''') curl.chmod(0o755) - for scenario in ('unavailable', 'missing', 'malformed', 'duplicate', 'mismatch', 'no-hash-tool', 'download-failed', 'valid'): + for scenario in ('unavailable', 'missing', 'malformed', 'trailing-field', 'duplicate', 'mismatch', 'no-hash-tool', 'download-failed', 'valid'): target = root / (scenario + ' install with spaces') target.mkdir() binary = target / 'codedb' @@ -55,13 +56,13 @@ def main(): fixture_home.mkdir() claude_config = fixture_home / '.claude.json' existing_config = json.dumps({'mcpServers': {'other': {'command': 'other-tool'}}}) - if scenario == 'mismatch': + if scenario in ('mismatch', 'trailing-field'): claude_config.write_text(existing_config) env = {k: v for k, v in os.environ.items() if not k.startswith('CODEDB_')} env.update(HOME=str(fixture_home), PATH=str(commands), CODEDB_DIR=str(target), CODEDB_VERSION='0.2.5855', CODEDB_NO_INTEGRATIONS='1', INSTALLER_TEST_SCENARIO=scenario, INSTALLER_TEST_DIGEST=digest) - if scenario == 'mismatch': + if scenario in ('mismatch', 'trailing-field'): # A checksum failure must stop before normal client registration. env.pop('CODEDB_NO_INTEGRATIONS') env['CODEDB_INSTALL_DEEPWIKI'] = '0' @@ -72,7 +73,7 @@ def main(): assert binary.read_bytes() == (payload if success else b'existing installation\n'), scenario assert not list(target.glob('.codedb-download.*')), scenario assert 'unbound variable' not in done.stderr, done.stderr - if scenario == 'mismatch': + if scenario in ('mismatch', 'trailing-field'): assert claude_config.read_text() == existing_config, scenario else: assert not claude_config.exists(), scenario diff --git a/website/app/install_script.sh b/website/app/install_script.sh index 970f581f..8ead80da 100644 --- a/website/app/install_script.sh +++ b/website/app/install_script.sh @@ -814,7 +814,7 @@ main() { # Keep any existing binary until the downloaded bytes are verified. local checksum_text expected_hash actual_hash="" checksum_text="$(curl -fsSL -A 'codedb-installer' "$checksum_url" 2>/dev/null || true)" - expected_hash="$(printf '%s\n' "$checksum_text" | awk -v asset="codedb-${platform}${ext}" '$2 == asset { count++; hash=$1 } END { if (count == 1) print hash }')" + expected_hash="$(printf '%s\n' "$checksum_text" | awk -v asset="codedb-${platform}${ext}" '$2 == asset { count++; if (NF != 2) malformed=1; hash=$1 } END { if (count == 1 && !malformed) print hash }')" if [[ "$expected_hash" =~ ^[0-9a-f]{64}$ ]]; then if command -v sha256sum >/dev/null 2>&1; then actual_hash="$(sha256sum "$tmp" | awk '{print $1}')" diff --git a/website/dist/install.sh b/website/dist/install.sh index 970f581f..8ead80da 100644 --- a/website/dist/install.sh +++ b/website/dist/install.sh @@ -814,7 +814,7 @@ main() { # Keep any existing binary until the downloaded bytes are verified. local checksum_text expected_hash actual_hash="" checksum_text="$(curl -fsSL -A 'codedb-installer' "$checksum_url" 2>/dev/null || true)" - expected_hash="$(printf '%s\n' "$checksum_text" | awk -v asset="codedb-${platform}${ext}" '$2 == asset { count++; hash=$1 } END { if (count == 1) print hash }')" + expected_hash="$(printf '%s\n' "$checksum_text" | awk -v asset="codedb-${platform}${ext}" '$2 == asset { count++; if (NF != 2) malformed=1; hash=$1 } END { if (count == 1 && !malformed) print hash }')" if [[ "$expected_hash" =~ ^[0-9a-f]{64}$ ]]; then if command -v sha256sum >/dev/null 2>&1; then actual_hash="$(sha256sum "$tmp" | awk '{print $1}')"