Merge pull request #1316 from DeusData/fix/launcher-av-false-positive
fix(windows): ship one binary — remove the launcher stub flagged as a dropper
This commit is contained in:
@@ -206,18 +206,18 @@ jobs:
|
||||
mkdir -p "$ARTIFACT_DIR"
|
||||
unzip -o "codebase-memory-mcp${SUFFIX}-windows-${ARCH}.zip" -d "$ARTIFACT_DIR"
|
||||
test -s "$ARTIFACT_DIR/codebase-memory-mcp.exe"
|
||||
test -s "$ARTIFACT_DIR/codebase-memory-mcp.payload.exe"
|
||||
# Pre-flight both images from a profile-rooted directory: the shared
|
||||
# runner workspace is deliberately not a valid launcher bundle root.
|
||||
# ONE binary per platform: a payload sibling would mean the
|
||||
# AV-flagged launcher stub came back.
|
||||
test ! -e "$ARTIFACT_DIR/codebase-memory-mcp.payload.exe"
|
||||
# Pre-flight the image from a profile-rooted directory: the shared
|
||||
# runner workspace is deliberately not a valid install root.
|
||||
PROFILE_ROOT="$(cygpath -u "$USERPROFILE")"
|
||||
LAUNCH_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-release-version.XXXXXX")"
|
||||
# Best-effort cleanup with one retry: Windows holds the just-exited
|
||||
# image's file busy for ~100ms (section teardown / first-touch AV),
|
||||
# and a temp dir on an ephemeral runner must never fail the job.
|
||||
trap 'rm -rf "$LAUNCH_DIR" 2>/dev/null || { sleep 2; rm -rf "$LAUNCH_DIR" 2>/dev/null || true; }' EXIT
|
||||
cp "$ARTIFACT_DIR/codebase-memory-mcp.exe" \
|
||||
"$ARTIFACT_DIR/codebase-memory-mcp.payload.exe" "$LAUNCH_DIR/"
|
||||
"$LAUNCH_DIR/codebase-memory-mcp.payload.exe" --version
|
||||
cp "$ARTIFACT_DIR/codebase-memory-mcp.exe" "$LAUNCH_DIR/"
|
||||
"$LAUNCH_DIR/codebase-memory-mcp.exe" --version
|
||||
|
||||
# ONE harness for every venue: the same script win.sh smoke-install and
|
||||
@@ -241,13 +241,11 @@ jobs:
|
||||
run: |
|
||||
ARTIFACT_DIR="$(cygpath -u "$RUNNER_TEMP")/cbm-artifact"
|
||||
scripts/security-strings.sh "$ARTIFACT_DIR/codebase-memory-mcp.exe"
|
||||
scripts/security-strings.sh "$ARTIFACT_DIR/codebase-memory-mcp.payload.exe"
|
||||
PROFILE_ROOT="$(cygpath -u "$USERPROFILE")"
|
||||
SECURITY_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-release-security.XXXXXX")"
|
||||
# Best-effort cleanup with one retry (see the version preflight).
|
||||
trap 'rm -rf "$SECURITY_DIR" 2>/dev/null || { sleep 2; rm -rf "$SECURITY_DIR" 2>/dev/null || true; }' EXIT
|
||||
cp "$ARTIFACT_DIR/codebase-memory-mcp.exe" \
|
||||
"$ARTIFACT_DIR/codebase-memory-mcp.payload.exe" "$SECURITY_DIR/"
|
||||
cp "$ARTIFACT_DIR/codebase-memory-mcp.exe" "$SECURITY_DIR/"
|
||||
TMPDIR="$SECURITY_DIR" \
|
||||
scripts/security-install.sh "$SECURITY_DIR/codebase-memory-mcp.exe"
|
||||
|
||||
@@ -256,7 +254,7 @@ jobs:
|
||||
run: |
|
||||
& "C:\Program Files\Windows Defender\MpCmdRun.exe" -SignatureUpdate 2>$null
|
||||
$artifactDir = Join-Path $env:RUNNER_TEMP "cbm-artifact"
|
||||
foreach ($binary in @("codebase-memory-mcp.exe", "codebase-memory-mcp.payload.exe")) {
|
||||
foreach ($binary in @("codebase-memory-mcp.exe")) {
|
||||
$result = & "C:\Program Files\Windows Defender\MpCmdRun.exe" -Scan -ScanType 3 -File (Join-Path $artifactDir $binary) -DisableRemediation
|
||||
$code = $LASTEXITCODE
|
||||
Write-Host $result
|
||||
|
||||
+6
-35
@@ -311,12 +311,7 @@ GIT_SRCS = src/git/git_context.c
|
||||
CLI_SRCS = src/cli/cli.c src/cli/progress_sink.c src/cli/hook_augment.c \
|
||||
src/cli/agent_clients.c src/cli/agent_profiles.c \
|
||||
src/cli/config_json_like.c src/cli/config_toml_edit.c src/cli/config_yaml_edit.c \
|
||||
src/cli/config_text_edit.c src/cli/activation_transaction.c \
|
||||
src/cli/windows_launcher_state.c
|
||||
|
||||
# Tiny permanent Windows launcher. It deliberately links only the stable
|
||||
# launcher-state module and Windows SDK libraries, never the parser/indexer.
|
||||
WINDOWS_LAUNCHER_SRC = src/launcher/windows_launcher.c
|
||||
src/cli/config_text_edit.c src/cli/activation_transaction.c
|
||||
|
||||
# UI module (graph visualization)
|
||||
UI_SRCS = \
|
||||
@@ -490,8 +485,7 @@ TEST_TRACES_SRCS = tests/test_traces.c
|
||||
TEST_CLI_SRCS = tests/test_cli.c tests/test_agent_clients.c tests/test_agent_profiles.c \
|
||||
tests/test_config_json_like.c \
|
||||
tests/test_config_toml_edit.c tests/test_config_yaml_edit.c tests/test_config_text_edit.c \
|
||||
tests/test_activation_transaction.c \
|
||||
tests/test_windows_launcher_state.c
|
||||
tests/test_activation_transaction.c
|
||||
|
||||
TEST_MEM_SRCS = tests/test_mem.c
|
||||
|
||||
@@ -601,7 +595,7 @@ PP_OBJ_TSAN = $(BUILD_DIR)/tsan_preprocessor.o
|
||||
|
||||
# ── Targets ──────────────────────────────────────────────────────
|
||||
|
||||
.PHONY: test test-par test-repro test-foundation test-tsan test-daemon-smoke cbm cbm-launcher cbm-with-ui frontend embed clean-c lint lint-tidy lint-cppcheck lint-format security
|
||||
.PHONY: test test-par test-repro test-foundation test-tsan test-daemon-smoke cbm cbm-with-ui frontend embed clean-c lint lint-tidy lint-cppcheck lint-format security
|
||||
|
||||
$(BUILD_DIR):
|
||||
mkdir -p $(BUILD_DIR)
|
||||
@@ -841,26 +835,6 @@ OBJS_VENDORED_PROD = $(MIMALLOC_OBJ_PROD) $(SQLITE3_OBJ_PROD) $(TRE_OBJ_PROD) $(
|
||||
|
||||
MAIN_SRC = src/main.c
|
||||
|
||||
WINDOWS_LAUNCHER_TARGET :=
|
||||
ifeq ($(IS_MINGW),yes)
|
||||
WINDOWS_LAUNCHER_TARGET := $(BUILD_DIR)/codebase-memory-mcp-launcher.exe
|
||||
endif
|
||||
|
||||
# Keep the explicit .exe target even on non-Windows hosts: packaging contracts
|
||||
# and native-Windows invocations share this single artifact name. A non-Windows
|
||||
# direct invocation builds a diagnostic stub and is not part of `cbm`.
|
||||
$(BUILD_DIR)/codebase-memory-mcp-launcher.exe: $(WINDOWS_LAUNCHER_SRC) src/cli/windows_launcher_state.c src/cli/windows_launcher_state.h | $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS_PROD) -o $@ \
|
||||
$(WINDOWS_LAUNCHER_SRC) src/cli/windows_launcher_state.c \
|
||||
$(if $(filter yes,$(IS_MINGW)),-municode -lbcrypt $(WIN32_LIBS),)
|
||||
|
||||
# Native-Windows compatibility fixture: its launcher ABI intentionally does
|
||||
# not fit the production payload's default [1,1] range. It is never packaged.
|
||||
$(BUILD_DIR)/codebase-memory-mcp-launcher-abi2.exe: $(WINDOWS_LAUNCHER_SRC) src/cli/windows_launcher_state.c src/cli/windows_launcher_state.h | $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS_PROD) -DCBM_WINDOWS_LAUNCHER_ABI_CURRENT=2 -o $@ \
|
||||
$(WINDOWS_LAUNCHER_SRC) src/cli/windows_launcher_state.c \
|
||||
$(if $(filter yes,$(IS_MINGW)),-municode -lbcrypt $(WIN32_LIBS),)
|
||||
|
||||
$(BUILD_DIR)/codebase-memory-mcp: $(MAIN_SRC) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_PROD) | $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS_PROD) -o $@ \
|
||||
$(MAIN_SRC) $(PROD_SRCS) \
|
||||
@@ -868,12 +842,9 @@ $(BUILD_DIR)/codebase-memory-mcp: $(MAIN_SRC) $(PROD_SRCS) $(EXTRACTION_SRCS) $(
|
||||
$(OBJS_VENDORED_PROD) \
|
||||
$(LDFLAGS)
|
||||
|
||||
cbm: $(BUILD_DIR)/codebase-memory-mcp $(WINDOWS_LAUNCHER_TARGET)
|
||||
cbm: $(BUILD_DIR)/codebase-memory-mcp
|
||||
@echo "Built: $(BUILD_DIR)/codebase-memory-mcp"
|
||||
|
||||
cbm-launcher: $(BUILD_DIR)/codebase-memory-mcp-launcher.exe
|
||||
@echo "Built: $(BUILD_DIR)/codebase-memory-mcp-launcher.exe"
|
||||
|
||||
# ── Build with embedded UI (requires Node.js) ───────────────────
|
||||
|
||||
# Swap embedded_stub.c for the generated embedded_assets.c
|
||||
@@ -889,7 +860,7 @@ frontend:
|
||||
embed: frontend
|
||||
scripts/embed-frontend.sh graph-ui/dist $(BUILD_DIR)/embedded
|
||||
|
||||
cbm-with-ui: embed $(OBJS_VENDORED_PROD) $(WINDOWS_LAUNCHER_TARGET)
|
||||
cbm-with-ui: embed $(OBJS_VENDORED_PROD)
|
||||
$(CC) $(CFLAGS_PROD) -o $(BUILD_DIR)/codebase-memory-mcp \
|
||||
$(MAIN_SRC) $(PROD_SRCS_WITH_ASSETS) \
|
||||
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
|
||||
@@ -919,7 +890,7 @@ SYSROOT_FLAG = $(if $(SYSROOT),-isysroot $(SYSROOT),)
|
||||
LINT_SRCS = $(FOUNDATION_SRCS) $(STORE_SRCS) $(CYPHER_SRCS) $(MCP_SRCS) $(DAEMON_SRCS) \
|
||||
$(DISCOVER_SRCS) $(GRAPH_BUFFER_SRCS) $(PIPELINE_SRCS) $(SIMHASH_SRCS) $(SEMANTIC_SRCS) \
|
||||
$(TRACES_SRCS) $(WATCHER_SRCS) $(CLI_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) \
|
||||
$(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(MAIN_SRC) $(WINDOWS_LAUNCHER_SRC)
|
||||
$(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(MAIN_SRC)
|
||||
LINT_HDRS = $(wildcard src/**/*.h src/*.h $(CBM_DIR)/*.h)
|
||||
LINT_TEST_SRCS = $(ALL_TEST_SRCS)
|
||||
|
||||
|
||||
+37
-20
@@ -14,10 +14,8 @@ Add-Type -AssemblyName System.Net.Http
|
||||
$Repo = "DeusData/codebase-memory-mcp"
|
||||
$InstallDir = "$env:LOCALAPPDATA\Programs\codebase-memory-mcp"
|
||||
$BinName = "codebase-memory-mcp.exe"
|
||||
$PayloadName = "codebase-memory-mcp.payload.exe"
|
||||
$WindowsArchiveNames = @(
|
||||
$BinName,
|
||||
$PayloadName,
|
||||
"LICENSE",
|
||||
"install.ps1",
|
||||
"THIRD_PARTY_NOTICES.md"
|
||||
@@ -237,32 +235,28 @@ try {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Extract the complete, validated bundle. The portable launcher owns the
|
||||
# managed install transaction and contains its adjacent payload.
|
||||
# Extract the validated bundle. Windows ships ONE binary, exactly like Linux
|
||||
# and macOS; this script is what replaces it, because a running executable
|
||||
# cannot replace itself on Windows. Re-running this script IS the update.
|
||||
Write-Host "Extracting..."
|
||||
Expand-Archive -Path "$TmpDir\$Archive" -DestinationPath $TmpDir -Force
|
||||
|
||||
$DownloadedLauncher = Join-Path $TmpDir $BinName
|
||||
$DownloadedPayload = Join-Path $TmpDir $PayloadName
|
||||
if (-not (Test-Path -LiteralPath $DownloadedLauncher -PathType Leaf) -or
|
||||
-not (Test-Path -LiteralPath $DownloadedPayload -PathType Leaf)) {
|
||||
Write-Host "error: launcher or payload not found after extraction" -ForegroundColor Red
|
||||
$DownloadedBinary = Join-Path $TmpDir $BinName
|
||||
if (-not (Test-Path -LiteralPath $DownloadedBinary -PathType Leaf)) {
|
||||
Write-Host "error: $BinName not found after extraction" -ForegroundColor Red
|
||||
Remove-Item -Recurse -Force $TmpDir
|
||||
exit 1
|
||||
}
|
||||
$launcherItem = Get-Item -LiteralPath $DownloadedLauncher
|
||||
$payloadItem = Get-Item -LiteralPath $DownloadedPayload
|
||||
if (($launcherItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -or
|
||||
($payloadItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
|
||||
$binaryItem = Get-Item -LiteralPath $DownloadedBinary
|
||||
if ($binaryItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) {
|
||||
Write-Host "error: refusing reparse-point executable in release archive" -ForegroundColor Red
|
||||
Remove-Item -Recurse -Force $TmpDir
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Verify the complete launcher/payload path before it asks all coordinated CBM
|
||||
# processes to stop.
|
||||
# Prove the downloaded binary runs before touching an existing installation.
|
||||
try {
|
||||
$candidateVersion = & $DownloadedLauncher --version 2>&1
|
||||
$candidateVersion = & $DownloadedBinary --version 2>&1
|
||||
if ($LASTEXITCODE -ne 0) { throw "candidate exited with $LASTEXITCODE" }
|
||||
Write-Host "Verified candidate: $candidateVersion"
|
||||
} catch {
|
||||
@@ -272,13 +266,36 @@ try {
|
||||
}
|
||||
|
||||
$Dest = Join-Path $InstallDir $BinName
|
||||
|
||||
# Retire the running installation before replacing it. Windows keeps an image
|
||||
# lock on a running .exe: the file cannot be overwritten, but it CAN be renamed
|
||||
# out of the way, which is what makes an in-place update possible from here.
|
||||
if (Test-Path -LiteralPath $Dest -PathType Leaf) {
|
||||
try { & $Dest daemon stop 2>&1 | Out-Null } catch { }
|
||||
$retired = "$Dest.retired-$(Get-Date -Format yyyyMMddHHmmss)"
|
||||
$renamed = $false
|
||||
foreach ($attempt in 1..10) {
|
||||
try { Move-Item -LiteralPath $Dest -Destination $retired -Force -ErrorAction Stop; $renamed = $true; break }
|
||||
catch { Start-Sleep -Milliseconds 500 }
|
||||
}
|
||||
if (-not $renamed) {
|
||||
Write-Host "error: could not retire the existing $BinName — close all running" -ForegroundColor Red
|
||||
Write-Host " codebase-memory-mcp sessions and coding agents, then re-run." -ForegroundColor Red
|
||||
Remove-Item -Recurse -Force $TmpDir -ErrorAction SilentlyContinue
|
||||
exit 1
|
||||
}
|
||||
# A retired image stays locked until its last process exits; delete it when
|
||||
# we can, and leave it for the next run when we cannot. Never fail here.
|
||||
Remove-Item -LiteralPath $retired -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
Get-ChildItem -LiteralPath $InstallDir -Filter "$BinName.retired-*" -ErrorAction SilentlyContinue |
|
||||
ForEach-Object { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue }
|
||||
|
||||
$InstallArgs = @("install", "-y", "--force", "--dir=$InstallDir")
|
||||
if ($SkipConfig) { $InstallArgs += "--skip-config" }
|
||||
# PowerShell's call operator waits for the launcher, which in turn contains the
|
||||
# install payload in its native job and relays the exact exit status.
|
||||
& $DownloadedLauncher @InstallArgs
|
||||
& $DownloadedBinary @InstallArgs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "error: coordinated activation failed (exit code $LASTEXITCODE)" -ForegroundColor Red
|
||||
Write-Host "error: installation failed (exit code $LASTEXITCODE)" -ForegroundColor Red
|
||||
Remove-Item -Recurse -Force $TmpDir -ErrorAction SilentlyContinue
|
||||
exit 1
|
||||
}
|
||||
|
||||
+10
-14
@@ -8,23 +8,19 @@ const fs = require('fs');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
const windowsLauncherName = 'codebase-memory-mcp.exe';
|
||||
// npm is a portable one-shot wrapper. On Windows the public .exe in the
|
||||
// release archive is the permanent launcher, so privately validate the
|
||||
// release pair, cache both files, and enter the native process through that
|
||||
// launcher. The adjacent payload remains the immutable cache readiness signal.
|
||||
const binName = isWindows ? 'codebase-memory-mcp.payload.exe' : 'codebase-memory-mcp';
|
||||
// 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 launcherPath = path.join(__dirname, 'bin', windowsLauncherName);
|
||||
const executionPath = isWindows ? launcherPath : binPath;
|
||||
const executionPath = binPath;
|
||||
const cacheReady = () => {
|
||||
if (!fs.existsSync(binPath) || (isWindows && !fs.existsSync(launcherPath))) {
|
||||
if (!fs.existsSync(binPath)) {
|
||||
return false;
|
||||
}
|
||||
if (!isWindows) return true;
|
||||
// The launcher resolving --version through its adjacent payload validates
|
||||
// both halves of the immutable Windows cache pair.
|
||||
const probe = spawnSync(launcherPath, ['--version'], {
|
||||
// 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,
|
||||
@@ -77,8 +73,8 @@ if (mutation) {
|
||||
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 to create ' +
|
||||
'a managed launcher with coordinated self-update/uninstall.\n',
|
||||
'"codebase-memory-mcp install --yes" once for a managed install with its ' +
|
||||
'own install.ps1 for updates.\n',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+46
-54
@@ -18,8 +18,7 @@ const MAX_REDIRECTS = 5;
|
||||
const DOWNLOAD_HOP_TIMEOUT_MS = 120_000;
|
||||
const CANDIDATE_TIMEOUT_MS = 15_000;
|
||||
const MAX_CHECKSUM_MANIFEST_BYTES = 1024 * 1024;
|
||||
const WINDOWS_LAUNCHER_NAME = 'codebase-memory-mcp.exe';
|
||||
const WINDOWS_PAYLOAD_NAME = 'codebase-memory-mcp.payload.exe';
|
||||
const WINDOWS_BINARY_NAME = 'codebase-memory-mcp.exe';
|
||||
const UNIX_ARCHIVE_NAMES = [
|
||||
'codebase-memory-mcp',
|
||||
'LICENSE',
|
||||
@@ -27,8 +26,7 @@ const UNIX_ARCHIVE_NAMES = [
|
||||
'THIRD_PARTY_NOTICES.md',
|
||||
];
|
||||
const WINDOWS_ARCHIVE_NAMES = [
|
||||
WINDOWS_LAUNCHER_NAME,
|
||||
WINDOWS_PAYLOAD_NAME,
|
||||
WINDOWS_BINARY_NAME,
|
||||
'LICENSE',
|
||||
'install.ps1',
|
||||
'THIRD_PARTY_NOTICES.md',
|
||||
@@ -258,7 +256,7 @@ function processIsAlive(pid) {
|
||||
}
|
||||
}
|
||||
|
||||
function readPairLockOwner(lockPath) {
|
||||
function readBinaryLockOwner(lockPath) {
|
||||
try {
|
||||
const owner = JSON.parse(
|
||||
fs.readFileSync(path.join(lockPath, 'owner.json'), 'utf8'),
|
||||
@@ -273,9 +271,9 @@ function readPairLockOwner(lockPath) {
|
||||
}
|
||||
}
|
||||
|
||||
function tryReclaimPairLock(lockPath, contenderToken) {
|
||||
function tryReclaimBinaryLock(lockPath, contenderToken) {
|
||||
let reclaim = false;
|
||||
const owner = readPairLockOwner(lockPath);
|
||||
const owner = readBinaryLockOwner(lockPath);
|
||||
if (owner) {
|
||||
reclaim = !processIsAlive(owner.pid);
|
||||
} else {
|
||||
@@ -299,7 +297,7 @@ function tryReclaimPairLock(lockPath, contenderToken) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function acquireWindowsPairLock(destDir) {
|
||||
function acquireWindowsBinaryLock(destDir) {
|
||||
const lockPath = path.join(destDir, WINDOWS_PAIR_LOCK_NAME);
|
||||
const token = crypto.randomBytes(16).toString('hex');
|
||||
const deadline = Date.now() + WINDOWS_PAIR_LOCK_WAIT_MS;
|
||||
@@ -319,7 +317,7 @@ function acquireWindowsPairLock(destDir) {
|
||||
return { lockPath, token };
|
||||
} catch (err) {
|
||||
if (err.code !== 'EEXIST') throw err;
|
||||
if (tryReclaimPairLock(lockPath, token)) continue;
|
||||
if (tryReclaimBinaryLock(lockPath, token)) continue;
|
||||
if (Date.now() >= deadline) {
|
||||
throw new Error('timed out waiting for Windows package-cache publication lock');
|
||||
}
|
||||
@@ -328,8 +326,8 @@ function acquireWindowsPairLock(destDir) {
|
||||
}
|
||||
}
|
||||
|
||||
function releaseWindowsPairLock(lock) {
|
||||
const owner = readPairLockOwner(lock.lockPath);
|
||||
function releaseWindowsBinaryLock(lock) {
|
||||
const owner = readBinaryLockOwner(lock.lockPath);
|
||||
if (!owner || owner.token !== lock.token || owner.pid !== process.pid) {
|
||||
throw new Error('Windows package-cache publication lock ownership changed');
|
||||
}
|
||||
@@ -337,17 +335,14 @@ function releaseWindowsPairLock(lock) {
|
||||
fs.rmdirSync(lock.lockPath);
|
||||
}
|
||||
|
||||
function windowsPairReady(destDir, verifier = verifyCandidate) {
|
||||
const launcher = path.join(destDir, WINDOWS_LAUNCHER_NAME);
|
||||
const payload = path.join(destDir, WINDOWS_PAYLOAD_NAME);
|
||||
function windowsBinaryReady(destDir, verifier = verifyCandidate) {
|
||||
const binary = path.join(destDir, WINDOWS_BINARY_NAME);
|
||||
try {
|
||||
const launcherStatus = fs.lstatSync(launcher);
|
||||
const payloadStatus = fs.lstatSync(payload);
|
||||
if (!launcherStatus.isFile() || launcherStatus.isSymbolicLink() ||
|
||||
!payloadStatus.isFile() || payloadStatus.isSymbolicLink()) {
|
||||
const status = fs.lstatSync(binary);
|
||||
if (!status.isFile() || status.isSymbolicLink()) {
|
||||
return false;
|
||||
}
|
||||
verifier(launcher);
|
||||
verifier(binary);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
@@ -364,8 +359,8 @@ function pathMatchesDigest(candidatePath, expectedDigest) {
|
||||
}
|
||||
}
|
||||
|
||||
function removeOwnedPublishedPair(destDir, publishedDigests) {
|
||||
for (const name of [WINDOWS_PAYLOAD_NAME, WINDOWS_LAUNCHER_NAME]) {
|
||||
function removeOwnedPublishedBinary(destDir, publishedDigests) {
|
||||
for (const name of [WINDOWS_BINARY_NAME]) {
|
||||
const digest = publishedDigests.get(name);
|
||||
const target = path.join(destDir, name);
|
||||
if (digest && pathMatchesDigest(target, digest)) {
|
||||
@@ -374,8 +369,8 @@ function removeOwnedPublishedPair(destDir, publishedDigests) {
|
||||
}
|
||||
}
|
||||
|
||||
function restorePairBackups(destDir, backups) {
|
||||
for (const name of [WINDOWS_LAUNCHER_NAME, WINDOWS_PAYLOAD_NAME]) {
|
||||
function restoreBinaryBackups(destDir, backups) {
|
||||
for (const name of [WINDOWS_BINARY_NAME]) {
|
||||
const backup = backups.get(name);
|
||||
const target = path.join(destDir, name);
|
||||
if (!backup || fs.existsSync(target)) continue;
|
||||
@@ -383,15 +378,15 @@ function restorePairBackups(destDir, backups) {
|
||||
}
|
||||
}
|
||||
|
||||
function installWindowsPairAtomically(sourceDir, destDir, verifier = verifyCandidate) {
|
||||
// Authenticate the complete source pair before it can contend for the cache.
|
||||
verifier(path.join(sourceDir, WINDOWS_LAUNCHER_NAME));
|
||||
const lock = acquireWindowsPairLock(destDir);
|
||||
function installWindowsBinaryAtomically(sourceDir, destDir, verifier = verifyCandidate) {
|
||||
// Authenticate the source binary before it can contend for the cache.
|
||||
verifier(path.join(sourceDir, WINDOWS_BINARY_NAME));
|
||||
const lock = acquireWindowsBinaryLock(destDir);
|
||||
let operationError = null;
|
||||
try {
|
||||
// A contender may have completed while this process waited. Its executable
|
||||
// pair wins and is never moved or deleted.
|
||||
if (windowsPairReady(destDir, verifier)) return;
|
||||
// wins and is never moved or deleted.
|
||||
if (windowsBinaryReady(destDir, verifier)) return;
|
||||
|
||||
const transaction = crypto.randomBytes(16).toString('hex');
|
||||
const stagedPaths = new Map();
|
||||
@@ -399,7 +394,7 @@ function installWindowsPairAtomically(sourceDir, destDir, verifier = verifyCandi
|
||||
const backups = new Map();
|
||||
const publishedDigests = new Map();
|
||||
try {
|
||||
for (const name of [WINDOWS_LAUNCHER_NAME, WINDOWS_PAYLOAD_NAME]) {
|
||||
for (const name of [WINDOWS_BINARY_NAME]) {
|
||||
const staged = path.join(destDir, `.cbm-pair-stage-${transaction}-${name}`);
|
||||
fs.copyFileSync(path.join(sourceDir, name), staged, fs.constants.COPYFILE_EXCL);
|
||||
fs.chmodSync(staged, 0o755);
|
||||
@@ -407,7 +402,7 @@ function installWindowsPairAtomically(sourceDir, destDir, verifier = verifyCandi
|
||||
stagedDigests.set(name, fileSha256(staged));
|
||||
}
|
||||
|
||||
for (const name of [WINDOWS_LAUNCHER_NAME, WINDOWS_PAYLOAD_NAME]) {
|
||||
for (const name of [WINDOWS_BINARY_NAME]) {
|
||||
const target = path.join(destDir, name);
|
||||
const status = fs.lstatSync(target, { throwIfNoEntry: false });
|
||||
if (!status) continue;
|
||||
@@ -419,32 +414,31 @@ function installWindowsPairAtomically(sourceDir, destDir, verifier = verifyCandi
|
||||
backups.set(name, backup);
|
||||
}
|
||||
|
||||
// Payload is the readiness signal and is deliberately published last.
|
||||
for (const name of [WINDOWS_LAUNCHER_NAME, WINDOWS_PAYLOAD_NAME]) {
|
||||
for (const name of [WINDOWS_BINARY_NAME]) {
|
||||
const target = path.join(destDir, name);
|
||||
fs.renameSync(stagedPaths.get(name), target);
|
||||
stagedPaths.delete(name);
|
||||
publishedDigests.set(name, stagedDigests.get(name));
|
||||
}
|
||||
if (!windowsPairReady(destDir, verifier)) {
|
||||
throw new Error('published Windows package-cache pair failed verification');
|
||||
if (!windowsBinaryReady(destDir, verifier)) {
|
||||
throw new Error('published Windows package-cache binary failed verification');
|
||||
}
|
||||
for (const backup of backups.values()) {
|
||||
try { fs.unlinkSync(backup); } catch (_) { /* valid pair is already committed */ }
|
||||
try { fs.unlinkSync(backup); } catch (_) { /* valid binary is already committed */ }
|
||||
}
|
||||
return;
|
||||
} catch (err) {
|
||||
// If a non-cooperating contender nevertheless published a valid pair,
|
||||
// If a non-cooperating contender nevertheless published a valid binary,
|
||||
// preserve that winner. Otherwise remove only our exact staged bytes and
|
||||
// restore the prior files when their target names are still absent.
|
||||
if (windowsPairReady(destDir, verifier)) {
|
||||
if (windowsBinaryReady(destDir, verifier)) {
|
||||
for (const backup of backups.values()) {
|
||||
try { fs.unlinkSync(backup); } catch (_) { /* winner remains authoritative */ }
|
||||
}
|
||||
return;
|
||||
}
|
||||
removeOwnedPublishedPair(destDir, publishedDigests);
|
||||
restorePairBackups(destDir, backups);
|
||||
removeOwnedPublishedBinary(destDir, publishedDigests);
|
||||
restoreBinaryBackups(destDir, backups);
|
||||
throw err;
|
||||
} finally {
|
||||
for (const staged of stagedPaths.values()) {
|
||||
@@ -456,7 +450,7 @@ function installWindowsPairAtomically(sourceDir, destDir, verifier = verifyCandi
|
||||
throw err;
|
||||
} finally {
|
||||
try {
|
||||
releaseWindowsPairLock(lock);
|
||||
releaseWindowsBinaryLock(lock);
|
||||
} catch (releaseError) {
|
||||
if (!operationError) throw releaseError;
|
||||
}
|
||||
@@ -552,25 +546,24 @@ async function main() {
|
||||
const platform = getPlatform();
|
||||
const arch = getArch();
|
||||
const ext = platform === 'windows' ? 'zip' : 'tar.gz';
|
||||
// Package-manager shims are portable one-shot instances on Windows. They
|
||||
// enter through the cached launcher and never create managed launcher state.
|
||||
// The payload path remains the immutable pair's readiness signal.
|
||||
// Package-manager shims are portable one-shot instances: one cached binary
|
||||
// per platform, entered directly.
|
||||
const binName = platform === 'windows'
|
||||
? WINDOWS_PAYLOAD_NAME
|
||||
? WINDOWS_BINARY_NAME
|
||||
: 'codebase-memory-mcp';
|
||||
const binPath = path.join(BIN_DIR, binName);
|
||||
const cacheNames = platform === 'windows'
|
||||
? [WINDOWS_LAUNCHER_NAME, WINDOWS_PAYLOAD_NAME]
|
||||
? [WINDOWS_BINARY_NAME]
|
||||
: [binName];
|
||||
const extractedNames = platform === 'windows'
|
||||
? [WINDOWS_LAUNCHER_NAME, WINDOWS_PAYLOAD_NAME]
|
||||
? [WINDOWS_BINARY_NAME]
|
||||
: [binName];
|
||||
const archiveNames = platform === 'windows'
|
||||
? WINDOWS_ARCHIVE_NAMES
|
||||
: UNIX_ARCHIVE_NAMES;
|
||||
const cachePaths = cacheNames.map((name) => path.join(BIN_DIR, name));
|
||||
|
||||
if (platform === 'windows' && windowsPairReady(BIN_DIR)) return;
|
||||
if (platform === 'windows' && windowsBinaryReady(BIN_DIR)) return;
|
||||
if (platform !== 'windows' &&
|
||||
cachePaths.every((candidate) => fs.existsSync(candidate))) {
|
||||
try {
|
||||
@@ -631,13 +624,13 @@ async function main() {
|
||||
|
||||
if (platform === 'windows') {
|
||||
// The launcher resolves the adjacent portable payload in this directory.
|
||||
verifyCandidate(extractedPaths.get(WINDOWS_LAUNCHER_NAME));
|
||||
verifyCandidate(extractedPaths.get(WINDOWS_BINARY_NAME));
|
||||
} else {
|
||||
verifyCandidate(extractedPaths.get(binName));
|
||||
}
|
||||
|
||||
if (platform === 'windows') {
|
||||
installWindowsPairAtomically(tmpDir, BIN_DIR);
|
||||
installWindowsBinaryAtomically(tmpDir, BIN_DIR);
|
||||
} else {
|
||||
const staged = path.join(
|
||||
BIN_DIR,
|
||||
@@ -683,10 +676,9 @@ if (require.main === module || module.parent == null) {
|
||||
|
||||
module.exports = {
|
||||
UNIX_ARCHIVE_NAMES,
|
||||
WINDOWS_LAUNCHER_NAME,
|
||||
WINDOWS_PAYLOAD_NAME,
|
||||
WINDOWS_BINARY_NAME,
|
||||
extractExactTarArchive,
|
||||
installWindowsPairAtomically,
|
||||
installWindowsBinaryAtomically,
|
||||
validateExactTarMemberListing,
|
||||
windowsPairReady,
|
||||
windowsBinaryReady,
|
||||
};
|
||||
|
||||
@@ -8,10 +8,9 @@ const test = require('node:test');
|
||||
|
||||
const {
|
||||
UNIX_ARCHIVE_NAMES,
|
||||
WINDOWS_LAUNCHER_NAME,
|
||||
WINDOWS_PAYLOAD_NAME,
|
||||
WINDOWS_BINARY_NAME,
|
||||
extractExactTarArchive,
|
||||
installWindowsPairAtomically,
|
||||
installWindowsBinaryAtomically,
|
||||
validateExactTarMemberListing,
|
||||
} = require('../install.js');
|
||||
|
||||
@@ -53,25 +52,20 @@ test('Unix extraction requests only the validated root executable', () => {
|
||||
);
|
||||
});
|
||||
|
||||
function writePair(directory, tag) {
|
||||
function writeBinary(directory, tag) {
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
fs.writeFileSync(path.join(directory, WINDOWS_LAUNCHER_NAME), `launcher:${tag}`);
|
||||
fs.writeFileSync(path.join(directory, WINDOWS_PAYLOAD_NAME), `payload:${tag}`);
|
||||
fs.writeFileSync(path.join(directory, WINDOWS_BINARY_NAME), `binary:${tag}`);
|
||||
}
|
||||
|
||||
function fakePairVerifier(launcherPath) {
|
||||
const launcher = fs.readFileSync(launcherPath, 'utf8');
|
||||
const payload = fs.readFileSync(
|
||||
path.join(path.dirname(launcherPath), WINDOWS_PAYLOAD_NAME), 'utf8',
|
||||
);
|
||||
const match = /^launcher:(.+)$/.exec(launcher);
|
||||
if (!match || payload !== `payload:${match[1]}`) {
|
||||
throw new Error('launcher/payload test pair mismatch');
|
||||
function fakeBinaryVerifier(binaryPath) {
|
||||
const binary = fs.readFileSync(binaryPath, 'utf8');
|
||||
if (!/^binary:(.+)$/.test(binary)) {
|
||||
throw new Error('cached Windows binary failed verification');
|
||||
}
|
||||
}
|
||||
|
||||
function withPairDirectories(callback) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cbm-npm-pair-test-'));
|
||||
function withBinaryDirectories(callback) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cbm-npm-binary-test-'));
|
||||
const source = path.join(root, 'source');
|
||||
const destination = path.join(root, 'destination');
|
||||
fs.mkdirSync(destination);
|
||||
@@ -82,56 +76,42 @@ function withPairDirectories(callback) {
|
||||
}
|
||||
}
|
||||
|
||||
function assertPair(directory, tag) {
|
||||
function assertBinary(directory, tag) {
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(directory, WINDOWS_LAUNCHER_NAME), 'utf8'),
|
||||
`launcher:${tag}`,
|
||||
);
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(directory, WINDOWS_PAYLOAD_NAME), 'utf8'),
|
||||
`payload:${tag}`,
|
||||
fs.readFileSync(path.join(directory, WINDOWS_BINARY_NAME), 'utf8'),
|
||||
`binary:${tag}`,
|
||||
);
|
||||
}
|
||||
|
||||
test('Windows publication repairs a corrupt launcher', () => {
|
||||
withPairDirectories(({ source, destination }) => {
|
||||
writePair(source, 'candidate');
|
||||
writePair(destination, 'old');
|
||||
fs.writeFileSync(path.join(destination, WINDOWS_LAUNCHER_NAME), 'corrupt');
|
||||
test('Windows publication repairs a corrupt cached binary', () => {
|
||||
withBinaryDirectories(({ source, destination }) => {
|
||||
writeBinary(source, 'candidate');
|
||||
writeBinary(destination, 'old');
|
||||
fs.writeFileSync(path.join(destination, WINDOWS_BINARY_NAME), 'corrupt');
|
||||
|
||||
installWindowsPairAtomically(source, destination, fakePairVerifier);
|
||||
installWindowsBinaryAtomically(source, destination, fakeBinaryVerifier);
|
||||
|
||||
assertPair(destination, 'candidate');
|
||||
assertBinary(destination, 'candidate');
|
||||
});
|
||||
});
|
||||
|
||||
test('Windows publication repairs corrupt or missing payload state', () => {
|
||||
for (const payload of ['corrupt', null]) {
|
||||
withPairDirectories(({ source, destination }) => {
|
||||
writePair(source, 'candidate');
|
||||
fs.writeFileSync(
|
||||
path.join(destination, WINDOWS_LAUNCHER_NAME), 'launcher:partial',
|
||||
);
|
||||
if (payload !== null) {
|
||||
fs.writeFileSync(
|
||||
path.join(destination, WINDOWS_PAYLOAD_NAME), payload,
|
||||
);
|
||||
}
|
||||
test('Windows publication installs into an empty cache', () => {
|
||||
withBinaryDirectories(({ source, destination }) => {
|
||||
writeBinary(source, 'candidate');
|
||||
|
||||
installWindowsPairAtomically(source, destination, fakePairVerifier);
|
||||
installWindowsBinaryAtomically(source, destination, fakeBinaryVerifier);
|
||||
|
||||
assertPair(destination, 'candidate');
|
||||
});
|
||||
}
|
||||
assertBinary(destination, 'candidate');
|
||||
});
|
||||
});
|
||||
|
||||
test('Windows publication preserves a valid concurrent winner', () => {
|
||||
withPairDirectories(({ source, destination }) => {
|
||||
writePair(source, 'loser');
|
||||
writePair(destination, 'winner');
|
||||
withBinaryDirectories(({ source, destination }) => {
|
||||
writeBinary(source, 'loser');
|
||||
writeBinary(destination, 'winner');
|
||||
|
||||
installWindowsPairAtomically(source, destination, fakePairVerifier);
|
||||
installWindowsBinaryAtomically(source, destination, fakeBinaryVerifier);
|
||||
|
||||
assertPair(destination, 'winner');
|
||||
assertBinary(destination, 'winner');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,7 +95,7 @@ test('non-Windows npm shim keeps its native payload execution path', () => {
|
||||
assert.equal(path.basename(observed.calls[0].executable), 'codebase-memory-mcp');
|
||||
});
|
||||
|
||||
test('PowerShell install mutation runs through the downloaded launcher', () => {
|
||||
test('PowerShell install mutation runs through the downloaded binary', () => {
|
||||
const installer = fs.readFileSync(
|
||||
path.join(__dirname, '..', '..', '..', 'install.ps1'),
|
||||
'utf8',
|
||||
@@ -103,14 +103,16 @@ test('PowerShell install mutation runs through the downloaded launcher', () => {
|
||||
|
||||
assert.match(
|
||||
installer,
|
||||
/^\s*\$candidateVersion\s*=\s*&\s*\$DownloadedLauncher\s+--version\b/m,
|
||||
/^\s*\$candidateVersion\s*=\s*&\s*\$DownloadedBinary\s+--version\b/m,
|
||||
);
|
||||
assert.match(
|
||||
installer,
|
||||
/^\s*&\s*\$DownloadedLauncher\s+@InstallArgs\b/m,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
installer,
|
||||
/^\s*&\s*\$DownloadedPayload\s+@InstallArgs\b/m,
|
||||
/^\s*&\s*\$DownloadedBinary\s+@InstallArgs\b/m,
|
||||
);
|
||||
// One binary ships per platform — no launcher/payload pair to resolve.
|
||||
assert.doesNotMatch(installer, /payload/i);
|
||||
// A running .exe cannot be overwritten, so an in-place update has to retire
|
||||
// the existing binary by renaming it aside first. This script IS the Windows
|
||||
// update path, so losing that step would silently break every update.
|
||||
assert.match(installer, /Move-Item[\s\S]{0,80}\$Dest[\s\S]{0,40}\$retired/);
|
||||
});
|
||||
|
||||
@@ -15,11 +15,9 @@ import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
REPO = "DeusData/codebase-memory-mcp"
|
||||
_WINDOWS_LAUNCHER_NAME = "codebase-memory-mcp.exe"
|
||||
_WINDOWS_PAYLOAD_NAME = "codebase-memory-mcp.payload.exe"
|
||||
_WINDOWS_BINARY_NAME = "codebase-memory-mcp.exe"
|
||||
_WINDOWS_ARCHIVE_NAMES = (
|
||||
_WINDOWS_LAUNCHER_NAME,
|
||||
_WINDOWS_PAYLOAD_NAME,
|
||||
_WINDOWS_BINARY_NAME,
|
||||
"LICENSE",
|
||||
"install.ps1",
|
||||
"THIRD_PARTY_NOTICES.md",
|
||||
@@ -332,37 +330,26 @@ def _cache_dir() -> Path:
|
||||
|
||||
|
||||
def _bin_path(version: str) -> Path:
|
||||
# The payload remains the immutable Windows cache pair's readiness signal.
|
||||
# _execution_path selects the adjacent launcher for process execution.
|
||||
# One binary per platform: the cached file is the executed file.
|
||||
name = (
|
||||
_WINDOWS_PAYLOAD_NAME
|
||||
_WINDOWS_BINARY_NAME
|
||||
if sys.platform == "win32"
|
||||
else "codebase-memory-mcp"
|
||||
)
|
||||
return _cache_dir() / version / name
|
||||
|
||||
|
||||
def _windows_pair_paths(version: str):
|
||||
version_dir = _cache_dir() / version
|
||||
return (
|
||||
version_dir / _WINDOWS_LAUNCHER_NAME,
|
||||
version_dir / _WINDOWS_PAYLOAD_NAME,
|
||||
)
|
||||
def _execution_path(binary: Path, target_platform: str) -> Path:
|
||||
"""The cached binary is executed directly on every platform."""
|
||||
return binary
|
||||
|
||||
|
||||
def _execution_path(payload: Path, target_platform: str) -> Path:
|
||||
"""Select the permanent Windows launcher without changing other platforms."""
|
||||
if target_platform == "win32":
|
||||
return payload.with_name(_WINDOWS_LAUNCHER_NAME)
|
||||
return payload
|
||||
|
||||
|
||||
def _windows_pair_ready(version: str) -> bool:
|
||||
launcher, payload = _windows_pair_paths(version)
|
||||
if not launcher.is_file() or not payload.is_file():
|
||||
def _windows_binary_ready(version: str) -> bool:
|
||||
binary = _cache_dir() / version / _WINDOWS_BINARY_NAME
|
||||
if not binary.is_file():
|
||||
return False
|
||||
try:
|
||||
_verify_candidate(launcher)
|
||||
_verify_candidate(binary)
|
||||
except RuntimeError:
|
||||
return False
|
||||
return True
|
||||
@@ -404,21 +391,12 @@ def _download(version: str) -> Path:
|
||||
|
||||
_verify_checksum(tmp_archive, archive, version)
|
||||
|
||||
if os_name == "windows":
|
||||
bin_name = _WINDOWS_PAYLOAD_NAME
|
||||
extraction_names = (
|
||||
_WINDOWS_LAUNCHER_NAME,
|
||||
_WINDOWS_PAYLOAD_NAME,
|
||||
)
|
||||
else:
|
||||
bin_name = "codebase-memory-mcp"
|
||||
extraction_names = (bin_name,)
|
||||
cache_names = extraction_names
|
||||
publish_names = (
|
||||
(_WINDOWS_LAUNCHER_NAME, _WINDOWS_PAYLOAD_NAME)
|
||||
if os_name == "windows"
|
||||
else cache_names
|
||||
bin_name = (
|
||||
_WINDOWS_BINARY_NAME if os_name == "windows" else "codebase-memory-mcp"
|
||||
)
|
||||
extraction_names = (bin_name,)
|
||||
cache_names = extraction_names
|
||||
publish_names = cache_names
|
||||
if ext == "tar.gz":
|
||||
import tarfile
|
||||
with tarfile.open(tmp_archive) as tf:
|
||||
@@ -447,11 +425,7 @@ def _download(version: str) -> Path:
|
||||
)
|
||||
extracted_paths[name] = extracted_path
|
||||
try:
|
||||
if os_name == "windows":
|
||||
# The portable launcher must resolve this adjacent payload.
|
||||
_verify_candidate(extracted_paths[_WINDOWS_LAUNCHER_NAME])
|
||||
else:
|
||||
_verify_candidate(extracted_paths[bin_name])
|
||||
_verify_candidate(extracted_paths[bin_name])
|
||||
except RuntimeError as exc:
|
||||
sys.exit(f"codebase-memory-mcp: {exc}")
|
||||
|
||||
@@ -488,7 +462,7 @@ def _download(version: str) -> Path:
|
||||
if not _files_equal_sha256(staged_paths[name], target):
|
||||
raise publish_error
|
||||
if os_name == "windows":
|
||||
_verify_candidate(dest.parent / _WINDOWS_LAUNCHER_NAME)
|
||||
_verify_candidate(dest.parent / _WINDOWS_BINARY_NAME)
|
||||
else:
|
||||
_verify_candidate(dest)
|
||||
except RuntimeError as exc:
|
||||
@@ -508,7 +482,7 @@ def main() -> None:
|
||||
bin_path = _bin_path(version)
|
||||
|
||||
cache_ready = (
|
||||
_windows_pair_ready(version)
|
||||
_windows_binary_ready(version)
|
||||
if sys.platform == "win32"
|
||||
else bin_path.is_file()
|
||||
)
|
||||
|
||||
@@ -9,19 +9,18 @@ sys.path.insert(0, str(PACKAGE_ROOT / "src"))
|
||||
from codebase_memory_mcp import _cli # noqa: E402
|
||||
|
||||
|
||||
class WindowsLauncherSelectionTests(unittest.TestCase):
|
||||
def test_windows_uses_adjacent_launcher(self):
|
||||
payload = Path("cache") / "0.8.1" / _cli._WINDOWS_PAYLOAD_NAME
|
||||
class BinarySelectionTests(unittest.TestCase):
|
||||
"""One binary ships per platform: the cached file is the executed file."""
|
||||
|
||||
self.assertEqual(
|
||||
_cli._execution_path(payload, "win32"),
|
||||
payload.with_name(_cli._WINDOWS_LAUNCHER_NAME),
|
||||
)
|
||||
def test_windows_executes_the_cached_binary(self):
|
||||
binary = Path("cache") / "0.8.1" / _cli._WINDOWS_BINARY_NAME
|
||||
|
||||
def test_non_windows_keeps_payload(self):
|
||||
payload = Path("cache") / "0.8.1" / "codebase-memory-mcp"
|
||||
self.assertEqual(_cli._execution_path(binary, "win32"), binary)
|
||||
|
||||
self.assertEqual(_cli._execution_path(payload, "linux"), payload)
|
||||
def test_non_windows_executes_the_cached_binary(self):
|
||||
binary = Path("cache") / "0.8.1" / "codebase-memory-mcp"
|
||||
|
||||
self.assertEqual(_cli._execution_path(binary, "linux"), binary)
|
||||
|
||||
def test_portable_mutation_guidance_classification_is_preserved(self):
|
||||
self.assertEqual(_cli._portable_mutation_action(["update"]), "update")
|
||||
|
||||
@@ -104,7 +104,9 @@ mkdir -p "$EXTRACT_DIR"
|
||||
if [ "$GOOS" = "windows" ]; then
|
||||
unzip -q -o "$WORK_DIR/$NAME.zip" -d "$EXTRACT_DIR"
|
||||
test -s "$EXTRACT_DIR/codebase-memory-mcp.exe"
|
||||
test -s "$EXTRACT_DIR/codebase-memory-mcp.payload.exe"
|
||||
# ONE binary per platform: a payload sibling means the AV-flagged launcher
|
||||
# stub came back.
|
||||
test ! -e "$EXTRACT_DIR/codebase-memory-mcp.payload.exe"
|
||||
echo "=== smoke-artifact: smoking EXTRACTED $NAME.zip via vm-smoke.sh ==="
|
||||
SMOKE_ARCH="$GOARCH" SMOKE_VARIANT="$VARIANT" \
|
||||
CBM_SMOKE_ARTIFACT_DIR="$EXTRACT_DIR" \
|
||||
|
||||
+15
-12
@@ -30,16 +30,16 @@ local artifact-flow smoke lane.
|
||||
matching binary must already have been built (--with-ui for ui).
|
||||
--out-dir where to place the archive (default: repository root).
|
||||
|
||||
Make passthrough (VAR=VAL, forwarded to the Windows launcher build):
|
||||
Make passthrough (VAR=VAL, forwarded to the build):
|
||||
CC= CXX= compiler override, e.g. CC=clang CXX=clang++.
|
||||
|
||||
Environment:
|
||||
BUILD_DIR build tree to archive from (default build/c).
|
||||
|
||||
Archive contents (defined here, canonical):
|
||||
Archive contents (defined here, canonical) — ONE binary per platform:
|
||||
unix: codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md (.tar.gz)
|
||||
windows: codebase-memory-mcp.exe (launcher) codebase-memory-mcp.payload.exe
|
||||
LICENSE install.ps1 THIRD_PARTY_NOTICES.md (.zip)
|
||||
windows: codebase-memory-mcp.exe LICENSE install.ps1
|
||||
THIRD_PARTY_NOTICES.md (.zip)
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -92,25 +92,28 @@ OUT_DIR="$(mkdir -p "$OUT_DIR" && cd "$OUT_DIR" && pwd)"
|
||||
NAME="codebase-memory-mcp${SUFFIX}-${GOOS}-${GOARCH}"
|
||||
|
||||
if [ "$GOOS" = "windows" ]; then
|
||||
# The launcher is part of the ARCHIVE layout (launcher fronts the payload),
|
||||
# so it is built here, exactly as the release venue does.
|
||||
make -f Makefile.cbm "$BUILD_DIR/codebase-memory-mcp-launcher.exe" \
|
||||
BUILD_DIR="$BUILD_DIR" ${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}
|
||||
# Windows ships ONE binary, exactly like every other platform. There is no
|
||||
# launcher stub: a small unsigned PE whose entire job is to verify and
|
||||
# execute another binary is statically indistinguishable from a dropper,
|
||||
# and Defender's ML scored it Trojan:Win32/Wacatac.B!ml on x64 regardless
|
||||
# of what we changed (bcrypt-free, stripped, versioned, and even
|
||||
# resource-free builds were all flagged, while the product binary itself
|
||||
# scans clean on every platform). Self-update — the launcher's whole reason
|
||||
# to exist — moves OUT of the running process into install.ps1: Windows'
|
||||
# executable lock only blocks a process from replacing ITSELF.
|
||||
PAYLOAD="$BUILD_DIR/codebase-memory-mcp"
|
||||
[ -f "${PAYLOAD}.exe" ] && PAYLOAD="${PAYLOAD}.exe"
|
||||
[ -f "$PAYLOAD" ] || { echo "package-release: build first; missing $PAYLOAD" >&2; exit 2; }
|
||||
PACK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/cbm-package.XXXXXX")"
|
||||
trap 'rm -rf "$PACK_DIR"' EXIT
|
||||
cp "$BUILD_DIR/codebase-memory-mcp-launcher.exe" "$PACK_DIR/codebase-memory-mcp.exe"
|
||||
cp "$PAYLOAD" "$PACK_DIR/codebase-memory-mcp.payload.exe"
|
||||
cp "$PAYLOAD" "$PACK_DIR/codebase-memory-mcp.exe"
|
||||
cp LICENSE install.ps1 "$PACK_DIR/"
|
||||
scripts/gen-third-party-notices.sh "$PACK_DIR/THIRD_PARTY_NOTICES.md"
|
||||
(
|
||||
cd "$PACK_DIR"
|
||||
rm -f "$OUT_DIR/$NAME.zip"
|
||||
zip -q "$OUT_DIR/$NAME.zip" \
|
||||
codebase-memory-mcp.exe codebase-memory-mcp.payload.exe \
|
||||
LICENSE install.ps1 THIRD_PARTY_NOTICES.md
|
||||
codebase-memory-mcp.exe LICENSE install.ps1 THIRD_PARTY_NOTICES.md
|
||||
)
|
||||
echo "=== package-release: $OUT_DIR/$NAME.zip ==="
|
||||
else
|
||||
|
||||
+60
-48
@@ -57,24 +57,11 @@ smoke_mktemp_dir() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Windows release archives contain a small permanent launcher plus a portable
|
||||
# payload. Whenever a smoke fixture copies the launcher, keep the payload next
|
||||
# to it so the copied fixture remains a complete portable bundle.
|
||||
WINDOWS_PAYLOAD=""
|
||||
if [[ "$BINARY" == *.exe ]]; then
|
||||
WINDOWS_PAYLOAD="$(cd "$(dirname "$BINARY")" && pwd)/codebase-memory-mcp.payload.exe"
|
||||
if [ ! -f "$WINDOWS_PAYLOAD" ]; then
|
||||
echo "FAIL: Windows launcher has no adjacent codebase-memory-mcp.payload.exe"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Every platform ships ONE binary, Windows included: a fixture copy is complete
|
||||
# with nothing beside it.
|
||||
copy_smoke_binary() {
|
||||
local destination="$1"
|
||||
cp "$BINARY" "$destination"
|
||||
if [ -n "$WINDOWS_PAYLOAD" ]; then
|
||||
cp "$WINDOWS_PAYLOAD" "$(dirname "$destination")/codebase-memory-mcp.payload.exe"
|
||||
fi
|
||||
}
|
||||
|
||||
# Retire the shared account daemon (if one is running) and wait until it
|
||||
@@ -1174,26 +1161,32 @@ HOME="$FAKE_HOME" \
|
||||
"$BINARY" install -y > "$PHASE8_INSTALL_LOG" 2>&1 || PHASE8_INSTALL_RC=$?
|
||||
cat "$PHASE8_INSTALL_LOG"
|
||||
if [[ "$BINARY" == *.exe ]]; then
|
||||
# The managed install itself must SUCCEED on Windows — an install-time
|
||||
# staging/ACL refusal used to scroll past as tolerated noise while the
|
||||
# downstream config assertions kept passing against a previous generation
|
||||
# ("staging transaction open failed (status -3, os 0)" hid a real install
|
||||
# failure class on Administrators-default-owner profiles).
|
||||
# The install itself must SUCCEED on Windows — an install-time staging/ACL
|
||||
# refusal used to scroll past as tolerated noise while the downstream config
|
||||
# assertions kept passing against a previous copy ("staging transaction open
|
||||
# failed (status -3, os 0)" hid a real install failure class on
|
||||
# Administrators-default-owner profiles).
|
||||
if [ "$PHASE8_INSTALL_RC" -ne 0 ]; then
|
||||
echo "FAIL 8-0: managed install exited rc=$PHASE8_INSTALL_RC"
|
||||
echo "FAIL 8-0: install exited rc=$PHASE8_INSTALL_RC"
|
||||
exit 1
|
||||
fi
|
||||
PHASE8_CANONICAL="$FAKE_HOME/.local/bin/codebase-memory-mcp.exe"
|
||||
if [ ! -f "$PHASE8_CANONICAL" ]; then
|
||||
echo "FAIL 8-0: canonical launcher missing after managed install"
|
||||
echo "FAIL 8-0: installed binary missing after install"
|
||||
exit 1
|
||||
fi
|
||||
PHASE8_LINKS=$(stat -c %h "$PHASE8_CANONICAL" 2>/dev/null || echo 0)
|
||||
if [ "$PHASE8_LINKS" != "2" ]; then
|
||||
echo "FAIL 8-0: canonical launcher is not an exact two-link file (links=$PHASE8_LINKS)"
|
||||
# ONE binary, one link: a second hard link here would mean the retired
|
||||
# launcher/generation layout came back.
|
||||
PHASE8_LINKS=$(stat -c %h "$PHASE8_CANONICAL" 2>/dev/null || echo 1)
|
||||
if [ "$PHASE8_LINKS" != "1" ]; then
|
||||
echo "FAIL 8-0: installed binary is not a single-link file (links=$PHASE8_LINKS)"
|
||||
exit 1
|
||||
fi
|
||||
echo "OK 8-0: managed install committed an exact two-link canonical launcher"
|
||||
if [ -e "$FAKE_HOME/.local/bin/codebase-memory-mcp.payload.exe" ]; then
|
||||
echo "FAIL 8-0: install produced a launcher/payload pair"
|
||||
exit 1
|
||||
fi
|
||||
echo "OK 8-0: install committed exactly one Windows binary"
|
||||
fi
|
||||
|
||||
# Helper for JSON validation (pipe file to python — avoids MSYS2 path translation issues)
|
||||
@@ -2911,9 +2904,9 @@ if [ -n "${SMOKE_DOWNLOAD_URL:-}" ]; then
|
||||
UPDATE_HOME=$(smoke_mktemp_dir)
|
||||
mkdir -p "$UPDATE_HOME/.claude" "$UPDATE_HOME/.local/bin"
|
||||
if [[ "$BINARY" == *.exe ]]; then
|
||||
# Keep the managed canonical absent until WINDOWS_PAYLOAD installs the
|
||||
# authenticated two-link launcher layout below.
|
||||
:
|
||||
cp "$BINARY" "$UPDATE_HOME/.local/bin/codebase-memory-mcp.exe"
|
||||
mkdir -p "$UPDATE_HOME/retired-install"
|
||||
cp "$BINARY" "$UPDATE_HOME/retired-install/codebase-memory-mcp.exe"
|
||||
else
|
||||
cp "$BINARY" "$UPDATE_HOME/.local/bin/codebase-memory-mcp"
|
||||
chmod 755 "$UPDATE_HOME/.local/bin/codebase-memory-mcp"
|
||||
@@ -2927,18 +2920,12 @@ if [ -n "${SMOKE_DOWNLOAD_URL:-}" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# A portable Windows payload may install a managed launcher, but it must not
|
||||
# perform update/uninstall directly. Establish the managed layout first and
|
||||
# exercise those mutations through its canonical launcher.
|
||||
UPDATE_DRIVER="$BINARY"
|
||||
# POSIX runs `update` from the retired image so the in-process replacement is
|
||||
# exercised end to end. Windows has no in-process replacement: a running .exe
|
||||
# cannot replace itself, so `update` hands off to install.ps1 (asserted in
|
||||
# 14a below) and the installed copy drives the later uninstall phases.
|
||||
if [[ "$BINARY" == *.exe ]]; then
|
||||
HOME="$UPDATE_HOME" "$WINDOWS_PAYLOAD" install -y --force --skip-config \
|
||||
"--dir=$UPDATE_HOME/.local/bin"
|
||||
UPDATE_DRIVER="$UPDATE_HOME/.local/bin/codebase-memory-mcp.exe"
|
||||
if [ ! -f "$UPDATE_DRIVER" ]; then
|
||||
echo "FAIL 14a: managed Windows launcher missing after install"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
RETIRED_DIR=$(cd "$UPDATE_HOME/retired-install" && pwd -P)
|
||||
UPDATE_DRIVER="$RETIRED_DIR/codebase-memory-mcp"
|
||||
@@ -2965,8 +2952,31 @@ if [ -n "${SMOKE_DOWNLOAD_URL:-}" ]; then
|
||||
if curl --noproxy '*' -sf "$SMOKE_DOWNLOAD_URL/" 2>/dev/null | grep -q "ui-"; then
|
||||
UPDATE_VARIANT="--ui"
|
||||
fi
|
||||
UPDATE_LOG=$(smoke_mktemp_file)
|
||||
HOME="$UPDATE_HOME" CBM_DOWNLOAD_URL="$UPDATE_DOWNLOAD_URL" \
|
||||
"$UPDATE_DRIVER" update $UPDATE_VARIANT -y 2>&1
|
||||
"$UPDATE_DRIVER" update $UPDATE_VARIANT -y > "$UPDATE_LOG" 2>&1
|
||||
UPDATE_RC=$?
|
||||
cat "$UPDATE_LOG"
|
||||
|
||||
if [[ "$BINARY" == *.exe ]]; then
|
||||
# Windows contract: update NEVER replaces the running image in-process. It
|
||||
# exits 0 and prints the install.ps1 command. Regressing to an in-process
|
||||
# self-update means reintroducing the AV-flagged launcher stub.
|
||||
if [ "$UPDATE_RC" -ne 0 ]; then
|
||||
echo "FAIL 14a: Windows update exited rc=$UPDATE_RC (expected 0)"
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q "install.ps1" "$UPDATE_LOG"; then
|
||||
echo "FAIL 14a: Windows update did not print the install.ps1 command"
|
||||
exit 1
|
||||
fi
|
||||
if ! cmp -s "$BINARY" "$UPDATE_HOME/.local/bin/codebase-memory-mcp.exe"; then
|
||||
echo "FAIL 14a: Windows update replaced the binary in-process"
|
||||
exit 1
|
||||
fi
|
||||
echo "OK 14a: Windows update handed off to install.ps1 without touching the binary"
|
||||
fi
|
||||
rm -f "$UPDATE_LOG"
|
||||
|
||||
# 14b: Verify new binary exists and runs
|
||||
if [[ "$BINARY" == *.exe ]]; then
|
||||
@@ -2988,6 +2998,11 @@ if [ -n "${SMOKE_DOWNLOAD_URL:-}" ]; then
|
||||
echo "OK 14b: updated binary runs"
|
||||
|
||||
# 14c: Verify agent config was refreshed to the exact installed binary.
|
||||
# Windows has no in-process update, so there is no config refresh to assert:
|
||||
# install.ps1 re-runs `install`, which is covered by Phase 8 and Phase 13.
|
||||
if [[ "$BINARY" == *.exe ]]; then
|
||||
echo "SKIP 14c: Windows update hands off to install.ps1 (config refresh covered by install)"
|
||||
else
|
||||
UPD_CMD=$(cat "$UPDATE_HOME/.claude.json" 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('mcpServers',{}).get('codebase-memory-mcp',{}).get('command',''))" 2>/dev/null || echo "")
|
||||
EXPECTED_UPD_CMD="$UPD_BIN"
|
||||
if command -v cygpath &>/dev/null; then
|
||||
@@ -3000,6 +3015,7 @@ if [ -n "${SMOKE_DOWNLOAD_URL:-}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
echo "OK 14c: agent config refreshed (path=$UPD_CMD)"
|
||||
fi
|
||||
|
||||
# ── 14d-f: Real uninstall with binary removal ──
|
||||
# First verify binary + configs exist
|
||||
@@ -3158,9 +3174,10 @@ echo "--- Phase 12d: extraction ---"
|
||||
(cd "$DL_DIR" && if [ "$DL_EXT" = "zip" ]; then unzip -q "$DL_ARCHIVE"; else tar -xzf "$DL_ARCHIVE"; fi)
|
||||
if [ "$DL_OS" = "windows" ]; then
|
||||
DL_BIN="$DL_DIR/codebase-memory-mcp.exe"
|
||||
DL_PAYLOAD="$DL_DIR/codebase-memory-mcp.payload.exe"
|
||||
if [ ! -f "$DL_PAYLOAD" ]; then
|
||||
echo "FAIL 12d: Windows payload not found after extraction"
|
||||
# ONE binary per platform: a second executable in the archive would mean the
|
||||
# AV-flagged launcher/payload split came back.
|
||||
if [ -e "$DL_DIR/codebase-memory-mcp.payload.exe" ]; then
|
||||
echo "FAIL 12d: Windows archive still ships a launcher/payload pair"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
@@ -3173,11 +3190,6 @@ fi
|
||||
chmod +x "$DL_BIN"
|
||||
echo "OK 12d: binary extracted"
|
||||
|
||||
if [ "$DL_OS" = "windows" ] && ! "$DL_PAYLOAD" --version > /dev/null 2>&1; then
|
||||
echo "FAIL 12d: extracted Windows payload doesn't run"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 12e: extracted binary runs
|
||||
if ! "$DL_BIN" --version > /dev/null 2>&1; then
|
||||
# On macOS arm64, may need signing
|
||||
|
||||
+21
-61
@@ -3,10 +3,10 @@
|
||||
Run the native-Windows product-surface test suite for codebase-memory-mcp.
|
||||
|
||||
.DESCRIPTION
|
||||
Builds the payload and permanent launcher if they are not already present,
|
||||
stages them under their release names, then runs the deterministic Windows
|
||||
integration tests under tests/windows/ through the launcher (real stdio /
|
||||
CLI / HTTP UI, real SQLite DB).
|
||||
Builds the product binary if it is not already present, stages it under its
|
||||
release name, then runs the deterministic Windows integration tests under
|
||||
tests/windows/ against it (real stdio / CLI / HTTP UI, real SQLite DB).
|
||||
Windows ships ONE binary, exactly like Linux and macOS.
|
||||
|
||||
Two categories of test:
|
||||
|
||||
@@ -20,8 +20,9 @@
|
||||
* test_cli_non_ascii_arg.py guards #423/#20 (wide-argv main())
|
||||
* test_daemon_stability.py guards the daemon parameter
|
||||
surface, crash recovery, busy-stop refusal, and churn
|
||||
* test_windows_launcher.py guards the permanent launcher,
|
||||
managed layout, portable refusal, and crash containment
|
||||
* test_windows_update_handoff.py guards that `update`
|
||||
hands off to install.ps1 instead of replacing its own
|
||||
running image (the removed launcher stub's only job)
|
||||
|
||||
KNOWN REDS - genuine, still-open Windows bugs reproduced at the product
|
||||
surface. They are EXPECTED to be RED (exit 1) and are opt-in
|
||||
@@ -40,13 +41,8 @@
|
||||
AddressSanitizer/UBSan (Linux containers, WSL), prefer scripts/test.sh.
|
||||
|
||||
.PARAMETER Binary
|
||||
Path to an existing portable payload executable. If omitted, the script
|
||||
builds it (target selected by -Target) into build/c/.
|
||||
|
||||
.PARAMETER Launcher
|
||||
Path to the permanent launcher executable. If omitted, the script uses
|
||||
build/c/codebase-memory-mcp-launcher.exe, building target cbm-launcher when
|
||||
needed.
|
||||
Path to an existing product executable. If omitted, the script builds it
|
||||
(target selected by -Target) into build/c/.
|
||||
|
||||
.PARAMETER Target
|
||||
Makefile.cbm target used when building: 'cbm-with-ui' (default; needed for the
|
||||
@@ -68,7 +64,6 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Binary,
|
||||
[string]$Launcher,
|
||||
[ValidateSet("cbm-with-ui", "cbm")]
|
||||
[string]$Target = "cbm-with-ui",
|
||||
[switch]$GuardsOnly,
|
||||
@@ -103,36 +98,8 @@ function Resolve-Binary {
|
||||
return $built
|
||||
}
|
||||
|
||||
function Resolve-Launcher {
|
||||
param([string]$Explicit)
|
||||
if ($Explicit) { return (Resolve-Path $Explicit).Path }
|
||||
$built = Join-Path $repoRoot "build\c\codebase-memory-mcp-launcher.exe"
|
||||
if (Test-Path $built) { return $built }
|
||||
Write-Host "Building permanent launcher via Makefile.cbm ..." -ForegroundColor Cyan
|
||||
& $Make "-j" "-f" "Makefile.cbm" "cbm-launcher" "SANITIZE=" "TMP=$tmp" "TEMP=$tmp" "TMPDIR=$tmp" | Out-Host
|
||||
$buildExit = $LASTEXITCODE
|
||||
if ($buildExit -ne 0) { throw "launcher build failed (exit $buildExit)" }
|
||||
if (-not (Test-Path $built)) { throw "launcher not produced at $built" }
|
||||
return $built
|
||||
}
|
||||
|
||||
function Resolve-AbiMismatchLauncher {
|
||||
$built = Join-Path $repoRoot "build\c\codebase-memory-mcp-launcher-abi2.exe"
|
||||
if (Test-Path $built) { return $built }
|
||||
Write-Host "Building launcher ABI mismatch fixture via Makefile.cbm ..." -ForegroundColor Cyan
|
||||
& $Make "-j" "-f" "Makefile.cbm" "build/c/codebase-memory-mcp-launcher-abi2.exe" "SANITIZE=" "TMP=$tmp" "TEMP=$tmp" "TMPDIR=$tmp" | Out-Host
|
||||
$buildExit = $LASTEXITCODE
|
||||
if ($buildExit -ne 0) { throw "launcher ABI fixture build failed (exit $buildExit)" }
|
||||
if (-not (Test-Path $built)) { throw "launcher ABI fixture not produced at $built" }
|
||||
return $built
|
||||
}
|
||||
|
||||
$bin = Resolve-Binary -Explicit $Binary
|
||||
$launcherBin = Resolve-Launcher -Explicit $Launcher
|
||||
$abiMismatchLauncher = Resolve-AbiMismatchLauncher
|
||||
Write-Host "Payload: $bin" -ForegroundColor Green
|
||||
Write-Host "Launcher: $launcherBin" -ForegroundColor Green
|
||||
Write-Host "ABI mismatch fixture: $abiMismatchLauncher" -ForegroundColor Green
|
||||
Write-Host "Binary: $bin" -ForegroundColor Green
|
||||
|
||||
$previousTemp = $env:TEMP
|
||||
$previousTmp = $env:TMP
|
||||
@@ -145,8 +112,7 @@ try {
|
||||
New-Item -ItemType Directory -Path $guardRoot | Out-Null
|
||||
|
||||
# GitHub-hosted runner profile children can inherit mutation-capable ACEs
|
||||
# even though the profile ancestry itself passes the launcher's bounded
|
||||
# trust policy. Replace that inheritance before creating any executable or
|
||||
# even though the profile ancestry itself passes the bounded trust policy. Replace that inheritance before creating any executable or
|
||||
# Python temporary descendant. Use SIDs rather than localized account names.
|
||||
$currentSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
||||
if (-not $currentSid) { throw "could not resolve the current user's SID" }
|
||||
@@ -167,24 +133,22 @@ try {
|
||||
$guardBundle = Join-Path $guardRoot ("cbm-windows-guards-" + [guid]::NewGuid().ToString("N"))
|
||||
New-Item -ItemType Directory -Path $guardBundle | Out-Null
|
||||
$guardBin = Join-Path $guardBundle "codebase-memory-mcp.exe"
|
||||
$guardPayload = Join-Path $guardBundle "codebase-memory-mcp.payload.exe"
|
||||
Copy-Item -LiteralPath $launcherBin -Destination $guardBin
|
||||
Copy-Item -LiteralPath $bin -Destination $guardPayload
|
||||
Copy-Item -LiteralPath $bin -Destination $guardBin
|
||||
|
||||
# Ownership is never inherited on Windows: descendants created under the
|
||||
# hardened root by an admin-group token can default to the Administrators
|
||||
# SID, and the launcher's exe policy demands the exact current user as
|
||||
# owner. Stamp the current SID explicitly on everything staged here.
|
||||
foreach ($staged in @($guardBundle, $guardBin, $guardPayload)) {
|
||||
# SID, while the exe policy demands the exact current user as owner. Stamp
|
||||
# the current SID explicitly on everything staged here.
|
||||
foreach ($staged in @($guardBundle, $guardBin)) {
|
||||
$stagedAcl = Get-Acl -LiteralPath $staged
|
||||
$stagedAcl.SetOwner($currentSid)
|
||||
Set-Acl -LiteralPath $staged -AclObject $stagedAcl
|
||||
}
|
||||
Write-Host "Guard bundle: $guardBin" -ForegroundColor Green
|
||||
|
||||
# The launcher deliberately rejects GitHub's shared D:\a ancestry and the
|
||||
# hosted runner's inherited LocalAppData\Temp ACL. Keep launcher fixtures
|
||||
# and Python-created descendants below the accepted profile ancestry.
|
||||
# The guards deliberately reject GitHub's shared D:\a ancestry and the
|
||||
# hosted runner's inherited LocalAppData\Temp ACL. Keep staged fixtures and
|
||||
# Python-created descendants below the accepted profile ancestry.
|
||||
$env:TEMP = $guardRoot
|
||||
$env:TMP = $guardRoot
|
||||
$env:TMPDIR = $guardRoot
|
||||
@@ -202,7 +166,7 @@ $guards = @(
|
||||
"tests\windows\test_hook_augment.py",
|
||||
"tests\windows\test_ui_drive_listing.py",
|
||||
"tests\windows\test_cli_non_ascii_arg.py",
|
||||
"tests\windows\test_windows_launcher.py"
|
||||
"tests\windows\test_windows_update_handoff.py"
|
||||
)
|
||||
|
||||
# Opt-in known-red repros - EXPECTED red (exit 1); never gate CI. Currently empty:
|
||||
@@ -216,15 +180,11 @@ $fixedKeepers = @()
|
||||
Write-Host "`n--- Green guards ---" -ForegroundColor Cyan
|
||||
foreach ($t in $guards) {
|
||||
Write-Host "`n=== $t ===" -ForegroundColor Cyan
|
||||
if ($t -eq "tests\windows\test_windows_launcher.py") {
|
||||
& $py $t $guardBin $guardPayload $abiMismatchLauncher
|
||||
} else {
|
||||
& $py $t $guardBin
|
||||
}
|
||||
& $py $t $guardBin
|
||||
$code = $LASTEXITCODE
|
||||
if ($code -eq 0) {
|
||||
Write-Host "GREEN ($t)" -ForegroundColor Green
|
||||
} elseif ($code -eq 1 -or $t -eq "tests\windows\test_windows_launcher.py") {
|
||||
} elseif ($code -eq 1 -or $t -eq "tests\windows\test_windows_update_handoff.py") {
|
||||
Write-Host "RED ($t) - REGRESSION: a fixed Windows bug is broken again" -ForegroundColor Red
|
||||
$guardFailures += $t
|
||||
} elseif ($code -eq 2) {
|
||||
|
||||
+47
-2296
File diff suppressed because it is too large
Load Diff
@@ -348,19 +348,6 @@ unsigned char *cbm_extract_binary_from_targz(const unsigned char *data, int data
|
||||
* Returns NULL on error. Caller must free. */
|
||||
unsigned char *cbm_extract_binary_from_zip(const unsigned char *data, int data_len, int *out_len);
|
||||
|
||||
/* Strict two-file Windows release bundle extraction. The archive must contain
|
||||
* exactly one root launcher and one root payload; ambiguous aliases,
|
||||
* traversal, duplicates, and malformed central/local metadata fail closed. */
|
||||
typedef struct {
|
||||
unsigned char *launcher;
|
||||
int launcher_len;
|
||||
unsigned char *payload;
|
||||
int payload_len;
|
||||
} cbm_windows_release_pair_t;
|
||||
bool cbm_extract_windows_release_pair_from_zip(const unsigned char *data, int data_len,
|
||||
cbm_windows_release_pair_t *pair_out);
|
||||
void cbm_windows_release_pair_free(cbm_windows_release_pair_t *pair);
|
||||
|
||||
/* ── Index management ─────────────────────────────────────────── */
|
||||
|
||||
/* List .db files in the cache directory (~/.cache/codebase-memory-mcp/).
|
||||
@@ -447,14 +434,6 @@ void cbm_cli_set_activation_ops_for_test(const cbm_cli_activation_ops_t *ops);
|
||||
* command-line or environment override. */
|
||||
void cbm_cli_set_activation_runtime_parent_for_test(const char *runtime_parent);
|
||||
|
||||
/* Consume and authenticate any inherited permanent-launcher context before
|
||||
* process-role classification. An absent context is the normal portable
|
||||
* payload case; an advertised but invalid context fails closed. */
|
||||
int cbm_cli_windows_launcher_startup_authenticate(int argc, char *const argv[]);
|
||||
/* Internal release-pair probe. Returns -1 when argv does not select the role,
|
||||
* otherwise a process exit code. It runs before cache/daemon initialization. */
|
||||
int cbm_cli_windows_payload_descriptor_role(int argc, char *const argv[]);
|
||||
|
||||
/* ── Subcommands (wired from main.c) ─────────────────────────── */
|
||||
|
||||
/* install: copy binary, install skills, install editor MCP configs, ensure PATH.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,157 +0,0 @@
|
||||
/*
|
||||
* windows_launcher_state.h -- Stable state shared by the Windows launcher and
|
||||
* the CBM payload.
|
||||
*
|
||||
* The on-disk current-v1 record is deliberately fixed-size and contains no
|
||||
* native C layout. Keep its codec platform-independent: release compatibility
|
||||
* is a byte contract, not a compiler/architecture contract.
|
||||
*/
|
||||
#ifndef CBM_WINDOWS_LAUNCHER_STATE_H
|
||||
#define CBM_WINDOWS_LAUNCHER_STATE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <wchar.h>
|
||||
|
||||
#define CBM_WINDOWS_CURRENT_V1_SIZE 128U
|
||||
#define CBM_WINDOWS_RELEASE_DESCRIPTOR_V1_SIZE 128U
|
||||
#ifndef CBM_WINDOWS_LAUNCHER_ABI_CURRENT
|
||||
#define CBM_WINDOWS_LAUNCHER_ABI_CURRENT 1U
|
||||
#endif
|
||||
#ifndef CBM_WINDOWS_PAYLOAD_LAUNCHER_ABI_MIN
|
||||
#define CBM_WINDOWS_PAYLOAD_LAUNCHER_ABI_MIN 1U
|
||||
#endif
|
||||
#ifndef CBM_WINDOWS_PAYLOAD_LAUNCHER_ABI_MAX
|
||||
#define CBM_WINDOWS_PAYLOAD_LAUNCHER_ABI_MAX 1U
|
||||
#endif
|
||||
#define CBM_WINDOWS_LAUNCHER_PATH_CAP 32768U
|
||||
|
||||
/* Hex-char count of the payload digest embedded in a retired-state directory
|
||||
* name. 16 chars (64 bits) uniquely identifies the retired generation while
|
||||
* keeping the retired path short enough for the managed uninstall's
|
||||
* handle-based directory rename at the deepest supported install depth. */
|
||||
#define CBM_WINDOWS_RETIRED_TAG_HEX 16U
|
||||
|
||||
typedef struct {
|
||||
uint32_t launcher_abi_min;
|
||||
uint32_t launcher_abi_max;
|
||||
uint64_t payload_size;
|
||||
char payload_sha256[65];
|
||||
} cbm_windows_current_v1_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t launcher_abi;
|
||||
uint32_t payload_launcher_abi_min;
|
||||
uint32_t payload_launcher_abi_max;
|
||||
uint64_t payload_size;
|
||||
char payload_sha256[65];
|
||||
} cbm_windows_release_descriptor_v1_t;
|
||||
|
||||
typedef enum {
|
||||
CBM_WINDOWS_TRANSITION_INCOMPATIBLE = 0,
|
||||
CBM_WINDOWS_TRANSITION_LAUNCHER_FIRST = 1,
|
||||
CBM_WINDOWS_TRANSITION_CURRENT_FIRST = 2,
|
||||
} cbm_windows_transition_plan_t;
|
||||
|
||||
typedef enum {
|
||||
CBM_WINDOWS_LAUNCHER_ACTION_ORDINARY = 0,
|
||||
CBM_WINDOWS_LAUNCHER_ACTION_UPDATE = 1,
|
||||
CBM_WINDOWS_LAUNCHER_ACTION_UNINSTALL = 2,
|
||||
} cbm_windows_launcher_action_t;
|
||||
|
||||
bool cbm_windows_current_v1_encode(const cbm_windows_current_v1_t *state,
|
||||
uint8_t out[CBM_WINDOWS_CURRENT_V1_SIZE]);
|
||||
bool cbm_windows_current_v1_decode(const uint8_t *record, size_t record_size,
|
||||
cbm_windows_current_v1_t *state_out);
|
||||
bool cbm_windows_current_v1_supports_launcher_abi(const cbm_windows_current_v1_t *state,
|
||||
uint32_t launcher_abi);
|
||||
|
||||
bool cbm_windows_release_descriptor_v1_encode(const cbm_windows_release_descriptor_v1_t *descriptor,
|
||||
uint8_t out[CBM_WINDOWS_RELEASE_DESCRIPTOR_V1_SIZE]);
|
||||
bool cbm_windows_release_descriptor_v1_decode(const uint8_t *record, size_t record_size,
|
||||
cbm_windows_release_descriptor_v1_t *descriptor_out);
|
||||
cbm_windows_transition_plan_t cbm_windows_transition_plan(
|
||||
const cbm_windows_current_v1_t *current, const cbm_windows_release_descriptor_v1_t *candidate);
|
||||
|
||||
/* Resolve the immutable generation pair using the canonical launcher's
|
||||
* directory. A managed generation contains exactly these two executables:
|
||||
* the payload and the launcher's hard-link backing. */
|
||||
bool cbm_windows_generation_payload_path(const wchar_t *canonical_launcher_path,
|
||||
const char payload_sha256[65], wchar_t *path_out,
|
||||
size_t path_capacity);
|
||||
bool cbm_windows_generation_launcher_path(const wchar_t *canonical_launcher_path,
|
||||
const char payload_sha256[65], wchar_t *path_out,
|
||||
size_t path_capacity);
|
||||
/* Derive the race-free retired state sibling shared by the uninstall payload
|
||||
* (its own PID) and supervising launcher (the authenticated child PID). */
|
||||
bool cbm_windows_retired_state_path(const wchar_t *canonical_launcher_path,
|
||||
const char payload_sha256[65], uint32_t payload_pid,
|
||||
wchar_t *path_out, size_t path_capacity);
|
||||
|
||||
/* Match main's top-level dispatch. Tokens after a mode selector (cli,
|
||||
* install, config, hook-augment, help/version) are opaque user input. */
|
||||
cbm_windows_launcher_action_t cbm_windows_launcher_classify_action(int argc,
|
||||
const char *const argv[]);
|
||||
bool cbm_windows_launcher_action_allowed(cbm_windows_launcher_action_t action, bool managed);
|
||||
|
||||
/* Trusted launch data delivered over a launcher-owned inherited named pipe.
|
||||
* An absent context is valid and returns true with present=false (direct
|
||||
* portable payload). An advertised but invalid context fails closed. */
|
||||
typedef struct {
|
||||
bool present;
|
||||
bool managed;
|
||||
bool private_activation;
|
||||
cbm_windows_launcher_action_t action;
|
||||
uint64_t payload_size;
|
||||
char expected_payload_sha256[65];
|
||||
wchar_t canonical_launcher_path[CBM_WINDOWS_LAUNCHER_PATH_CAP];
|
||||
/* Opaque one-shot authority retained only until startup validation has
|
||||
* completed. Callers must use context_complete, never inspect it. */
|
||||
uintptr_t _authority_handle;
|
||||
} cbm_windows_launcher_context_t;
|
||||
|
||||
bool cbm_windows_launcher_context_consume(cbm_windows_launcher_context_t *context_out, char *error,
|
||||
size_t error_size);
|
||||
bool cbm_windows_launcher_context_complete(cbm_windows_launcher_context_t *context, bool accepted,
|
||||
char *error, size_t error_size);
|
||||
|
||||
/* Windows managed-install primitives. They fail closed on other platforms.
|
||||
* Diagnostics are optional and always NUL-terminated when capacity is nonzero.
|
||||
*/
|
||||
bool cbm_windows_launcher_capability_probe(const wchar_t *target_directory,
|
||||
const wchar_t *launcher_candidate, char *error,
|
||||
size_t error_size);
|
||||
bool cbm_windows_launcher_file_secure(const wchar_t *launcher_path, char *error, size_t error_size);
|
||||
/* Find the unique managed-generation backing for canonical_launcher_path.
|
||||
* The canonical name and backing must be the same file identity with exactly
|
||||
* two links. The backing need not be in the current payload generation while
|
||||
* a crash-safe cross-ABI transition is between its two publication steps. */
|
||||
bool cbm_windows_managed_launcher_backing(const wchar_t *canonical_launcher_path,
|
||||
wchar_t *backing_path_out, size_t backing_path_capacity,
|
||||
char *error, size_t error_size);
|
||||
bool cbm_windows_release_descriptor_probe(const wchar_t *launcher_candidate,
|
||||
cbm_windows_release_descriptor_v1_t *descriptor_out,
|
||||
char *error, size_t error_size);
|
||||
bool cbm_windows_current_v1_write_atomic(const wchar_t *canonical_launcher_path,
|
||||
const cbm_windows_current_v1_t *state, char *error,
|
||||
size_t error_size);
|
||||
/* Publish an exact-one-link immutable generation backing at the canonical
|
||||
* name by hard-linking a same-directory stage and POSIX-renaming that stage.
|
||||
* Success leaves canonical and backing as the same exact-two-link file. */
|
||||
bool cbm_windows_launcher_replace_atomic(const wchar_t *target_path, const wchar_t *backing_path,
|
||||
char *error, size_t error_size);
|
||||
bool cbm_windows_launcher_remove_posix(const wchar_t *target_path, char *error, size_t error_size);
|
||||
/* Retire .cbm to its generation/PID-qualified sibling, then unlink canonical
|
||||
* as the final uninstall commit. A failed unlink restores .cbm. */
|
||||
bool cbm_windows_launcher_uninstall_commit(const wchar_t *canonical_launcher_path,
|
||||
const char payload_sha256[65], char *error,
|
||||
size_t error_size);
|
||||
bool cbm_windows_generation_rollback_if_unreferenced(const wchar_t *canonical_launcher_path,
|
||||
const char payload_sha256[65],
|
||||
bool created_by_activation, char *error,
|
||||
size_t error_size);
|
||||
bool cbm_windows_generations_prune(const wchar_t *canonical_launcher_path, size_t *removed_out,
|
||||
char *error, size_t error_size);
|
||||
|
||||
#endif /* CBM_WINDOWS_LAUNCHER_STATE_H */
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1719,13 +1719,6 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
int windows_descriptor_role = cbm_cli_windows_payload_descriptor_role(argc, argv);
|
||||
if (windows_descriptor_role >= 0) {
|
||||
return windows_descriptor_role;
|
||||
}
|
||||
if (cbm_cli_windows_launcher_startup_authenticate(argc, argv) != 0) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
cbm_daemon_process_role_t role = cbm_daemon_process_role(argc, argv);
|
||||
if (role == CBM_DAEMON_PROCESS_INVALID) {
|
||||
(void)fprintf(stderr, "codebase-memory-mcp: invalid internal process arguments\n");
|
||||
|
||||
@@ -198,9 +198,8 @@ services:
|
||||
command:
|
||||
- |
|
||||
scripts/build.sh CC=x86_64-w64-mingw32-clang CXX=x86_64-w64-mingw32-clang++ BUILD_DIR=build/win-cross &&
|
||||
mv build/win-cross/codebase-memory-mcp.exe build/win-cross/codebase-memory-mcp.payload.exe &&
|
||||
mv build/win-cross/codebase-memory-mcp-launcher.exe build/win-cross/codebase-memory-mcp.exe &&
|
||||
WINEDEBUG=-all wine64 ./build/win-cross/codebase-memory-mcp.payload.exe --version &&
|
||||
test ! -e build/win-cross/codebase-memory-mcp.payload.exe &&
|
||||
WINEDEBUG=-all wine64 ./build/win-cross/codebase-memory-mcp.exe --version &&
|
||||
WINEDEBUG=-all wine64 cmd /c build/win-cross/codebase-memory-mcp.exe --version
|
||||
|
||||
# ── Linux portable (Alpine musl static) ────────────────────
|
||||
|
||||
@@ -206,11 +206,11 @@ case "${1:-full}" in
|
||||
$COMPOSE run --rm -e CBM_SKIP_PERF=1 test-portable
|
||||
;;
|
||||
windows)
|
||||
echo "=== Windows: cross-compile + launcher/payload version check (Wine) ==="
|
||||
echo "=== Windows: cross-compile + binary version check (Wine) ==="
|
||||
$COMPOSE run --rm smoke-windows
|
||||
;;
|
||||
smoke-windows)
|
||||
echo "=== Windows: launcher/payload version check (cross-compile + Wine) ==="
|
||||
echo "=== Windows: binary version check (cross-compile + Wine) ==="
|
||||
$COMPOSE run --rm smoke-windows
|
||||
;;
|
||||
amd64)
|
||||
@@ -237,7 +237,7 @@ case "${1:-full}" in
|
||||
$COMPOSE run --rm -e CBM_SKIP_PERF=1 test-amd64
|
||||
$COMPOSE run --rm build-amd64
|
||||
$COMPOSE run --rm smoke-amd64
|
||||
echo "=== Windows: cross-compile + launcher/payload version check (Wine) ==="
|
||||
echo "=== Windows: cross-compile + binary version check (Wine) ==="
|
||||
$COMPOSE run --rm smoke-windows
|
||||
print_real_windows_gate
|
||||
;;
|
||||
|
||||
@@ -110,7 +110,7 @@ else
|
||||
vm "clangarm64" "git clone --branch ${BRANCH} --single-branch --depth 200 ${REPO_URL} /c/cbm && cd /c/cbm && git log --oneline -1"
|
||||
fi
|
||||
|
||||
step "5/6 build: native ARM64 binary + launcher + test-runner (no ASan on arm64)"
|
||||
step "5/6 build: native ARM64 binary + test-runner (no ASan on arm64)"
|
||||
vm "clangarm64" "cd /c/cbm && make -j${JOBS} -f Makefile.cbm CC=clang CXX=clang++ SANITIZE= cbm build/c/test-runner > /tmp/provision-build.log 2>&1 && echo BUILD_OK || (echo BUILD_FAIL; tail -15 /tmp/provision-build.log; exit 1)"
|
||||
|
||||
step "6/6 smoke: binary + test-runner start"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
# Stages and serves a complete Windows release fixture from a profile-rooted
|
||||
# directory. MSYS2 /tmp and the shared runner workspace are intentionally not
|
||||
# valid launcher bundle roots.
|
||||
# valid install roots.
|
||||
#
|
||||
# Run inside the VM's CLANGARM64 shell from the repo root:
|
||||
# bash test-infrastructure/vm/vm-smoke.sh
|
||||
@@ -19,7 +19,7 @@ Usage: [env] bash test-infrastructure/vm/vm-smoke.sh
|
||||
|
||||
The canonical WINDOWS smoke entry: identical in local CI (win.sh
|
||||
smoke-install), PR CI (pr.yml) and the release venues (_smoke.yml). Stages a
|
||||
complete launcher+payload release fixture under a disposable profile root,
|
||||
complete single-binary release fixture under a disposable profile root,
|
||||
serves it on a kernel-assigned port, prepares/verifies/cleans the user-PATH
|
||||
registry via windows-user-path-guard.ps1, neutralizes every agent-config
|
||||
destination override, then runs scripts/smoke-test.sh (all phases).
|
||||
@@ -33,11 +33,11 @@ Environment:
|
||||
binary cannot pass a ui run.
|
||||
CBM_SMOKE_ARTIFACT_DIR
|
||||
Release mode: an EXTRACTED windows release artifact
|
||||
(codebase-memory-mcp.exe + .payload.exe + LICENSE +
|
||||
install.ps1 + THIRD_PARTY_NOTICES.md). All five are required
|
||||
and served verbatim — an incomplete archive fails the smoke.
|
||||
Unset (default): stages the freshly built launcher/payload
|
||||
out of build/c and synthesizes the sidecars (local/PR mode).
|
||||
(codebase-memory-mcp.exe + LICENSE + install.ps1 +
|
||||
THIRD_PARTY_NOTICES.md). All four are required and served
|
||||
verbatim — an incomplete archive fails the smoke.
|
||||
Unset (default): stages the freshly built binary out of
|
||||
build/c and synthesizes the sidecars (local/PR mode).
|
||||
|
||||
On failure the smoke root is preserved for post-mortem (path printed).
|
||||
EOF
|
||||
@@ -51,27 +51,22 @@ cd "$(dirname "$0")/../.."
|
||||
ROOT="$PWD"
|
||||
|
||||
# Two staging sources, one smoke. Unset (the local + PR default): stage the
|
||||
# freshly built launcher/payload out of build/c and synthesize the release
|
||||
# sidecars. Set: stage an EXTRACTED release artifact verbatim, so the release
|
||||
# freshly built binary out of build/c and synthesize the release sidecars. Set: stage an EXTRACTED release artifact verbatim, so the release
|
||||
# venue smokes the bytes it is about to publish rather than a local rebuild of
|
||||
# them. Requiring the sidecars here also makes an incomplete archive a smoke
|
||||
# failure instead of a discovery made after publishing.
|
||||
ARTIFACT_DIR="${CBM_SMOKE_ARTIFACT_DIR:-}"
|
||||
if [ -n "$ARTIFACT_DIR" ]; then
|
||||
ARTIFACT_DIR="$(cd "$ARTIFACT_DIR" && pwd)"
|
||||
for required in codebase-memory-mcp.exe codebase-memory-mcp.payload.exe \
|
||||
for required in codebase-memory-mcp.exe \
|
||||
LICENSE install.ps1 THIRD_PARTY_NOTICES.md; do
|
||||
[ -s "$ARTIFACT_DIR/$required" ] ||
|
||||
{ echo "vm-smoke: release artifact is missing $required" >&2; exit 2; }
|
||||
done
|
||||
LAUNCHER_SRC="$ARTIFACT_DIR/codebase-memory-mcp.exe"
|
||||
PAYLOAD_SRC="$ARTIFACT_DIR/codebase-memory-mcp.payload.exe"
|
||||
BINARY_SRC="$ARTIFACT_DIR/codebase-memory-mcp.exe"
|
||||
else
|
||||
LAUNCHER_SRC="build/c/codebase-memory-mcp-launcher.exe"
|
||||
PAYLOAD_SRC="build/c/codebase-memory-mcp.exe"
|
||||
for binary in "$LAUNCHER_SRC" "$PAYLOAD_SRC"; do
|
||||
[ -x "$binary" ] || { echo "build first; missing $binary" >&2; exit 2; }
|
||||
done
|
||||
BINARY_SRC="build/c/codebase-memory-mcp.exe"
|
||||
[ -x "$BINARY_SRC" ] || { echo "build first; missing $BINARY_SRC" >&2; exit 2; }
|
||||
fi
|
||||
|
||||
SMOKE_ARCH="${SMOKE_ARCH:-arm64}"
|
||||
@@ -149,10 +144,8 @@ MSYS2_ARG_CONV_EXCL='*' powershell.exe -NoProfile -ExecutionPolicy Bypass \
|
||||
-RunId "$PATH_RUN_ID" \
|
||||
-SnapshotPath "$(cygpath -w "$PATH_SNAPSHOT")" \
|
||||
-SmokeRoot "$(cygpath -w "$SMOKE_DIR")"
|
||||
cp "$LAUNCHER_SRC" "$SMOKE_DIR/codebase-memory-mcp.exe"
|
||||
cp "$PAYLOAD_SRC" "$SMOKE_DIR/codebase-memory-mcp.payload.exe"
|
||||
cp "$SMOKE_DIR/codebase-memory-mcp.exe" \
|
||||
"$SMOKE_DIR/codebase-memory-mcp.payload.exe" "$FIXTURE_DIR/"
|
||||
cp "$BINARY_SRC" "$SMOKE_DIR/codebase-memory-mcp.exe"
|
||||
cp "$SMOKE_DIR/codebase-memory-mcp.exe" "$FIXTURE_DIR/"
|
||||
# The install/update phases fetch these out of the served archive, so they must
|
||||
# be the artifact's own copies whenever one was supplied — regenerating them
|
||||
# here would smoke a sidecar the release never ships.
|
||||
@@ -168,8 +161,7 @@ EXPECTED_ARTIFACT="codebase-memory-mcp${SUFFIX}-windows-${SMOKE_ARCH}.zip"
|
||||
(
|
||||
cd "$FIXTURE_DIR"
|
||||
zip -q "$EXPECTED_ARTIFACT" \
|
||||
codebase-memory-mcp.exe codebase-memory-mcp.payload.exe \
|
||||
LICENSE install.ps1 THIRD_PARTY_NOTICES.md
|
||||
codebase-memory-mcp.exe LICENSE install.ps1 THIRD_PARTY_NOTICES.md
|
||||
if [ -n "$SUFFIX" ]; then
|
||||
cp "$EXPECTED_ARTIFACT" "codebase-memory-mcp-windows-${SMOKE_ARCH}.zip"
|
||||
fi
|
||||
|
||||
@@ -196,7 +196,7 @@ sync)
|
||||
;;
|
||||
build)
|
||||
# The canonical build entry, exactly as CI runs it: a CLEAN scripts/build.sh
|
||||
# (product binary + launcher). ccache engages through env.sh's masquerade
|
||||
# (the single product binary). ccache engages through env.sh's masquerade
|
||||
# (content-verified, so a warm cache only accelerates, never goes stale) —
|
||||
# NOT via CC='ccache clang', which bypassed the verified-cache env layer.
|
||||
# The test-runner is no longer built here: the test leg (scripts/test.sh)
|
||||
@@ -213,8 +213,8 @@ test)
|
||||
vm clangarm64 "cd /c/cbm && bash test-infrastructure/vm/vm-run-tests.sh $*"
|
||||
;;
|
||||
guards)
|
||||
# Match the Windows CI product build: a clean, embedded-UI payload plus the
|
||||
# permanent launcher. Passing those freshly built artifacts to the maintained
|
||||
# Match the Windows CI product build: a clean, embedded-UI product binary.
|
||||
# Passing that freshly built artifact to the maintained
|
||||
# PowerShell driver prevents an earlier non-UI `win.sh build` from silently
|
||||
# turning product guards into precondition skips. BUILD_DIR isolates the
|
||||
# clean product build from build/c, which build.sh would otherwise wipe —
|
||||
@@ -230,7 +230,7 @@ guards)
|
||||
# different environment shape than CI's profile-rooted TEMP. Python must
|
||||
# be PREPENDED: the Microsoft Store python.exe alias stub lives early in
|
||||
# the profile PATH and otherwise shadows any appended interpreter.
|
||||
vm_cmd "cd /d C:\\cbm && set PATH=C:\\msys64\\clangarm64\\bin;C:\\msys64\\usr\\bin;%PATH%&& powershell -NoProfile -ExecutionPolicy Bypass -File scripts\\test-windows.ps1 -GuardsOnly -Binary build\\guards\\codebase-memory-mcp.exe -Launcher build\\guards\\codebase-memory-mcp-launcher.exe -Make C:\\msys64\\usr\\bin\\make.exe"
|
||||
vm_cmd "cd /d C:\\cbm && set PATH=C:\\msys64\\clangarm64\\bin;C:\\msys64\\usr\\bin;%PATH%&& powershell -NoProfile -ExecutionPolicy Bypass -File scripts\\test-windows.ps1 -GuardsOnly -Binary build\\guards\\codebase-memory-mcp.exe -Make C:\\msys64\\usr\\bin\\make.exe"
|
||||
;;
|
||||
smoke-install)
|
||||
# EXACTLY the PR CI smoke job (pr.yml pr-smoke windows): a clean canonical
|
||||
|
||||
+15
-306
@@ -465,10 +465,10 @@ static cbm_cli_activation_ops_t cli_activation_fake_ops(cli_activation_fake_t *f
|
||||
|
||||
/* Every install/update/uninstall in this suite dispatches through here. On
|
||||
* Windows a test that has not installed its own activation ops gets a default
|
||||
* fake for the duration of the command: without the seam, the portable-payload
|
||||
* gate (correctly) refuses managed mutations before the shared agent-config
|
||||
* logic these tests verify ever runs. POSIX behavior is untouched — tests
|
||||
* without ops keep exercising the real activation machinery. */
|
||||
* fake for the duration of the command: without the seam, `update` (correctly)
|
||||
* hands off to install.ps1 before the shared agent-config logic these tests
|
||||
* verify ever runs. POSIX behavior is untouched — tests without ops keep
|
||||
* exercising the real activation machinery. */
|
||||
static cli_activation_fake_t g_cli_test_seam_fake;
|
||||
static cbm_cli_activation_ops_t g_cli_test_seam_ops;
|
||||
|
||||
@@ -3688,249 +3688,6 @@ static unsigned char *create_test_zip_stored(const char *filename, const unsigne
|
||||
return zip;
|
||||
}
|
||||
|
||||
static void test_zip_put_u16(unsigned char *output, uint16_t value) {
|
||||
output[0] = (unsigned char)value;
|
||||
output[1] = (unsigned char)(value >> 8);
|
||||
}
|
||||
|
||||
static void test_zip_put_u32(unsigned char *output, uint32_t value) {
|
||||
output[0] = (unsigned char)value;
|
||||
output[1] = (unsigned char)(value >> 8);
|
||||
output[2] = (unsigned char)(value >> 16);
|
||||
output[3] = (unsigned char)(value >> 24);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const unsigned char *content;
|
||||
size_t content_size;
|
||||
} test_zip_entry_t;
|
||||
|
||||
static unsigned char *create_test_zip_entries(const test_zip_entry_t *entries, size_t entry_count,
|
||||
int *out_len) {
|
||||
enum { TEST_ZIP_ENTRY_MAX = 8 };
|
||||
if (!entries || !out_len || entry_count == 0 || entry_count > TEST_ZIP_ENTRY_MAX) {
|
||||
return NULL;
|
||||
}
|
||||
size_t local_size = 0;
|
||||
size_t central_size = 0;
|
||||
for (size_t index = 0; index < entry_count; index++) {
|
||||
size_t name_size = strlen(entries[index].name);
|
||||
local_size += 30U + name_size + entries[index].content_size;
|
||||
central_size += 46U + name_size;
|
||||
}
|
||||
size_t total = local_size + central_size + 22U;
|
||||
if (total > INT_MAX) {
|
||||
return NULL;
|
||||
}
|
||||
unsigned char *zip = calloc(1, total);
|
||||
if (!zip) {
|
||||
return NULL;
|
||||
}
|
||||
uint32_t local_offsets[TEST_ZIP_ENTRY_MAX];
|
||||
uint32_t crcs[TEST_ZIP_ENTRY_MAX];
|
||||
size_t cursor = 0;
|
||||
for (size_t index = 0; index < entry_count; index++) {
|
||||
size_t name_size = strlen(entries[index].name);
|
||||
local_offsets[index] = (uint32_t)cursor;
|
||||
crcs[index] =
|
||||
(uint32_t)crc32(0L, entries[index].content, (uInt)entries[index].content_size);
|
||||
zip[cursor] = 0x50;
|
||||
zip[cursor + 1U] = 0x4b;
|
||||
zip[cursor + 2U] = 0x03;
|
||||
zip[cursor + 3U] = 0x04;
|
||||
test_zip_put_u16(zip + cursor + 4U, 20U);
|
||||
test_zip_put_u32(zip + cursor + 14U, crcs[index]);
|
||||
test_zip_put_u32(zip + cursor + 18U, (uint32_t)entries[index].content_size);
|
||||
test_zip_put_u32(zip + cursor + 22U, (uint32_t)entries[index].content_size);
|
||||
test_zip_put_u16(zip + cursor + 26U, (uint16_t)name_size);
|
||||
memcpy(zip + cursor + 30U, entries[index].name, name_size);
|
||||
cursor += 30U + name_size;
|
||||
memcpy(zip + cursor, entries[index].content, entries[index].content_size);
|
||||
cursor += entries[index].content_size;
|
||||
}
|
||||
|
||||
size_t central_offset = cursor;
|
||||
for (size_t index = 0; index < entry_count; index++) {
|
||||
size_t name_size = strlen(entries[index].name);
|
||||
zip[cursor] = 0x50;
|
||||
zip[cursor + 1U] = 0x4b;
|
||||
zip[cursor + 2U] = 0x01;
|
||||
zip[cursor + 3U] = 0x02;
|
||||
test_zip_put_u16(zip + cursor + 4U, 20U);
|
||||
test_zip_put_u16(zip + cursor + 6U, 20U);
|
||||
test_zip_put_u32(zip + cursor + 16U, crcs[index]);
|
||||
test_zip_put_u32(zip + cursor + 20U, (uint32_t)entries[index].content_size);
|
||||
test_zip_put_u32(zip + cursor + 24U, (uint32_t)entries[index].content_size);
|
||||
test_zip_put_u16(zip + cursor + 28U, (uint16_t)name_size);
|
||||
test_zip_put_u32(zip + cursor + 42U, local_offsets[index]);
|
||||
memcpy(zip + cursor + 46U, entries[index].name, name_size);
|
||||
cursor += 46U + name_size;
|
||||
}
|
||||
size_t central_length = cursor - central_offset;
|
||||
zip[cursor] = 0x50;
|
||||
zip[cursor + 1U] = 0x4b;
|
||||
zip[cursor + 2U] = 0x05;
|
||||
zip[cursor + 3U] = 0x06;
|
||||
test_zip_put_u16(zip + cursor + 8U, (uint16_t)entry_count);
|
||||
test_zip_put_u16(zip + cursor + 10U, (uint16_t)entry_count);
|
||||
test_zip_put_u32(zip + cursor + 12U, (uint32_t)central_length);
|
||||
test_zip_put_u32(zip + cursor + 16U, (uint32_t)central_offset);
|
||||
*out_len = (int)total;
|
||||
return zip;
|
||||
}
|
||||
|
||||
static unsigned char *create_test_zip_pair(const test_zip_entry_t entries[2], int *out_len) {
|
||||
return create_test_zip_entries(entries, 2U, out_len);
|
||||
}
|
||||
|
||||
static unsigned char *create_test_windows_release_zip(const char *launcher_name,
|
||||
const char *payload_name, int *out_len) {
|
||||
static const unsigned char launcher[] = "MZ-launcher";
|
||||
static const unsigned char payload[] = "MZ-payload";
|
||||
static const unsigned char license[] = "license";
|
||||
static const unsigned char installer[] = "installer";
|
||||
static const unsigned char notices[] = "notices";
|
||||
test_zip_entry_t entries[5] = {
|
||||
{
|
||||
.name = launcher_name,
|
||||
.content = launcher,
|
||||
.content_size = sizeof(launcher) - 1U,
|
||||
},
|
||||
{
|
||||
.name = payload_name,
|
||||
.content = payload,
|
||||
.content_size = sizeof(payload) - 1U,
|
||||
},
|
||||
{"LICENSE", license, sizeof(license) - 1U},
|
||||
{"install.ps1", installer, sizeof(installer) - 1U},
|
||||
{"THIRD_PARTY_NOTICES.md", notices, sizeof(notices) - 1U},
|
||||
};
|
||||
return create_test_zip_entries(entries, 5U, out_len);
|
||||
}
|
||||
|
||||
TEST(cli_extract_windows_release_pair_rejects_incomplete_release_namespace) {
|
||||
static const unsigned char launcher[] = "MZ-launcher";
|
||||
static const unsigned char payload[] = "MZ-payload";
|
||||
const test_zip_entry_t entries[2] = {
|
||||
{"codebase-memory-mcp.exe", launcher, sizeof(launcher) - 1U},
|
||||
{"codebase-memory-mcp.payload.exe", payload, sizeof(payload) - 1U},
|
||||
};
|
||||
int zip_length = 0;
|
||||
unsigned char *zip = create_test_zip_pair(entries, &zip_length);
|
||||
ASSERT_NOT_NULL(zip);
|
||||
cbm_windows_release_pair_t pair;
|
||||
ASSERT_FALSE(cbm_extract_windows_release_pair_from_zip(zip, zip_length, &pair));
|
||||
cbm_windows_release_pair_free(&pair);
|
||||
free(zip);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Release archives retain their legal notices and the standalone installer.
|
||||
* The updater must accept that exact official namespace while extracting only
|
||||
* the launcher/payload pair. Synthetic two-file fixtures previously hid that
|
||||
* every published Windows update would be rejected. */
|
||||
TEST(cli_extract_windows_release_pair_accepts_official_release_namespace) {
|
||||
static const unsigned char launcher[] = "MZ-launcher";
|
||||
static const unsigned char payload[] = "MZ-payload";
|
||||
static const unsigned char license[] = "license";
|
||||
static const unsigned char installer[] = "installer";
|
||||
static const unsigned char notices[] = "notices";
|
||||
const test_zip_entry_t entries[] = {
|
||||
{"codebase-memory-mcp.exe", launcher, sizeof(launcher) - 1U},
|
||||
{"codebase-memory-mcp.payload.exe", payload, sizeof(payload) - 1U},
|
||||
{"LICENSE", license, sizeof(license) - 1U},
|
||||
{"install.ps1", installer, sizeof(installer) - 1U},
|
||||
{"THIRD_PARTY_NOTICES.md", notices, sizeof(notices) - 1U},
|
||||
};
|
||||
int zip_length = 0;
|
||||
unsigned char *zip =
|
||||
create_test_zip_entries(entries, sizeof(entries) / sizeof(entries[0]), &zip_length);
|
||||
ASSERT_NOT_NULL(zip);
|
||||
cbm_windows_release_pair_t pair;
|
||||
ASSERT_TRUE(cbm_extract_windows_release_pair_from_zip(zip, zip_length, &pair));
|
||||
ASSERT_EQ(pair.launcher_len, 11);
|
||||
ASSERT_EQ(pair.payload_len, 10);
|
||||
ASSERT_MEM_EQ(pair.launcher, "MZ-launcher", 11);
|
||||
ASSERT_MEM_EQ(pair.payload, "MZ-payload", 10);
|
||||
cbm_windows_release_pair_free(&pair);
|
||||
free(zip);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(cli_extract_windows_release_pair_rejects_unknown_release_member) {
|
||||
static const unsigned char content[] = "x";
|
||||
const test_zip_entry_t entries[] = {
|
||||
{"codebase-memory-mcp.exe", content, sizeof(content) - 1U},
|
||||
{"codebase-memory-mcp.payload.exe", content, sizeof(content) - 1U},
|
||||
{"LICENSE", content, sizeof(content) - 1U},
|
||||
{"install.ps1", content, sizeof(content) - 1U},
|
||||
{"unexpected.dll", content, sizeof(content) - 1U},
|
||||
};
|
||||
int zip_length = 0;
|
||||
unsigned char *zip =
|
||||
create_test_zip_entries(entries, sizeof(entries) / sizeof(entries[0]), &zip_length);
|
||||
ASSERT_NOT_NULL(zip);
|
||||
cbm_windows_release_pair_t pair;
|
||||
ASSERT_FALSE(cbm_extract_windows_release_pair_from_zip(zip, zip_length, &pair));
|
||||
cbm_windows_release_pair_free(&pair);
|
||||
free(zip);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(cli_extract_windows_release_pair_rejects_aliases_and_duplicates) {
|
||||
static const struct {
|
||||
const char *launcher;
|
||||
const char *payload;
|
||||
} attacks[] = {
|
||||
{
|
||||
"CODEBASE-MEMORY-MCP.EXE",
|
||||
"codebase-memory-mcp.payload.exe",
|
||||
},
|
||||
{
|
||||
"codebase-memory-mcp.exe",
|
||||
"codebase-memory-mcp.exe",
|
||||
},
|
||||
{
|
||||
".\\codebase-memory-mcp.exe",
|
||||
"codebase-memory-mcp.payload.exe",
|
||||
},
|
||||
{
|
||||
"codebase-memory-mcp.exe.",
|
||||
"codebase-memory-mcp.payload.exe",
|
||||
},
|
||||
{
|
||||
"codebase-memory-mcp.exe",
|
||||
"codebase-memory-mcp.payload.exe ",
|
||||
},
|
||||
};
|
||||
for (size_t index = 0; index < sizeof(attacks) / sizeof(attacks[0]); index++) {
|
||||
int zip_length = 0;
|
||||
unsigned char *zip = create_test_windows_release_zip(attacks[index].launcher,
|
||||
attacks[index].payload, &zip_length);
|
||||
ASSERT_NOT_NULL(zip);
|
||||
cbm_windows_release_pair_t pair;
|
||||
ASSERT_FALSE(cbm_extract_windows_release_pair_from_zip(zip, zip_length, &pair));
|
||||
cbm_windows_release_pair_free(&pair);
|
||||
free(zip);
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(cli_extract_windows_release_pair_rejects_local_central_mismatch) {
|
||||
int zip_length = 0;
|
||||
unsigned char *zip = create_test_windows_release_zip(
|
||||
"codebase-memory-mcp.exe", "codebase-memory-mcp.payload.exe", &zip_length);
|
||||
ASSERT_NOT_NULL(zip);
|
||||
/* Local name starts at offset 30; central metadata remains unchanged. */
|
||||
zip[30] = 'x';
|
||||
cbm_windows_release_pair_t pair;
|
||||
ASSERT_FALSE(cbm_extract_windows_release_pair_from_zip(zip, zip_length, &pair));
|
||||
cbm_windows_release_pair_free(&pair);
|
||||
free(zip);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(cli_extract_binary_from_zip) {
|
||||
const char *content = "#!/bin/sh\necho test\n";
|
||||
int zip_len = 0;
|
||||
@@ -11950,12 +11707,14 @@ TEST(cli_sha256_file_matches_known_vector) {
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
/* The fail-closed release contract, asserted with the activation seam OFF:
|
||||
* these calls take the exact dispatch a release binary ships (the portable
|
||||
* body is unreachable), so the gates themselves keep direct unit coverage. */
|
||||
TEST(cli_windows_release_gates_refuse_portable_mutations) {
|
||||
/* The Windows update contract, asserted with the activation seam OFF so this
|
||||
* takes the exact dispatch a release binary ships: `update` never replaces the
|
||||
* running image in-process (Windows locks it), it prints the install.ps1
|
||||
* command and exits 0. Regressing to an in-process self-update would mean
|
||||
* reintroducing the launcher stub Defender flags as Trojan:Win32/Wacatac.B!ml. */
|
||||
TEST(cli_windows_update_hands_off_to_install_script) {
|
||||
char tmpdir[256];
|
||||
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-portable-refusal-XXXXXX");
|
||||
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-update-handoff-XXXXXX");
|
||||
if (!cbm_mkdtemp(tmpdir)) {
|
||||
FAIL("cbm_mkdtemp failed");
|
||||
}
|
||||
@@ -11972,61 +11731,17 @@ TEST(cli_windows_release_gates_refuse_portable_mutations) {
|
||||
snprintf(bin_dir, sizeof(bin_dir), "%s/.local/bin", tmpdir);
|
||||
test_mkdirp(bin_dir);
|
||||
snprintf(bin_target, sizeof(bin_target), "%s/codebase-memory-mcp.exe", bin_dir);
|
||||
write_test_file(bin_target, "portable refusal must not touch this");
|
||||
write_test_file(bin_target, "in-process update must not touch this");
|
||||
|
||||
/* A one-shot portable payload (this test runner has no launcher context)
|
||||
* may never self-mutate a managed surface. */
|
||||
char *uninstall_argv[] = {"--yes"};
|
||||
int uninstall_rc = cbm_cmd_uninstall(1, uninstall_argv);
|
||||
char *update_argv[] = {"--yes"};
|
||||
int update_rc = cbm_cmd_update(1, update_argv);
|
||||
|
||||
const char *installed = read_test_file(bin_target);
|
||||
bool preserved = installed && strcmp(installed, "portable refusal must not touch this") == 0;
|
||||
bool preserved = installed && strcmp(installed, "in-process update must not touch this") == 0;
|
||||
cli_activation_restore_env(old_home, old_cache);
|
||||
test_rmdir_r(tmpdir);
|
||||
|
||||
ASSERT_EQ(uninstall_rc, 1);
|
||||
ASSERT_EQ(update_rc, 1);
|
||||
ASSERT_TRUE(preserved);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(cli_windows_release_gate_refuses_foreign_install_target) {
|
||||
char tmpdir[256];
|
||||
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-foreign-target-XXXXXX");
|
||||
if (!cbm_mkdtemp(tmpdir)) {
|
||||
FAIL("cbm_mkdtemp failed");
|
||||
}
|
||||
char *old_home = NULL;
|
||||
char *old_cache = NULL;
|
||||
cli_activation_save_env(&old_home, &old_cache);
|
||||
cbm_setenv("HOME", tmpdir, 1);
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/cache", tmpdir);
|
||||
cbm_setenv("CBM_CACHE_DIR", cache_dir, 1);
|
||||
|
||||
char bin_dir[512];
|
||||
char bin_target[640];
|
||||
snprintf(bin_dir, sizeof(bin_dir), "%s/.local/bin", tmpdir);
|
||||
test_mkdirp(bin_dir);
|
||||
snprintf(bin_target, sizeof(bin_target), "%s/codebase-memory-mcp.exe", bin_dir);
|
||||
write_test_file(bin_target, "foreign binary must be preserved");
|
||||
|
||||
/* An unmanaged file at the canonical launcher path is a conflict the
|
||||
* managed transaction refuses to adopt or replace — even under --force
|
||||
* with prompts auto-answered. */
|
||||
cbm_set_auto_answer_for_test(-1);
|
||||
char *argv[] = {"--force", "-y"};
|
||||
int rc = cbm_cmd_install(2, argv);
|
||||
cbm_set_auto_answer_for_test(0);
|
||||
|
||||
const char *installed = read_test_file(bin_target);
|
||||
bool preserved = installed && strcmp(installed, "foreign binary must be preserved") == 0;
|
||||
cli_activation_restore_env(old_home, old_cache);
|
||||
test_rmdir_r(tmpdir);
|
||||
|
||||
ASSERT_EQ(rc, 1);
|
||||
ASSERT_EQ(update_rc, 0);
|
||||
ASSERT_TRUE(preserved);
|
||||
PASS();
|
||||
}
|
||||
@@ -12070,8 +11785,7 @@ SUITE(cli) {
|
||||
RUN_TEST(cli_uninstall_preserves_binary_and_index_when_cohort_does_not_drain);
|
||||
RUN_TEST(cli_activation_guard_is_bypassed_for_dry_run_and_plan);
|
||||
#ifdef _WIN32
|
||||
RUN_TEST(cli_windows_release_gates_refuse_portable_mutations);
|
||||
RUN_TEST(cli_windows_release_gate_refuses_foreign_install_target);
|
||||
RUN_TEST(cli_windows_update_hands_off_to_install_script);
|
||||
#endif
|
||||
|
||||
/* Version (2 tests — selfupdate_test.go) */
|
||||
@@ -12159,11 +11873,6 @@ SUITE(cli) {
|
||||
RUN_TEST(cli_extract_binary_from_targz_not_found);
|
||||
RUN_TEST(cli_extract_binary_from_targz_invalid_data);
|
||||
RUN_TEST(cli_extract_binary_from_zip);
|
||||
RUN_TEST(cli_extract_windows_release_pair_rejects_incomplete_release_namespace);
|
||||
RUN_TEST(cli_extract_windows_release_pair_accepts_official_release_namespace);
|
||||
RUN_TEST(cli_extract_windows_release_pair_rejects_unknown_release_member);
|
||||
RUN_TEST(cli_extract_windows_release_pair_rejects_aliases_and_duplicates);
|
||||
RUN_TEST(cli_extract_windows_release_pair_rejects_local_central_mismatch);
|
||||
RUN_TEST(cli_extract_binary_from_zip_not_found);
|
||||
RUN_TEST(cli_extract_binary_from_zip_path_traversal);
|
||||
RUN_TEST(cli_extract_binary_from_zip_invalid);
|
||||
|
||||
@@ -682,7 +682,6 @@ extern void suite_config_toml_edit(void);
|
||||
extern void suite_config_yaml_edit(void);
|
||||
extern void suite_config_text_edit(void);
|
||||
extern void suite_activation_transaction(void);
|
||||
extern void suite_windows_launcher_state(void);
|
||||
extern void suite_system_info(void);
|
||||
extern void suite_worker_pool(void);
|
||||
extern void suite_parallel(void);
|
||||
@@ -949,7 +948,6 @@ int main(int argc, char **argv) {
|
||||
RUN_SELECTED_SUITE(config_yaml_edit);
|
||||
RUN_SELECTED_SUITE(config_text_edit);
|
||||
RUN_SELECTED_SUITE(activation_transaction);
|
||||
RUN_SELECTED_SUITE(windows_launcher_state);
|
||||
|
||||
/* System info + worker pool (parallelism) */
|
||||
RUN_SELECTED_SUITE(system_info);
|
||||
|
||||
@@ -163,16 +163,20 @@ require(
|
||||
"fixture checksums must name exact artifact basenames, never ./-prefixed paths",
|
||||
)
|
||||
|
||||
# Native Windows packages and serves the exact five-file release bundle, then
|
||||
# runs the full smoke from a protected profile-rooted directory/cache.
|
||||
# Native Windows packages and serves the exact four-file release bundle (ONE
|
||||
# binary, like every other platform), then runs the full smoke from a protected
|
||||
# profile-rooted directory/cache.
|
||||
for name in (
|
||||
"codebase-memory-mcp.exe",
|
||||
"codebase-memory-mcp.payload.exe",
|
||||
"LICENSE",
|
||||
"install.ps1",
|
||||
"THIRD_PARTY_NOTICES.md",
|
||||
):
|
||||
require(name in vm_smoke, f"vm-smoke.sh archive must include {name}")
|
||||
require(
|
||||
"codebase-memory-mcp.payload.exe" not in vm_smoke,
|
||||
"vm-smoke.sh must not stage a Windows launcher/payload pair",
|
||||
)
|
||||
require("checksums.txt" in vm_smoke, "vm-smoke.sh must generate checksums.txt")
|
||||
require(
|
||||
"SMOKE_DOWNLOAD_URL=" in vm_smoke
|
||||
@@ -245,17 +249,21 @@ for service in ("smoke-windows:",):
|
||||
)
|
||||
section = match.group("body") if match else ""
|
||||
require(
|
||||
"mv build/win-cross/codebase-memory-mcp.exe "
|
||||
"build/win-cross/codebase-memory-mcp.payload.exe" in section
|
||||
and "mv build/win-cross/codebase-memory-mcp-launcher.exe "
|
||||
"build/win-cross/codebase-memory-mcp.exe" in section,
|
||||
f"docker-compose {service[:-1]} must assemble the Windows launcher/payload pair",
|
||||
"codebase-memory-mcp-launcher" not in section
|
||||
and "codebase-memory-mcp.payload.exe" not in section.replace(
|
||||
"test ! -e build/win-cross/codebase-memory-mcp.payload.exe", ""
|
||||
),
|
||||
f"docker-compose {service[:-1]} must build ONE Windows binary, not a launcher/payload pair",
|
||||
)
|
||||
require(
|
||||
"test ! -e build/win-cross/codebase-memory-mcp.payload.exe" in section,
|
||||
f"docker-compose {service[:-1]} must assert no payload sibling is produced",
|
||||
)
|
||||
require(
|
||||
"wine64 ./build/win-cross/codebase-memory-mcp.payload.exe --version" in compose
|
||||
"wine64 ./build/win-cross/codebase-memory-mcp.exe --version" in compose
|
||||
and "wine64 cmd /c build/win-cross/codebase-memory-mcp.exe --version" in compose,
|
||||
"docker-compose Windows cross-smoke must execute the payload through Wine and the launcher "
|
||||
"through a Wine Windows parent",
|
||||
"docker-compose Windows cross-smoke must execute the single binary through Wine and through a "
|
||||
"Wine Windows parent",
|
||||
)
|
||||
require(
|
||||
"soak-windows:" not in compose,
|
||||
|
||||
@@ -98,9 +98,8 @@ ALLOWED_CMDS = {
|
||||
# CI plumbing
|
||||
"gh", "python3", "node", "codesign", "xcrun", "command", "awk",
|
||||
}
|
||||
# Per-file additions: _build.yml packages artifacts (its make invocation builds
|
||||
# the launcher exe for the zip — packaging, not a test leg) and drives npm for
|
||||
# the embedded UI; release.yml pushes finished artifacts to registries after
|
||||
# Per-file additions: _build.yml packages artifacts (make/strip are packaging
|
||||
# steps, not test legs) and drives npm for the embedded UI; release.yml pushes finished artifacts to registries after
|
||||
# every product-exercising gate already ran; the brew tap smoke installs the
|
||||
# released formula.
|
||||
ALLOWED_EXTRA = {
|
||||
|
||||
@@ -116,16 +116,15 @@ if ! grep -Fq "JOBS='\$(nproc)'" "$provisioner" ||
|
||||
exit 1
|
||||
fi
|
||||
guards_block="$(sed -n '/^guards)/,/^ ;;/p' "$driver")"
|
||||
# The guards leg builds the CI-shaped product payload (embedded UI + launcher)
|
||||
# into an isolated BUILD_DIR, then hands those artifacts to the maintained
|
||||
# The guards leg builds the CI-shaped product binary (embedded UI) into an
|
||||
# isolated BUILD_DIR, then hands that artifact to the maintained
|
||||
# PowerShell driver through a plain cmd shell (CI's environment shape, not
|
||||
# the MSYS2 login shell whose TMP ancestry the daemon correctly refuses).
|
||||
if ! grep -Fq 'scripts/build.sh --with-ui CC=clang CXX=clang++ SANITIZE= BUILD_DIR=build/guards' \
|
||||
<<<"$guards_block" ||
|
||||
! grep -Fq 'powershell -NoProfile -ExecutionPolicy Bypass -File scripts\\test-windows.ps1' \
|
||||
<<<"$guards_block" ||
|
||||
! grep -Fq -- '-GuardsOnly -Binary build\\guards\\codebase-memory-mcp.exe' <<<"$guards_block" ||
|
||||
! grep -Fq -- '-Launcher build\\guards\\codebase-memory-mcp-launcher.exe' <<<"$guards_block"; then
|
||||
! grep -Fq -- '-GuardsOnly -Binary build\\guards\\codebase-memory-mcp.exe' <<<"$guards_block"; then
|
||||
echo "FAIL: Windows VM guards must delegate to the maintained native-Windows driver" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,450 +0,0 @@
|
||||
/*
|
||||
* test_windows_launcher_state.c — Permanent Windows launcher state contract.
|
||||
*
|
||||
* This suite exercises the stable public byte contract for the managed Windows
|
||||
* layout: a tiny launcher reads one fixed current-v1 record and resolves the
|
||||
* immutable payload generation
|
||||
* relative to its own location. Keep these pure helpers platform-independent
|
||||
* so the record format and command policy are exercised on every CI host.
|
||||
*/
|
||||
#include "test_framework.h"
|
||||
|
||||
#include <cli/windows_launcher_state.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <wchar.h>
|
||||
|
||||
static const char launcher_sha256[] = "0123456789abcdef0123456789abcdef"
|
||||
"0123456789abcdef0123456789abcdef";
|
||||
|
||||
static cbm_windows_current_v1_t launcher_valid_state(void) {
|
||||
cbm_windows_current_v1_t state;
|
||||
memset(&state, 0, sizeof(state));
|
||||
state.launcher_abi_min = 1;
|
||||
state.launcher_abi_max = 3;
|
||||
state.payload_size = UINT64_C(0x0102030405060708);
|
||||
memcpy(state.payload_sha256, launcher_sha256, sizeof(launcher_sha256));
|
||||
return state;
|
||||
}
|
||||
|
||||
static bool launcher_memory_is_zero(const void *memory, size_t size) {
|
||||
const uint8_t *bytes = memory;
|
||||
for (size_t index = 0; index < size; index++) {
|
||||
if (bytes[index] != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void launcher_put_u32_le(uint8_t *out, uint32_t value) {
|
||||
out[0] = (uint8_t)value;
|
||||
out[1] = (uint8_t)(value >> 8);
|
||||
out[2] = (uint8_t)(value >> 16);
|
||||
out[3] = (uint8_t)(value >> 24);
|
||||
}
|
||||
|
||||
static void launcher_put_u64_le(uint8_t *out, uint64_t value) {
|
||||
for (unsigned int index = 0; index < 8U; index++) {
|
||||
out[index] = (uint8_t)(value >> (index * 8U));
|
||||
}
|
||||
}
|
||||
|
||||
static cbm_windows_release_descriptor_v1_t launcher_valid_descriptor(void) {
|
||||
cbm_windows_release_descriptor_v1_t descriptor;
|
||||
memset(&descriptor, 0, sizeof(descriptor));
|
||||
descriptor.launcher_abi = 2U;
|
||||
descriptor.payload_launcher_abi_min = 1U;
|
||||
descriptor.payload_launcher_abi_max = 3U;
|
||||
descriptor.payload_size = UINT64_C(0x0102030405060708);
|
||||
memcpy(descriptor.payload_sha256, launcher_sha256, sizeof(launcher_sha256));
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
TEST(windows_release_descriptor_v1_encoding_is_exact_and_round_trips) {
|
||||
cbm_windows_release_descriptor_v1_t descriptor = launcher_valid_descriptor();
|
||||
uint8_t actual[CBM_WINDOWS_RELEASE_DESCRIPTOR_V1_SIZE];
|
||||
uint8_t expected[CBM_WINDOWS_RELEASE_DESCRIPTOR_V1_SIZE];
|
||||
memset(expected, 0, sizeof(expected));
|
||||
memcpy(expected, "CBMWRD1\0", 8U);
|
||||
launcher_put_u32_le(expected + 8U, 1U);
|
||||
launcher_put_u32_le(expected + 12U, CBM_WINDOWS_RELEASE_DESCRIPTOR_V1_SIZE);
|
||||
launcher_put_u32_le(expected + 16U, descriptor.launcher_abi);
|
||||
launcher_put_u32_le(expected + 20U, descriptor.payload_launcher_abi_min);
|
||||
launcher_put_u32_le(expected + 24U, descriptor.payload_launcher_abi_max);
|
||||
launcher_put_u64_le(expected + 32U, descriptor.payload_size);
|
||||
memcpy(expected + 40U, launcher_sha256, 64U);
|
||||
ASSERT_TRUE(cbm_windows_release_descriptor_v1_encode(&descriptor, actual));
|
||||
ASSERT_MEM_EQ(actual, expected, sizeof(actual));
|
||||
|
||||
cbm_windows_release_descriptor_v1_t decoded;
|
||||
ASSERT_TRUE(cbm_windows_release_descriptor_v1_decode(actual, sizeof(actual), &decoded));
|
||||
ASSERT_EQ(decoded.launcher_abi, descriptor.launcher_abi);
|
||||
ASSERT_EQ(decoded.payload_launcher_abi_min, descriptor.payload_launcher_abi_min);
|
||||
ASSERT_EQ(decoded.payload_launcher_abi_max, descriptor.payload_launcher_abi_max);
|
||||
ASSERT_EQ(decoded.payload_size, descriptor.payload_size);
|
||||
ASSERT_STR_EQ(decoded.payload_sha256, descriptor.payload_sha256);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(windows_release_descriptor_v1_rejects_noncanonical_or_incompatible) {
|
||||
cbm_windows_release_descriptor_v1_t descriptor = launcher_valid_descriptor();
|
||||
uint8_t record[CBM_WINDOWS_RELEASE_DESCRIPTOR_V1_SIZE];
|
||||
ASSERT_TRUE(cbm_windows_release_descriptor_v1_encode(&descriptor, record));
|
||||
record[28] = 1U;
|
||||
ASSERT_FALSE(cbm_windows_release_descriptor_v1_decode(record, sizeof(record), &descriptor));
|
||||
record[28] = 0U;
|
||||
record[104] = 1U;
|
||||
ASSERT_FALSE(cbm_windows_release_descriptor_v1_decode(record, sizeof(record), &descriptor));
|
||||
|
||||
descriptor = launcher_valid_descriptor();
|
||||
descriptor.launcher_abi = 4U;
|
||||
ASSERT_FALSE(cbm_windows_release_descriptor_v1_encode(&descriptor, record));
|
||||
descriptor = launcher_valid_descriptor();
|
||||
descriptor.payload_launcher_abi_min = 0U;
|
||||
ASSERT_FALSE(cbm_windows_release_descriptor_v1_encode(&descriptor, record));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(windows_transition_plan_requires_a_crash_safe_compatibility_bridge) {
|
||||
cbm_windows_current_v1_t current = launcher_valid_state();
|
||||
cbm_windows_release_descriptor_v1_t candidate = launcher_valid_descriptor();
|
||||
|
||||
current.launcher_abi_min = 1U;
|
||||
current.launcher_abi_max = 1U;
|
||||
candidate.launcher_abi = 1U;
|
||||
candidate.payload_launcher_abi_min = 1U;
|
||||
candidate.payload_launcher_abi_max = 1U;
|
||||
ASSERT_EQ(cbm_windows_transition_plan(¤t, &candidate),
|
||||
CBM_WINDOWS_TRANSITION_LAUNCHER_FIRST);
|
||||
|
||||
candidate.launcher_abi = 2U;
|
||||
candidate.payload_launcher_abi_min = 1U;
|
||||
candidate.payload_launcher_abi_max = 2U;
|
||||
ASSERT_EQ(cbm_windows_transition_plan(¤t, &candidate),
|
||||
CBM_WINDOWS_TRANSITION_CURRENT_FIRST);
|
||||
|
||||
current.launcher_abi_min = 1U;
|
||||
current.launcher_abi_max = 2U;
|
||||
candidate.payload_launcher_abi_min = 2U;
|
||||
candidate.payload_launcher_abi_max = 2U;
|
||||
ASSERT_EQ(cbm_windows_transition_plan(¤t, &candidate),
|
||||
CBM_WINDOWS_TRANSITION_LAUNCHER_FIRST);
|
||||
|
||||
current.launcher_abi_min = 1U;
|
||||
current.launcher_abi_max = 1U;
|
||||
ASSERT_EQ(cbm_windows_transition_plan(¤t, &candidate),
|
||||
CBM_WINDOWS_TRANSITION_INCOMPATIBLE);
|
||||
|
||||
current.launcher_abi_min = 1U;
|
||||
current.launcher_abi_max = 3U;
|
||||
candidate.launcher_abi = 4U;
|
||||
candidate.payload_launcher_abi_min = 2U;
|
||||
candidate.payload_launcher_abi_max = 4U;
|
||||
ASSERT_EQ(cbm_windows_transition_plan(¤t, &candidate),
|
||||
CBM_WINDOWS_TRANSITION_INCOMPATIBLE);
|
||||
ASSERT_EQ(cbm_windows_transition_plan(NULL, &candidate), CBM_WINDOWS_TRANSITION_INCOMPATIBLE);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(windows_current_v1_encoding_is_exact_fixed_little_endian_record) {
|
||||
cbm_windows_current_v1_t state = launcher_valid_state();
|
||||
state.launcher_abi_min = UINT32_C(0x01020304);
|
||||
state.launcher_abi_max = UINT32_C(0x05060708);
|
||||
|
||||
uint8_t actual[CBM_WINDOWS_CURRENT_V1_SIZE];
|
||||
memset(actual, 0xa5, sizeof(actual));
|
||||
ASSERT_TRUE(cbm_windows_current_v1_encode(&state, actual));
|
||||
|
||||
uint8_t expected[128] = {0};
|
||||
const uint8_t magic[8] = {'C', 'B', 'M', 'C', 'U', 'R', '1', '\0'};
|
||||
memcpy(expected, magic, sizeof(magic));
|
||||
launcher_put_u32_le(expected + 8, 1);
|
||||
launcher_put_u32_le(expected + 12, 128);
|
||||
launcher_put_u32_le(expected + 16, state.launcher_abi_min);
|
||||
launcher_put_u32_le(expected + 20, state.launcher_abi_max);
|
||||
expected[24] = 0x08;
|
||||
expected[25] = 0x07;
|
||||
expected[26] = 0x06;
|
||||
expected[27] = 0x05;
|
||||
expected[28] = 0x04;
|
||||
expected[29] = 0x03;
|
||||
expected[30] = 0x02;
|
||||
expected[31] = 0x01;
|
||||
memcpy(expected + 32, launcher_sha256, 64);
|
||||
|
||||
ASSERT_EQ(CBM_WINDOWS_CURRENT_V1_SIZE, 128);
|
||||
ASSERT_MEM_EQ(actual, expected, sizeof(expected));
|
||||
ASSERT_TRUE(launcher_memory_is_zero(actual + 96, 32));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(windows_current_v1_round_trip_preserves_authenticated_payload_fields) {
|
||||
cbm_windows_current_v1_t input = launcher_valid_state();
|
||||
uint8_t record[CBM_WINDOWS_CURRENT_V1_SIZE];
|
||||
ASSERT_TRUE(cbm_windows_current_v1_encode(&input, record));
|
||||
|
||||
cbm_windows_current_v1_t output;
|
||||
memset(&output, 0xa5, sizeof(output));
|
||||
ASSERT_TRUE(cbm_windows_current_v1_decode(record, sizeof(record), &output));
|
||||
ASSERT_EQ(output.launcher_abi_min, input.launcher_abi_min);
|
||||
ASSERT_EQ(output.launcher_abi_max, input.launcher_abi_max);
|
||||
ASSERT_EQ(output.payload_size, input.payload_size);
|
||||
ASSERT_STR_EQ(output.payload_sha256, input.payload_sha256);
|
||||
ASSERT_EQ(output.payload_sha256[64], '\0');
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(windows_current_v1_launcher_abi_range_is_inclusive_and_fail_closed) {
|
||||
cbm_windows_current_v1_t state = launcher_valid_state();
|
||||
ASSERT_FALSE(cbm_windows_current_v1_supports_launcher_abi(&state, 0));
|
||||
ASSERT_TRUE(cbm_windows_current_v1_supports_launcher_abi(&state, 1));
|
||||
ASSERT_TRUE(cbm_windows_current_v1_supports_launcher_abi(&state, 2));
|
||||
ASSERT_TRUE(cbm_windows_current_v1_supports_launcher_abi(&state, 3));
|
||||
ASSERT_FALSE(cbm_windows_current_v1_supports_launcher_abi(&state, 4));
|
||||
ASSERT_FALSE(cbm_windows_current_v1_supports_launcher_abi(NULL, 2));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(windows_current_v1_encoder_rejects_noncanonical_state) {
|
||||
cbm_windows_current_v1_t state = launcher_valid_state();
|
||||
uint8_t record[CBM_WINDOWS_CURRENT_V1_SIZE];
|
||||
|
||||
ASSERT_FALSE(cbm_windows_current_v1_encode(NULL, record));
|
||||
ASSERT_FALSE(cbm_windows_current_v1_encode(&state, NULL));
|
||||
|
||||
state.launcher_abi_min = 0;
|
||||
ASSERT_FALSE(cbm_windows_current_v1_encode(&state, record));
|
||||
state = launcher_valid_state();
|
||||
state.launcher_abi_min = 4;
|
||||
state.launcher_abi_max = 3;
|
||||
ASSERT_FALSE(cbm_windows_current_v1_encode(&state, record));
|
||||
state = launcher_valid_state();
|
||||
state.payload_size = 0;
|
||||
ASSERT_FALSE(cbm_windows_current_v1_encode(&state, record));
|
||||
state = launcher_valid_state();
|
||||
state.payload_sha256[0] = 'A';
|
||||
ASSERT_FALSE(cbm_windows_current_v1_encode(&state, record));
|
||||
state = launcher_valid_state();
|
||||
state.payload_sha256[17] = 'g';
|
||||
ASSERT_FALSE(cbm_windows_current_v1_encode(&state, record));
|
||||
state = launcher_valid_state();
|
||||
memset(state.payload_sha256, 'a', sizeof(state.payload_sha256));
|
||||
ASSERT_FALSE(cbm_windows_current_v1_encode(&state, record));
|
||||
PASS();
|
||||
}
|
||||
|
||||
static int launcher_assert_decode_rejected(const uint8_t *record, size_t size) {
|
||||
cbm_windows_current_v1_t output;
|
||||
memset(&output, 0xa5, sizeof(output));
|
||||
if (cbm_windows_current_v1_decode(record, size, &output)) {
|
||||
return 1;
|
||||
}
|
||||
/* A rejected attacker-controlled record must not leave partial state that
|
||||
* a caller could accidentally consume. */
|
||||
return launcher_memory_is_zero(&output, sizeof(output)) ? 0 : 1;
|
||||
}
|
||||
|
||||
TEST(windows_current_v1_decoder_rejects_every_noncanonical_field) {
|
||||
cbm_windows_current_v1_t state = launcher_valid_state();
|
||||
uint8_t valid[CBM_WINDOWS_CURRENT_V1_SIZE + 1];
|
||||
memset(valid, 0, sizeof(valid));
|
||||
ASSERT_TRUE(cbm_windows_current_v1_encode(&state, valid));
|
||||
|
||||
ASSERT_EQ(launcher_assert_decode_rejected(valid, 127), 0);
|
||||
ASSERT_EQ(launcher_assert_decode_rejected(valid, 129), 0);
|
||||
ASSERT_FALSE(cbm_windows_current_v1_decode(NULL, 128, &state));
|
||||
ASSERT_FALSE(cbm_windows_current_v1_decode(valid, 128, NULL));
|
||||
|
||||
uint8_t damaged[CBM_WINDOWS_CURRENT_V1_SIZE];
|
||||
memcpy(damaged, valid, sizeof(damaged));
|
||||
damaged[0] ^= 1;
|
||||
ASSERT_EQ(launcher_assert_decode_rejected(damaged, sizeof(damaged)), 0);
|
||||
|
||||
memcpy(damaged, valid, sizeof(damaged));
|
||||
launcher_put_u32_le(damaged + 8, 2);
|
||||
ASSERT_EQ(launcher_assert_decode_rejected(damaged, sizeof(damaged)), 0);
|
||||
|
||||
memcpy(damaged, valid, sizeof(damaged));
|
||||
launcher_put_u32_le(damaged + 12, 127);
|
||||
ASSERT_EQ(launcher_assert_decode_rejected(damaged, sizeof(damaged)), 0);
|
||||
|
||||
memcpy(damaged, valid, sizeof(damaged));
|
||||
launcher_put_u32_le(damaged + 16, 0);
|
||||
ASSERT_EQ(launcher_assert_decode_rejected(damaged, sizeof(damaged)), 0);
|
||||
|
||||
memcpy(damaged, valid, sizeof(damaged));
|
||||
launcher_put_u32_le(damaged + 16, 4);
|
||||
launcher_put_u32_le(damaged + 20, 3);
|
||||
ASSERT_EQ(launcher_assert_decode_rejected(damaged, sizeof(damaged)), 0);
|
||||
|
||||
memcpy(damaged, valid, sizeof(damaged));
|
||||
memset(damaged + 24, 0, 8);
|
||||
ASSERT_EQ(launcher_assert_decode_rejected(damaged, sizeof(damaged)), 0);
|
||||
|
||||
memcpy(damaged, valid, sizeof(damaged));
|
||||
damaged[32] = 'A';
|
||||
ASSERT_EQ(launcher_assert_decode_rejected(damaged, sizeof(damaged)), 0);
|
||||
|
||||
memcpy(damaged, valid, sizeof(damaged));
|
||||
damaged[63] = 'g';
|
||||
ASSERT_EQ(launcher_assert_decode_rejected(damaged, sizeof(damaged)), 0);
|
||||
|
||||
memcpy(damaged, valid, sizeof(damaged));
|
||||
damaged[96] = 1;
|
||||
ASSERT_EQ(launcher_assert_decode_rejected(damaged, sizeof(damaged)), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(windows_generation_path_is_launcher_relative_for_custom_unicode_directory) {
|
||||
const wchar_t launcher[] =
|
||||
L"D:\\CBM Custom\\M\u00e4rtin\\\u5de5\u5177\\codebase-memory-mcp.exe";
|
||||
const wchar_t expected[] = L"D:\\CBM Custom\\M\u00e4rtin\\\u5de5\u5177\\.cbm\\generations\\"
|
||||
L"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\\"
|
||||
L"codebase-memory-mcp.payload.exe";
|
||||
wchar_t actual[512];
|
||||
ASSERT_TRUE(cbm_windows_generation_payload_path(launcher, launcher_sha256, actual,
|
||||
sizeof(actual) / sizeof(actual[0])));
|
||||
ASSERT_EQ(wcscmp(actual, expected), 0);
|
||||
|
||||
const wchar_t root_launcher[] = L"C:\\codebase-memory-mcp.exe";
|
||||
const wchar_t root_expected[] =
|
||||
L"C:\\.cbm\\generations\\"
|
||||
L"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\\"
|
||||
L"codebase-memory-mcp.payload.exe";
|
||||
ASSERT_TRUE(cbm_windows_generation_payload_path(root_launcher, launcher_sha256, actual,
|
||||
sizeof(actual) / sizeof(actual[0])));
|
||||
ASSERT_EQ(wcscmp(actual, root_expected), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(windows_generation_launcher_path_shares_the_payload_generation) {
|
||||
const wchar_t launcher[] =
|
||||
L"D:\\CBM Custom\\M\u00e4rtin\\\u5de5\u5177\\codebase-memory-mcp.exe";
|
||||
const wchar_t expected[] = L"D:\\CBM Custom\\M\u00e4rtin\\\u5de5\u5177\\.cbm\\generations\\"
|
||||
L"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\\"
|
||||
L"codebase-memory-mcp.exe";
|
||||
wchar_t actual[512];
|
||||
ASSERT_TRUE(cbm_windows_generation_launcher_path(launcher, launcher_sha256, actual,
|
||||
sizeof(actual) / sizeof(actual[0])));
|
||||
ASSERT_EQ(wcscmp(actual, expected), 0);
|
||||
|
||||
const wchar_t root_launcher[] = L"C:\\codebase-memory-mcp.exe";
|
||||
const wchar_t root_expected[] =
|
||||
L"C:\\.cbm\\generations\\"
|
||||
L"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\\"
|
||||
L"codebase-memory-mcp.exe";
|
||||
ASSERT_TRUE(cbm_windows_generation_launcher_path(root_launcher, launcher_sha256, actual,
|
||||
sizeof(actual) / sizeof(actual[0])));
|
||||
ASSERT_EQ(wcscmp(actual, root_expected), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(windows_retired_state_path_is_unique_safe_and_launcher_relative) {
|
||||
const wchar_t launcher[] =
|
||||
L"D:\\CBM Custom\\M\u00e4rtin\\\u5de5\u5177\\codebase-memory-mcp.exe";
|
||||
const wchar_t expected[] =
|
||||
L"D:\\CBM Custom\\M\u00e4rtin\\\u5de5\u5177\\.cbm-retired-v1-"
|
||||
L"0123456789abcdef-4242";
|
||||
wchar_t actual[512];
|
||||
ASSERT_TRUE(cbm_windows_retired_state_path(launcher, launcher_sha256, 4242U, actual,
|
||||
sizeof(actual) / sizeof(actual[0])));
|
||||
ASSERT_EQ(wcscmp(actual, expected), 0);
|
||||
ASSERT_FALSE(cbm_windows_retired_state_path(launcher, launcher_sha256, 0U, actual,
|
||||
sizeof(actual) / sizeof(actual[0])));
|
||||
ASSERT_EQ(actual[0], L'\0');
|
||||
ASSERT_FALSE(cbm_windows_retired_state_path(L"codebase-memory-mcp.exe", launcher_sha256, 4242U,
|
||||
actual, sizeof(actual) / sizeof(actual[0])));
|
||||
ASSERT_EQ(actual[0], L'\0');
|
||||
ASSERT_FALSE(cbm_windows_retired_state_path(launcher, launcher_sha256, 4242U, actual, 32U));
|
||||
ASSERT_EQ(actual[0], L'\0');
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(windows_generation_path_rejects_ambiguous_or_truncated_inputs) {
|
||||
wchar_t output[512];
|
||||
memset(output, 0xa5, sizeof(output));
|
||||
ASSERT_FALSE(cbm_windows_generation_payload_path(L"codebase-memory-mcp.exe", launcher_sha256,
|
||||
output, sizeof(output) / sizeof(output[0])));
|
||||
ASSERT_EQ(output[0], L'\0');
|
||||
|
||||
ASSERT_FALSE(cbm_windows_generation_payload_path(L"C:\\codebase-memory-mcp.exe",
|
||||
launcher_sha256, output, 12));
|
||||
ASSERT_EQ(output[0], L'\0');
|
||||
|
||||
char uppercase[65];
|
||||
memcpy(uppercase, launcher_sha256, sizeof(uppercase));
|
||||
uppercase[0] = 'A';
|
||||
ASSERT_FALSE(cbm_windows_generation_payload_path(L"C:\\codebase-memory-mcp.exe", uppercase,
|
||||
output, sizeof(output) / sizeof(output[0])));
|
||||
ASSERT_EQ(output[0], L'\0');
|
||||
|
||||
ASSERT_FALSE(cbm_windows_generation_launcher_path(L"codebase-memory-mcp.exe", launcher_sha256,
|
||||
output, sizeof(output) / sizeof(output[0])));
|
||||
ASSERT_EQ(output[0], L'\0');
|
||||
ASSERT_FALSE(cbm_windows_generation_launcher_path(L"C:\\codebase-memory-mcp.exe",
|
||||
launcher_sha256, output, 12));
|
||||
ASSERT_EQ(output[0], L'\0');
|
||||
ASSERT_FALSE(cbm_windows_generation_launcher_path(L"C:\\codebase-memory-mcp.exe", uppercase,
|
||||
output, sizeof(output) / sizeof(output[0])));
|
||||
ASSERT_EQ(output[0], L'\0');
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(windows_launcher_action_classifier_matches_top_level_dispatch) {
|
||||
const char *ordinary_mcp[] = {"codebase-memory-mcp.exe"};
|
||||
const char *ordinary_cli[] = {"codebase-memory-mcp.exe", "cli", "search_graph",
|
||||
"{\"query\":\"update\"}"};
|
||||
const char *ordinary_install[] = {"codebase-memory-mcp.exe", "install", "update"};
|
||||
const char *ordinary_help[] = {"codebase-memory-mcp.exe", "--help", "update"};
|
||||
const char *update[] = {"codebase-memory-mcp.exe", "--profile", "update", "--yes"};
|
||||
const char *uninstall[] = {"codebase-memory-mcp.exe", "ignored", "uninstall", "--yes"};
|
||||
const char *first_wins[] = {"codebase-memory-mcp.exe", "update", "uninstall"};
|
||||
|
||||
ASSERT_EQ(cbm_windows_launcher_classify_action(1, ordinary_mcp),
|
||||
CBM_WINDOWS_LAUNCHER_ACTION_ORDINARY);
|
||||
ASSERT_EQ(cbm_windows_launcher_classify_action(4, ordinary_cli),
|
||||
CBM_WINDOWS_LAUNCHER_ACTION_ORDINARY);
|
||||
ASSERT_EQ(cbm_windows_launcher_classify_action(3, ordinary_install),
|
||||
CBM_WINDOWS_LAUNCHER_ACTION_ORDINARY);
|
||||
ASSERT_EQ(cbm_windows_launcher_classify_action(3, ordinary_help),
|
||||
CBM_WINDOWS_LAUNCHER_ACTION_ORDINARY);
|
||||
ASSERT_EQ(cbm_windows_launcher_classify_action(4, update), CBM_WINDOWS_LAUNCHER_ACTION_UPDATE);
|
||||
ASSERT_EQ(cbm_windows_launcher_classify_action(4, uninstall),
|
||||
CBM_WINDOWS_LAUNCHER_ACTION_UNINSTALL);
|
||||
ASSERT_EQ(cbm_windows_launcher_classify_action(3, first_wins),
|
||||
CBM_WINDOWS_LAUNCHER_ACTION_UPDATE);
|
||||
ASSERT_EQ(cbm_windows_launcher_classify_action(0, NULL), CBM_WINDOWS_LAUNCHER_ACTION_ORDINARY);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(windows_portable_payload_rejects_only_managed_mutations) {
|
||||
ASSERT_TRUE(cbm_windows_launcher_action_allowed(CBM_WINDOWS_LAUNCHER_ACTION_ORDINARY, false));
|
||||
ASSERT_FALSE(cbm_windows_launcher_action_allowed(CBM_WINDOWS_LAUNCHER_ACTION_UPDATE, false));
|
||||
ASSERT_FALSE(cbm_windows_launcher_action_allowed(CBM_WINDOWS_LAUNCHER_ACTION_UNINSTALL, false));
|
||||
|
||||
ASSERT_TRUE(cbm_windows_launcher_action_allowed(CBM_WINDOWS_LAUNCHER_ACTION_ORDINARY, true));
|
||||
ASSERT_TRUE(cbm_windows_launcher_action_allowed(CBM_WINDOWS_LAUNCHER_ACTION_UPDATE, true));
|
||||
ASSERT_TRUE(cbm_windows_launcher_action_allowed(CBM_WINDOWS_LAUNCHER_ACTION_UNINSTALL, true));
|
||||
ASSERT_FALSE(cbm_windows_launcher_action_allowed((cbm_windows_launcher_action_t)99, true));
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(windows_launcher_state) {
|
||||
RUN_TEST(windows_release_descriptor_v1_encoding_is_exact_and_round_trips);
|
||||
RUN_TEST(windows_release_descriptor_v1_rejects_noncanonical_or_incompatible);
|
||||
RUN_TEST(windows_transition_plan_requires_a_crash_safe_compatibility_bridge);
|
||||
RUN_TEST(windows_current_v1_encoding_is_exact_fixed_little_endian_record);
|
||||
RUN_TEST(windows_current_v1_round_trip_preserves_authenticated_payload_fields);
|
||||
RUN_TEST(windows_current_v1_launcher_abi_range_is_inclusive_and_fail_closed);
|
||||
RUN_TEST(windows_current_v1_encoder_rejects_noncanonical_state);
|
||||
RUN_TEST(windows_current_v1_decoder_rejects_every_noncanonical_field);
|
||||
RUN_TEST(windows_generation_path_is_launcher_relative_for_custom_unicode_directory);
|
||||
RUN_TEST(windows_generation_launcher_path_shares_the_payload_generation);
|
||||
RUN_TEST(windows_retired_state_path_is_unique_safe_and_launcher_relative);
|
||||
RUN_TEST(windows_generation_path_rejects_ambiguous_or_truncated_inputs);
|
||||
RUN_TEST(windows_launcher_action_classifier_matches_top_level_dispatch);
|
||||
RUN_TEST(windows_portable_payload_rejects_only_managed_mutations);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,211 @@
|
||||
"""GREEN native-Windows guard for the `update` -> install.ps1 handoff.
|
||||
|
||||
Windows ships ONE binary, exactly like Linux and macOS: ``codebase-memory-mcp.exe``.
|
||||
|
||||
There used to be a second, permanently resident launcher stub whose only job
|
||||
was to swap the product binary out from under itself, because a running .exe
|
||||
cannot replace its own image on Windows. Defender's ML scored that stub
|
||||
Trojan:Win32/Wacatac.B!ml on x64 no matter what the build changed (bcrypt-free,
|
||||
stripped, versioned and resource-free variants were all flagged), while the
|
||||
product binary itself scans clean everywhere. So the swap moved OUT of the
|
||||
process into install.ps1, which runs while CBM is NOT running.
|
||||
|
||||
This guard asserts the replacement contract on real native Windows:
|
||||
|
||||
* ``update`` exits 0 and prints the exact install.ps1 command.
|
||||
* ``update`` NEVER replaces the running image in-process — the executable's
|
||||
bytes are unchanged, and no launcher/payload sibling appears next to it.
|
||||
* ``update`` refuses to reach the network first: it hands off before it
|
||||
consults CBM_DOWNLOAD_URL, so it stays fast even when that URL is a black
|
||||
hole.
|
||||
* ``update`` does not disturb an already-open MCP/daemon session.
|
||||
|
||||
A regression here means the in-process self-update — and therefore the
|
||||
AV-flagged launcher stub — came back.
|
||||
|
||||
Exit code: 0 == contract honored, 1 == regression, 2 == precondition failure.
|
||||
|
||||
Usage:
|
||||
python test_windows_update_handoff.py <codebase-memory-mcp.exe>
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from mcp_stdio import McpError, McpServer # noqa: E402
|
||||
|
||||
|
||||
class GuardFailure(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise GuardFailure(message)
|
||||
|
||||
|
||||
def sha256_file(path):
|
||||
return hashlib.sha256(pathlib.Path(path).read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def output_text(result):
|
||||
return ((result.stdout or b"") + b"\n" + (result.stderr or b"")).decode(
|
||||
"utf-8", "replace"
|
||||
)
|
||||
|
||||
|
||||
def run(command, env, timeout=30):
|
||||
try:
|
||||
return subprocess.run(
|
||||
[str(part) for part in command],
|
||||
input=b"",
|
||||
capture_output=True,
|
||||
env=env,
|
||||
timeout=timeout,
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise GuardFailure(
|
||||
"command exceeded %ss: %s" % (timeout, " ".join(map(str, command)))
|
||||
) from exc
|
||||
|
||||
|
||||
def isolated_environment(work):
|
||||
home = work / "home"
|
||||
cache = work / "cache"
|
||||
home.mkdir(parents=True)
|
||||
cache.mkdir(parents=True)
|
||||
env = dict(os.environ)
|
||||
env.update(
|
||||
{
|
||||
"HOME": str(home),
|
||||
"USERPROFILE": str(home),
|
||||
"APPDATA": str(home / "AppData" / "Roaming"),
|
||||
"LOCALAPPDATA": str(home / "AppData" / "Local"),
|
||||
"CBM_CACHE_DIR": str(cache),
|
||||
"PYTHONUTF8": "1",
|
||||
}
|
||||
)
|
||||
return env, cache
|
||||
|
||||
|
||||
def copy_binary(source, directory):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
binary = directory / "codebase-memory-mcp.exe"
|
||||
shutil.copy2(source, binary)
|
||||
return binary
|
||||
|
||||
|
||||
def assert_update_hands_off_to_install_script(source, env, work):
|
||||
binary = copy_binary(source, work / "update-handoff")
|
||||
before = sha256_file(binary)
|
||||
command_env = dict(env)
|
||||
# If the handoff regresses into a real in-process update, keep its
|
||||
# unintended network path deterministic and fast: a correct implementation
|
||||
# never consults this URL.
|
||||
command_env["CBM_DOWNLOAD_URL"] = "https://127.0.0.1:1"
|
||||
|
||||
# Warm the freshly-copied cold binary first: first-touch antivirus scanning
|
||||
# of the just-written image inflates process load time, and the handoff
|
||||
# itself is a fast STATELESS local print (no daemon IPC, no download) whose
|
||||
# timing is what this guard measures. The warm-up behaves identically.
|
||||
run([binary, "update", "--yes", "--standard"], command_env, timeout=20)
|
||||
started = time.monotonic()
|
||||
result = run([binary, "update", "--yes", "--standard"], command_env, timeout=20)
|
||||
elapsed = time.monotonic() - started
|
||||
diagnostic = output_text(result)
|
||||
lowered = diagnostic.lower()
|
||||
|
||||
require(
|
||||
result.returncode == 0,
|
||||
"update exited %s; the Windows handoff must succeed: %s"
|
||||
% (result.returncode, diagnostic[-800:]),
|
||||
)
|
||||
require(
|
||||
"install.ps1" in lowered,
|
||||
"update did not print the install.ps1 command: %s" % diagnostic[-800:],
|
||||
)
|
||||
require(
|
||||
"powershell" in lowered,
|
||||
"update did not print a runnable PowerShell command: %s" % diagnostic[-800:],
|
||||
)
|
||||
require(
|
||||
elapsed < 8.0,
|
||||
"update took %.1fs — it must hand off before any network I/O" % elapsed,
|
||||
)
|
||||
require(
|
||||
sha256_file(binary) == before,
|
||||
"update replaced the running image in-process (a running .exe cannot "
|
||||
"replace itself; that is exactly what the removed launcher stub was for)",
|
||||
)
|
||||
require(
|
||||
not (binary.parent / "codebase-memory-mcp.payload.exe").exists(),
|
||||
"update recreated a launcher/payload pair beside the binary",
|
||||
)
|
||||
require(
|
||||
not list(binary.parent.glob("*launcher*")),
|
||||
"update produced a launcher artifact beside the binary",
|
||||
)
|
||||
print("PASS: update handed off to install.ps1 without touching its own image")
|
||||
|
||||
|
||||
def assert_update_does_not_drain_active_session(source, env, cache, work):
|
||||
binary = copy_binary(source, work / "update-session")
|
||||
with McpServer(str(binary), cache_dir=str(cache), extra_env=env) as server:
|
||||
server.initialize(timeout=30)
|
||||
require(server.tools_list(timeout=30), "MCP control session has no tools")
|
||||
|
||||
command = copy_binary(source, work / "update-session-command")
|
||||
command_env = dict(env)
|
||||
command_env["CBM_DOWNLOAD_URL"] = "https://127.0.0.1:1"
|
||||
result = run([command, "update", "--yes", "--standard"], command_env, timeout=20)
|
||||
require(
|
||||
result.returncode == 0,
|
||||
"update exited %s beside a live session: %s"
|
||||
% (result.returncode, output_text(result)[-800:]),
|
||||
)
|
||||
# The same already-open stdio session must still own the same live
|
||||
# daemon connection. A stop-and-transparent-restart is not enough: the
|
||||
# existing pipe itself has to remain usable.
|
||||
require(
|
||||
server.tools_list(timeout=10),
|
||||
"update drained the active MCP/daemon session",
|
||||
)
|
||||
print("PASS: update left the active MCP/daemon session untouched")
|
||||
|
||||
|
||||
def main():
|
||||
if os.name != "nt":
|
||||
print("PRECONDITION: native Windows is required")
|
||||
return 2
|
||||
if len(sys.argv) != 2:
|
||||
print("usage: python test_windows_update_handoff.py <codebase-memory-mcp.exe>")
|
||||
return 2
|
||||
|
||||
source = pathlib.Path(sys.argv[1]).resolve()
|
||||
if not source.is_file():
|
||||
print("PRECONDITION: binary not found: %s" % source)
|
||||
return 2
|
||||
|
||||
work = pathlib.Path(tempfile.mkdtemp(prefix="cbm_win_update_"))
|
||||
try:
|
||||
env, cache = isolated_environment(work)
|
||||
assert_update_hands_off_to_install_script(source, env, work)
|
||||
assert_update_does_not_drain_active_session(source, env, cache, work)
|
||||
print("\nGREEN: Windows update handoff contract honored.")
|
||||
return 0
|
||||
except (GuardFailure, McpError, OSError, subprocess.SubprocessError) as exc:
|
||||
print("\nRED: %s" % exc)
|
||||
return 1
|
||||
finally:
|
||||
shutil.rmtree(work, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user