mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-12 09:47:20 +08:00
refactor(cli): move self-update out of the product on every platform
Windows already handed updating to install.ps1, because a running .exe cannot
replace its own image. The remaining platforms now do the same, for a different
and better reason.
An in-process updater is structurally a downloader: it fetches a remote archive,
verifies it, extracts it, marks the result executable and runs it. That composite
lived in every shipped artifact -- along with the release URLs it fetched -- to
serve a command most users run a handful of times. The install scripts already
do all of it, are idempotent (so re-running one IS the update), and run while cbm
is not running.
`update` now prints the exact command for the running platform and exits 0:
unix bash "<dir>/install.sh" (or the curl one-liner when not adjacent)
windows powershell -ExecutionPolicy Bypass -File "<dir>\install.ps1"
Both scripts ship inside their release archive, so the printed path resolves
next to the binary. Flags are still parsed and validated, so `update --dry-run
--ui` keeps rejecting typos rather than silently accepting them.
The updater itself is EXCLUDED from release builds rather than left to dead-code
elimination: build_update_url, download_verify_install, extract_and_install_binary,
checksum fetch, detect_os/detect_arch and the cli_download_* helpers now compile
only under CBM_CLI_ENABLE_TEST_API, which release builds do not define. The C
suite still covers the flow through the activation test seam. Verified against
the release binary: `releases/latest/download` is gone entirely.
Also removes the MCP background update check. It ran on the first tool call and
did:
cbm_popen("curl -sf --max-time 5 -H 'Accept: application/vnd.github+json' "
"'https://api.github.com/repos/.../releases/latest' 2>/dev/null")
An embedded shell command plus a startup network callout, in every session, to
tell users a newer version exists. It shelled out, depended on curl being
installed, and phoned home from every agent session. The install scripts report
versions; this did not need to be in the server.
Smoke follows the same collapse. Phases 6c and 14 asserted a Windows-specific
handoff and a POSIX in-process replacement; they now assert one platform-neutral
contract selected by UPDATE_SCRIPT, and 14a's byte-identity check compares the
driver against ITSELF before and after -- comparing against "$BINARY" passed on
Windows only because Windows has no signing step, while the POSIX fixture
ad-hoc re-signs its copy and so differed before `update` ever ran.
The Linux glibc guard is RELOCATED, not retired. The standard linux asset links
glibc 2.38+ and breaks Debian 11, RHEL 8 and Ubuntu 20.04, so the installer must
fetch the static "-portable" build. That constraint moved from the binary to
install.sh, and the smoke now guards it where the behaviour lives.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
+84
-28
@@ -6759,6 +6759,7 @@ int cbm_cli_checksum_manifest_digest(const char *manifest_path, const char *arch
|
||||
|
||||
/* ── Download helper (shell-free curl via exec) ───────────────── */
|
||||
|
||||
#ifdef CBM_CLI_ENABLE_TEST_API
|
||||
static bool cli_download_is_explicit_file_override(const char *url) {
|
||||
char override_buffer[CLI_BUF_512];
|
||||
const char *override =
|
||||
@@ -6782,6 +6783,7 @@ static const char *cli_download_protocol(const char *url) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Download primitives: update was their only caller. */
|
||||
static int cbm_download_to_file(const char *url, const char *dest) {
|
||||
const char *protocol = cli_download_protocol(url);
|
||||
if (!protocol || !dest) {
|
||||
@@ -6810,6 +6812,8 @@ static int cbm_download_to_file_quiet(const char *url, const char *dest) {
|
||||
return cbm_exec_no_shell(argv);
|
||||
}
|
||||
|
||||
#endif /* CBM_CLI_ENABLE_TEST_API */
|
||||
|
||||
/* ── macOS ad-hoc signing ─────────────────────────────────────── */
|
||||
|
||||
#ifdef __APPLE__
|
||||
@@ -6824,6 +6828,7 @@ static int cbm_macos_adhoc_sign(const char *binary_path) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CBM_CLI_ENABLE_TEST_API
|
||||
/* Download checksums.txt and verify the archive integrity. Every non-zero
|
||||
* result is a fail-closed refusal; verification is never optional. */
|
||||
static int verify_download_checksum(const char *archive_path, const char *archive_name) {
|
||||
@@ -6902,8 +6907,11 @@ static int verify_download_checksum(const char *archive_path, const char *archiv
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif /* CBM_CLI_ENABLE_TEST_API */
|
||||
|
||||
/* ── Detect OS/arch for download URL ──────────────────────────── */
|
||||
|
||||
#ifdef CBM_CLI_ENABLE_TEST_API
|
||||
static const char *detect_os(void) {
|
||||
#ifdef _WIN32
|
||||
return "windows";
|
||||
@@ -6922,6 +6930,8 @@ static const char *detect_arch(void) {
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif /* CBM_CLI_ENABLE_TEST_API */
|
||||
|
||||
/* ── Agent config install/refresh (shared by install + update) ── */
|
||||
|
||||
static void print_detected_registry_agents(const char *home, bool *any);
|
||||
@@ -10739,6 +10749,7 @@ typedef struct {
|
||||
bool delete_indexes;
|
||||
} extract_install_args_t;
|
||||
|
||||
#ifdef CBM_CLI_ENABLE_TEST_API
|
||||
typedef struct {
|
||||
const char *bin_dest;
|
||||
const char *home;
|
||||
@@ -10942,6 +10953,13 @@ static int extract_and_install_binary(extract_install_args_t args) {
|
||||
return activation_rc == CLI_OK ? CLI_OK : CLI_TRUE;
|
||||
}
|
||||
|
||||
#endif /* CBM_CLI_ENABLE_TEST_API */
|
||||
|
||||
#ifdef CBM_CLI_ENABLE_TEST_API
|
||||
/* Update-only helpers. The release build hands updating to the install
|
||||
* script, so none of this ships: no release URL construction, no archive
|
||||
* download, no extract-and-exec. Retained for the C suite, which still
|
||||
* covers the flow through the activation test seam. */
|
||||
/* Build the download URL for the update command. */
|
||||
static void build_update_url(char *url, int url_sz, const char *os, const char *arch,
|
||||
const char *ext, bool want_ui) {
|
||||
@@ -11148,6 +11166,8 @@ static bool check_already_latest(void) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif /* CBM_CLI_ENABLE_TEST_API */
|
||||
|
||||
int cbm_cmd_update(int argc, char **argv) {
|
||||
parse_auto_answer(argc, argv);
|
||||
|
||||
@@ -11170,53 +11190,88 @@ int cbm_cmd_update(int argc, char **argv) {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
/* Windows updates run from install.ps1, not from this process.
|
||||
/* Updates run from the install script, not from this process — on every
|
||||
* platform.
|
||||
*
|
||||
* A running .exe cannot replace itself on Windows — the image lock refuses
|
||||
* the replace — so an in-process updater needs a second, permanently
|
||||
* resident binary to swap the first one out. That launcher stub (small,
|
||||
* unsigned, doing nothing but verify-and-execute) is exactly the shape
|
||||
* Defender's ML scores as a dropper, and on x64 no build variant escaped
|
||||
* it. Handing the swap to a script that runs while we are NOT running
|
||||
* removes the constraint and the stub together: one binary per platform,
|
||||
* matching Linux and macOS.
|
||||
* Windows forced the split first: a running .exe cannot replace itself, so
|
||||
* an in-process updater needed a second resident binary to swap the first
|
||||
* one out, and that launcher stub was exactly the shape Defender's ML
|
||||
* scores as a dropper.
|
||||
*
|
||||
* install.ps1 ships beside the binary and is idempotent, so re-running it
|
||||
* IS the update. Print the exact command instead of feigning self-update. */
|
||||
bool update_seam_portable = false;
|
||||
#ifdef CBM_CLI_ENABLE_TEST_API
|
||||
update_seam_portable = g_cli_activation_test_ops_set;
|
||||
if (update_seam_portable) {
|
||||
(void)fprintf(stderr, "*** cbm test seam: portable update flow engaged; the Windows "
|
||||
"script-update handoff is bypassed (test builds only) ***\n");
|
||||
}
|
||||
* The rest followed for the same reason rather than a different one. An
|
||||
* in-process updater is, structurally, a downloader: it fetches a remote
|
||||
* archive, extracts it, marks the result executable and runs it. That is
|
||||
* the behaviour Microsoft's Wacatac family describes almost verbatim, and
|
||||
* carrying it in the product binary put download/extract/chmod/exec in
|
||||
* every shipped artifact for a command most users run a handful of times.
|
||||
*
|
||||
* The install script already does all of it, is idempotent -- so re-running
|
||||
* it IS the update -- and runs while cbm is NOT running. Print the exact
|
||||
* command instead of feigning self-update. */
|
||||
#ifndef CBM_CLI_ENABLE_TEST_API
|
||||
/* A release build has nothing to do but hand off. The flags are still
|
||||
* parsed and validated above, so `update --dry-run` and friends keep
|
||||
* rejecting typos instead of silently accepting them. */
|
||||
(void)dry_run;
|
||||
(void)force;
|
||||
(void)variant_flag;
|
||||
#endif
|
||||
if (!update_seam_portable) {
|
||||
#ifdef CBM_CLI_ENABLE_TEST_API
|
||||
if (g_cli_activation_test_ops_set) {
|
||||
(void)fprintf(stderr, "*** cbm test seam: portable update flow engaged; the "
|
||||
"script-update handoff is bypassed (test builds only) ***\n");
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
char self_dir[CLI_BUF_1K] = {0};
|
||||
bool have_dir = false;
|
||||
#ifdef _WIN32
|
||||
/* Native separators on purpose: this path is printed for the user to
|
||||
* paste into PowerShell verbatim. */
|
||||
char self_exe[CLI_BUF_1K] = {0};
|
||||
DWORD self_len = GetModuleFileNameA(NULL, self_exe, (DWORD)sizeof(self_exe));
|
||||
DWORD self_len = GetModuleFileNameA(NULL, self_dir, (DWORD)sizeof(self_dir));
|
||||
char *last_sep =
|
||||
(self_len > 0 && (size_t)self_len < sizeof(self_exe)) ? strrchr(self_exe, '\\') : NULL;
|
||||
(self_len > 0 && (size_t)self_len < sizeof(self_dir)) ? strrchr(self_dir, '\\') : NULL;
|
||||
#else
|
||||
char *last_sep = cbm_detect_self_path(self_dir, sizeof(self_dir), cbm_get_home_dir())
|
||||
? strrchr(self_dir, '/')
|
||||
: NULL;
|
||||
#endif
|
||||
if (last_sep) {
|
||||
*last_sep = '\0';
|
||||
have_dir = true;
|
||||
}
|
||||
printf("codebase-memory-mcp update (current: %s)\n\n", CBM_VERSION);
|
||||
printf("On Windows the update runs from install.ps1: a running executable\n"
|
||||
"cannot replace itself. Close any running sessions, then run\n\n");
|
||||
if (last_sep) {
|
||||
printf(" powershell -ExecutionPolicy Bypass -File \"%s\\install.ps1\"\n\n", self_exe);
|
||||
#ifdef _WIN32
|
||||
printf("The update runs from install.ps1, not from this process. Close any\n"
|
||||
"running sessions, then run\n\n");
|
||||
if (have_dir) {
|
||||
printf(" powershell -ExecutionPolicy Bypass -File \"%s\\install.ps1\"\n\n", self_dir);
|
||||
} else {
|
||||
printf(" powershell -ExecutionPolicy Bypass -File install.ps1\n\n");
|
||||
}
|
||||
printf("It downloads the latest release, verifies its checksum, and replaces\n"
|
||||
"this binary in place. If PowerShell refuses to run the script because\n"
|
||||
"it came from the internet, Unblock-File it first.\n");
|
||||
#else
|
||||
printf("The update runs from install.sh, not from this process. Run\n\n");
|
||||
if (have_dir) {
|
||||
printf(" bash \"%s/install.sh\"\n\n", self_dir);
|
||||
} else {
|
||||
printf(" curl -fsSL https://raw.githubusercontent.com/DeusData/"
|
||||
"codebase-memory-mcp/main/install.sh | bash\n\n");
|
||||
}
|
||||
printf("It downloads the latest release, verifies its checksum, and replaces\n"
|
||||
"this binary in place. install.sh is idempotent, so re-running it IS\n"
|
||||
"the update; pass --ui for the UI build.\n");
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Everything below is the in-process updater and is excluded from release
|
||||
* builds entirely -- that exclusion, not dead-code elimination, is what
|
||||
* keeps download/extract/chmod/exec and the release URLs out of the
|
||||
* shipped artifact. */
|
||||
#ifdef CBM_CLI_ENABLE_TEST_API
|
||||
const char *home = cbm_get_home_dir();
|
||||
if (!home) {
|
||||
(void)fprintf(stderr, "error: HOME not set (use USERPROFILE on Windows)\n");
|
||||
@@ -11308,6 +11363,7 @@ int cbm_cmd_update(int argc, char **argv) {
|
||||
"properly take this into account.\n");
|
||||
(void)variant;
|
||||
return 0;
|
||||
#endif /* CBM_CLI_ENABLE_TEST_API */
|
||||
}
|
||||
|
||||
/* ── CLI tool arguments (flags / --args-file / --help) ────────────── */
|
||||
|
||||
Reference in New Issue
Block a user