Files
codebase-memory-mcp/pkg/npm/bin.js
T
Martin Vogel a54ea95719 fix(windows): ship one binary — remove the launcher stub flagged as a dropper
Windows shipped a PAIR: a small permanent launcher (codebase-memory-mcp.exe)
plus the real product binary (codebase-memory-mcp.payload.exe). The launcher
existed for exactly one reason — a running .exe cannot replace its own image
on Windows, so an in-process self-update needs a second resident binary to do
the swap.

That stub is statically indistinguishable from a dropper: a small, unsigned,
zero-prevalence PE whose whole job is verify-and-execute another binary.
Defender's ML scored it Trojan:Win32/Wacatac.B!ml and blocked the v0.9.1-rc.1
release at the VirusTotal gate. It is not fixable in our code on x64 —
bcrypt-free, stripped, VERSIONINFO'd, minimal-resource and even
resource-FREE builds on CI's own MSYS2 CLANG64 toolchain were all flagged,
while the product binary scans clean on every platform.

So remove the stub and move self-update OUT of the process into install.ps1,
which runs while cbm is NOT running: Windows' image lock only blocks a
process from replacing ITSELF.  now prints the exact PowerShell
command (with the Unblock-File hint for Mark-of-the-Web); install.ps1 is
idempotent, so re-running it IS the update — it stops the daemon, renames the
running binary aside (the one mutation Windows permits on a running image),
publishes the new one, and sweeps retired copies.

Windows now matches Linux and macOS: ONE binary per platform.

  * packaging, install.ps1, npm and PyPI wrappers all carry a single binary
  * the launcher/payload ABI contract and ~2500 lines of stub state machinery
    are deleted
  * every daemon start, CLI call and hook fire loses a process spawn, a named
    pipe handshake and an stdio relay
  * test_windows_bundle_contract.sh is rewritten as an INVERTED contract: it
    now asserts no shipped surface can reintroduce a launcher/payload pair,
    and that install.ps1 retires the running binary before publishing

Verified: VirusTotal 0/67 on the packaged binary and 0/58 on install.ps1 (no
certificate involved); macOS and Linux full suites green; Windows guards all
green including the new update-handoff contract; npm 10/10; PyPI 3/3.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-28 17:03:01 +02:00

82 lines
2.7 KiB
JavaScript

#!/usr/bin/env node
'use strict';
// CLI shim: resolves the downloaded binary and replaces the current process with it.
// If the binary is missing (e.g. --ignore-scripts), attempts a one-time download.
const path = require('path');
const fs = require('fs');
const { spawnSync } = require('child_process');
const isWindows = process.platform === 'win32';
// npm is a portable one-shot wrapper. Every platform ships exactly ONE binary,
// so the cached file is the executed file — there is no launcher pair to keep
// in sync and no adjacent payload to validate.
const binName = isWindows ? 'codebase-memory-mcp.exe' : 'codebase-memory-mcp';
const binPath = path.join(__dirname, 'bin', binName);
const executionPath = binPath;
const cacheReady = () => {
if (!fs.existsSync(binPath)) {
return false;
}
if (!isWindows) return true;
// A cached Windows binary that cannot report its own version is not usable.
const probe = spawnSync(binPath, ['--version'], {
stdio: 'ignore',
timeout: 15_000,
windowsHide: true,
});
return !probe.error && probe.status === 0;
};
if (!cacheReady()) {
// Binary missing — try running the install script (handles --ignore-scripts case)
process.stderr.write('codebase-memory-mcp: binary not found, downloading...\n');
const installResult = spawnSync(process.execPath, [path.join(__dirname, 'install.js')], {
stdio: 'inherit',
});
if (installResult.status !== 0 || !cacheReady()) {
process.stderr.write(
'codebase-memory-mcp: download failed.\n' +
'Try reinstalling: npm install -g codebase-memory-mcp\n'
);
process.exit(1);
}
}
function portableMutationAction(args) {
for (const argument of args) {
if (['cli', 'hook-augment', 'config', 'install', '--help', '-h', '--version'].includes(argument)) {
return null;
}
if (argument === 'update' || argument === 'uninstall') return argument;
}
return null;
}
const result = spawnSync(executionPath, process.argv.slice(2), {
stdio: 'inherit',
windowsHide: false,
});
if (result.error) {
process.stderr.write(`codebase-memory-mcp: ${result.error.message}\n`);
process.exit(1);
}
const mutation = isWindows && result.status !== 0
? portableMutationAction(process.argv.slice(2))
: null;
if (mutation) {
const packageCommand = mutation === 'update'
? 'npm install codebase-memory-mcp@latest'
: 'npm uninstall codebase-memory-mcp';
process.stderr.write(
`This npm Windows copy is portable. Use "${packageCommand}" for package ` +
'maintenance (add -g for a global install), or run ' +
'"codebase-memory-mcp install --yes" once for a managed install with its ' +
'own install.ps1 for updates.\n',
);
}
process.exit(result.status ?? 0);