Skip to content

refactor(cli): move self-update to the install scripts; strip Mach-O fully - #1332

Merged
DeusData merged 2 commits into
mainfrom
fix/no-inprocess-update
Jul 29, 2026
Merged

refactor(cli): move self-update to the install scripts; strip Mach-O fully#1332
DeusData merged 2 commits into
mainfrom
fix/no-inprocess-update

Conversation

@DeusData

Copy link
Copy Markdown
Owner

Two changes, both grounded in measurement rather than inference.

1. Strip Mach-O fully

The first strip landed as strip -x on Mach-O, out of caution that a full strip
could leave an image dyld will not load. Wrong for this binary, and it cost a
release cycle.

-x retains external symbols — 4058 of them — so the macOS artifacts kept
the symbol table the ELF legs had just shed. Run 30414312364 then flagged
exactly those two artifacts while every Linux and Windows binary came back
clean. That read as the detection "moving to macOS"; it had simply stayed on the
unstripped set.

darwin-arm64 symbols VirusTotal
strip -x (shipped) 4058 1 malicious / 61
--strip-all 373 0 / 61 CLEAN

Packaged binary still passes codesign --verify --strict (re-signed after
stripping) and still runs. Symbol removal has now cleared the detection on
three independent artifacts across two platforms.

2. Self-update out of the product, everywhere

Windows already handed updating to install.ps1 because a running .exe cannot
replace itself. The other platforms now do the same for a better reason: an
in-process updater is structurally a downloader — fetch, verify, extract,
chmod +x, exec — and that composite shipped in every artifact, with the
release URLs it fetched, to serve a command most users run a handful of times.

update now prints the exact command and exits 0. Both install scripts ship
inside their archive, so the printed path resolves next to the binary. Flags are
still parsed, so update --dry-run --ui keeps rejecting typos.

The updater 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/arch and the
cli_download_* helpers compile only under CBM_CLI_ENABLE_TEST_API. Verified
against the release binary: releases/latest/download is gone.

Also removes the MCP background update check, which ran on the first tool call:

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 agent session,
to report that a newer version exists. It shelled out and depended on curl
being present. The install scripts report versions.

Smoke

Phases 6c and 14 collapse to one platform-neutral contract selected by
UPDATE_SCRIPT. Two things worth calling out:

  • 14a now compares the driver against itself before and after. Comparing
    against $BINARY passed on Windows only because Windows has no signing step —
    the POSIX fixture ad-hoc re-signs its copy, so the two 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 / Ubuntu 20.04, so the
    installer must fetch the static -portable build. That constraint moved from
    the binary into install.sh; the smoke now guards it where the behaviour lives.

Verification

Check Result
cli mcp daemon_application activation_transaction 500 passed, 2 skipped
Full macOS smoke (no-self-update binary) ALL PASSED
10 shell contracts pass
lint-ci (cppcheck + clang-format) clean
macOS packaged binary 368 symbols, codesign --verify --strict ok, runs

Note on the cppcheck fix: the always-true !update_seam_portable was resolved by
restructuring the handoff, not by suppressing the warning.

DeusData added 2 commits July 29, 2026 10:22
The first strip landed as `strip -x` on Mach-O, out of caution that a full strip
could leave an image dyld will not load. That caution was wrong for this binary,
and it cost a release cycle.

`-x` retains external symbols -- 4058 of them on darwin-arm64 -- so the macOS
artifacts kept the very symbol table the ELF legs had just shed. Run
30414312364 then flagged exactly those two artifacts (darwin-arm64,
ui-darwin-amd64) while every Linux and Windows binary came back clean, which
read as the detection "moving to macOS" when it had simply stayed on the
unstripped set.

Measured on the flagged darwin-arm64 artifact:

  strip -x        4058 symbols   VirusTotal 1 malicious / 61
  --strip-all      373 symbols   VirusTotal 0 malicious / 61  CLEAN

The packaged binary still verifies (`codesign --verify --strict` passes, having
been re-signed after stripping) and still runs. Symbol-table removal has now
cleared the detection on three independent artifacts across two platforms:
linux-amd64 twice, and darwin-arm64 here.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
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>
@DeusData
DeusData merged commit 3e2f9a0 into main Jul 29, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant