Skip to content

Commit afd67c5

Browse files
committed
refactor(cli): install scripts own updating; the binary carries no URL
Completes the move started on Windows. `update` prints the install script's command and exits 0 on every platform; the download/extract/chmod/exec sequence is compiled out of release builds entirely, and the MCP background thread that ran cbm_popen("curl ... api.github.com ...") on first tool call is gone. The installers now place themselves beside the binary, which closes the last gap: `update` referenced a raw.githubusercontent URL purely because an install.sh-installed user had no local copy to point at. Two details make that safe. The source is the install script from the archive we JUST checksum-verified, never "$0" -- which does not exist under `curl | bash`, and would pin the user to the OLD installer forever. Since the script ships inside the verified archive, it inherits that verification instead of needing its own. And it is published by atomic rename, never written over the live path. Bash reads a script incrementally by byte offset, so overwriting the file it is executing continues reading NEW bytes at the OLD offset: silent, bizarre corruption. cp to a temp name then mv -f swaps the directory entry while the running shell keeps its original inode. Windows follows the same rule via Copy-Item + Move-Item even though PowerShell parses up front. Deliberately NOT done: spawning the new installer to delete and replace its predecessor. Fetch remote code -> drop to disk -> spawn -> self-delete is a textbook stager, and self-deletion is among the most heavily weighted heuristics there is. It also gains nothing -- `"$DLBIN" install` already runs the NEW release's install logic, because the binary owns the install. Net: zero install or download URLs remain in the shipped binary, and the allow-list entry is retired with it. Both scripts scan CLEAN (0/61) with the self-placement code in them. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1 parent a28749f commit afd67c5

5 files changed

Lines changed: 74 additions & 10 deletions

File tree

‎install.ps1‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,28 @@ if ($LASTEXITCODE -ne 0) {
300300
exit 1
301301
}
302302

303+
# Place the installer beside the binary so `update` points at a local file
304+
# rather than a URL, and so the next update runs THIS release's installer.
305+
#
306+
# Sourced from the archive we just checksum-verified, and published by rename
307+
# rather than written over the live path. PowerShell parses a script fully
308+
# before executing it, so self-overwrite is less hazardous here than it is for
309+
# bash -- but rename costs nothing and keeps both platforms on one rule.
310+
# Best effort: a failure here still leaves a working install.
311+
$DownloadedInstaller = Join-Path $TmpDir "install.ps1"
312+
if (Test-Path -LiteralPath $DownloadedInstaller -PathType Leaf) {
313+
$InstallerDest = Join-Path $InstallDir "install.ps1"
314+
$InstallerTmp = "$InstallerDest.new"
315+
try {
316+
Copy-Item -LiteralPath $DownloadedInstaller -Destination $InstallerTmp -Force -ErrorAction Stop
317+
Move-Item -LiteralPath $InstallerTmp -Destination $InstallerDest -Force -ErrorAction Stop
318+
Write-Host "Installed updater -> $InstallerDest"
319+
} catch {
320+
Remove-Item -LiteralPath $InstallerTmp -Force -ErrorAction SilentlyContinue
321+
Write-Host "note: could not place install.ps1 in $InstallDir (update will explain where to find it)"
322+
}
323+
}
324+
303325
# Verify
304326
try {
305327
$ver = & $Dest --version 2>&1

‎install.sh‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,32 @@ if [ "$SKIP_CONFIG" = true ]; then
268268
fi
269269
"$DLBIN" install "${INSTALL_ARGS[@]}"
270270

271+
# Place the installer beside the binary so `update` can point at a local file
272+
# instead of a URL, and so the next update uses THIS release's installer.
273+
#
274+
# Two things make this safe. The source is the copy from the archive we just
275+
# checksum-verified -- never "$0", which does not exist under `curl | bash` and
276+
# would pin us to the OLD installer forever. And it is published by atomic
277+
# rename, never by writing over the live path: bash reads a script incrementally
278+
# by byte offset, so overwriting the file it is executing continues reading the
279+
# NEW bytes at the OLD offset. That fails silently and bizarrely, which is worse
280+
# than failing loudly.
281+
#
282+
# Best effort by design: a user who cannot write to INSTALL_DIR still gets a
283+
# working install, and `update` falls back to explaining where to find it.
284+
DL_INSTALLER="$DLDIR/install.sh"
285+
if [ -f "$DL_INSTALLER" ]; then
286+
INSTALLER_TMP="$INSTALL_DIR/.install.sh.$$"
287+
if cp "$DL_INSTALLER" "$INSTALLER_TMP" 2>/dev/null &&
288+
chmod 755 "$INSTALLER_TMP" 2>/dev/null &&
289+
mv -f "$INSTALLER_TMP" "$INSTALL_DIR/install.sh" 2>/dev/null; then
290+
echo "Installed updater -> $INSTALL_DIR/install.sh"
291+
else
292+
rm -f "$INSTALLER_TMP" 2>/dev/null || true
293+
echo "note: could not place install.sh in $INSTALL_DIR (update will explain where to find it)"
294+
fi
295+
fi
296+
271297
# Verify
272298
VERSION=$("$DEST" --version 2>&1) || {
273299
echo "error: installed binary failed to run" >&2

‎scripts/security-allowlist.txt‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ src/ui/http_server.c:execl:exec indexing binary in child process
6666
URL:https://api.github.com/repos/DeusData/codebase-memory-mcp/releases/latest:update check
6767
URL:https://github.com/DeusData/codebase-memory-mcp/releases/latest/download:binary download + checksums
6868
URL:https://github.com/DeusData/codebase-memory-mcp/releases/latest:version check via redirect header
69-
URL:https://raw.githubusercontent.com/DeusData/codebase-memory-mcp/main/install.sh:printed by `update` as the install-script command when install.sh is not beside the binary (a string in the message, not a network call - the binary no longer downloads anything)
7069
URL:http://127.0.0.1:UI server binding (localhost only)
7170
URL:https://www.sqlite.org/c3ref/c_checkpoint_full.html:sqlite WAL checkpoint API doc reference (comment only, not a network call)
7271
URL:https://github.com/DeusData/codebase-memory-mcp:project repository self-reference in update/star notice (src/mcp/mcp.c)

‎scripts/smoke-test.sh‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,27 @@ fi
151151
trap 'smoke_rmtree "$TMPDIR" "${DRYRUN_HOME:-}"' EXIT
152152

153153
CLI_STDERR=$(smoke_mktemp_file)
154-
cli() { "$BINARY" cli "$@" 2>"$CLI_STDERR"; }
154+
# 10 of the cli call sites assign directly (VAR=$(cli ...)). Under
155+
# `set -euo pipefail` a non-zero exit there kills the smoke with NOTHING
156+
# printed: no FAIL line, no stderr, just an abort indistinguishable from a hang,
157+
# a starved runner, or a real regression. One such abort cost a full Windows
158+
# cycle just to locate, and still could not be attributed. Surface the command
159+
# and its stderr here, while we still can.
160+
#
161+
# Neutral wording on purpose: one call site deliberately expects a non-zero exit
162+
# (the unknown-function query must error loudly), so this must not read as a
163+
# failure on its own.
164+
cli() {
165+
local rc=0
166+
"$BINARY" cli "$@" 2>"$CLI_STDERR" || rc=$?
167+
if [ "$rc" -ne 0 ]; then
168+
{
169+
printf 'cli: `%s` exited %s\n' "$*" "$rc"
170+
sed 's/^/ /' "$CLI_STDERR" 2>/dev/null
171+
} >&2
172+
fi
173+
return "$rc"
174+
}
155175

156176
echo "=== Phase 1: version ==="
157177
VERSION_STATUS=0

‎src/cli/cli.c‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11168,12 +11168,6 @@ static bool check_already_latest(void) {
1116811168

1116911169
#endif /* CBM_CLI_ENABLE_TEST_API */
1117011170

11171-
/* One token on purpose: the security audit extracts URLs literally, so a URL
11172-
* split across string-literal continuations cannot be matched against the
11173-
* allow-list. Printed for the user to paste; never fetched by this binary. */
11174-
#define CBM_INSTALL_SH_URL \
11175-
"https://raw.githubusercontent.com/DeusData/codebase-memory-mcp/main/install.sh"
11176-
1117711171
int cbm_cmd_update(int argc, char **argv) {
1117811172
parse_auto_answer(argc, argv);
1117911173

@@ -11253,7 +11247,9 @@ int cbm_cmd_update(int argc, char **argv) {
1125311247
if (have_dir) {
1125411248
printf(" powershell -ExecutionPolicy Bypass -File \"%s\\install.ps1\"\n\n", self_dir);
1125511249
} else {
11256-
printf(" powershell -ExecutionPolicy Bypass -File install.ps1\n\n");
11250+
printf(" powershell -ExecutionPolicy Bypass -File install.ps1\n"
11251+
" (ships in the release archive, and is placed beside the\n"
11252+
" binary on install)\n\n");
1125711253
}
1125811254
printf("It downloads the latest release, verifies its checksum, and replaces\n"
1125911255
"this binary in place. If PowerShell refuses to run the script because\n"
@@ -11263,7 +11259,8 @@ int cbm_cmd_update(int argc, char **argv) {
1126311259
if (have_dir) {
1126411260
printf(" bash \"%s/install.sh\"\n\n", self_dir);
1126511261
} else {
11266-
printf(" curl -fsSL " CBM_INSTALL_SH_URL " | bash\n\n");
11262+
printf(" install.sh (ships in the release archive, and is placed\n"
11263+
" beside the binary on install)\n\n");
1126711264
}
1126811265
printf("It downloads the latest release, verifies its checksum, and replaces\n"
1126911266
"this binary in place. install.sh is idempotent, so re-running it IS\n"

0 commit comments

Comments
 (0)