revert(release): re-embed runtime assets into the single shipped binary

Externalizing the integration templates (#1492/#1493) and the UI bundle
(#1501/#1503) was done to reduce the Microsoft `Wacatac.B!ml` surface. It did
not work: across dry runs the flagged artifact count stayed at ~3 and the
detections merely moved between artifacts.

Dissection of run 31286803592 shows there is no structural cause to fix. The
verdicts split across every axis at once — linux-amd64 (dynamic) flagged while
linux-amd64-portable (static) is clean, but linux-arm64 (dynamic) clean while
linux-arm64-portable (static) is flagged. The two macOS binaries have identical
segment structure and split clean/flagged. Siblings from one build landed in
different variant buckets (.B vs .C). Entropy is low everywhere
(code_vectors.bin 4.166, grammar tables 3.464 bits/byte, against 7.5-8.0 for
packed payloads), so the packed-payload hypothesis is excluded too.

So the complexity bought nothing, and installation goes back to being
self-contained: one binary that carries its own UI and agent integration
templates, with no adjacent data file that has to resolve before `install`
works. Only the UI-capable composition ships from now on, under the historical
unsuffixed archive name.

Removed: src/ui/asset_pack.{c,h}, asset_pack_stub.c, asset_manifest_stub.c,
scripts/pack-ui-assets.mjs, src/cli/integration_assets.{c,h},
assets/cbm-integrations.json, scripts/gen-integrations-hash.sh, the
--verify-runtime-assets probe (nothing adjacent left to verify), and the
composition gates A6/A7 whose property is now deliberately inverted.

Restored: scripts/embed-frontend.sh, src/ui/embedded_{assets.h,stub.c}, the
compiled-in hook/adapter template bodies, and the embed/EMBED_OBJS build path.

Kept from the reverted commits, re-applied by hand where a wholesale file
restore would have dropped them:
  - cbm_module_path_utf8() in both self-path sites. GetModuleFileNameA renders
    through the ANSI code page and mangles non-ASCII install paths.
  - the /__cbm/ui-readiness HMAC proof, secure_random and cbm_hmac_sha256, so
    `daemon start --open` still waits for a genuine CBM listener.
  - X-Content-Type-Options: nosniff on served assets.
  - the MinGW noexecstack gate, -lbcrypt, and the cppcheck/zip CI fixes.

Archives are now codebase-memory-mcp-<os>-<arch>[-portable] with exactly four
members (binary, LICENSE, installer, THIRD_PARTY_NOTICES.md). That restores the
names every static package manifest already points at — aur, chocolatey,
homebrew, scoop, winget and glama were all broken by the -ui- rename.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-08-09 13:06:42 +02:00
parent 10cb0e03fb
commit d58afe562d
58 changed files with 1560 additions and 11440 deletions
+8 -8
View File
@@ -85,13 +85,13 @@ jobs:
# Archive layout/name live in the ONE canonical script the local
# artifact-flow smoke lane also runs (venue-parity contract).
- name: Archive release binary (canonical package-release.sh)
run: scripts/package-release.sh ${{ matrix.goos }} ${{ matrix.goarch }} --variant ui
run: scripts/package-release.sh ${{ matrix.goos }} ${{ matrix.goarch }}
- name: Attest release binary provenance
if: ${{ inputs.attest }}
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: codebase-memory-mcp-ui-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
subject-path: codebase-memory-mcp-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
@@ -133,13 +133,13 @@ jobs:
# artifact-flow smoke lane also runs (venue-parity contract).
- name: Archive release binary (canonical package-release.sh)
shell: msys2 {0}
run: scripts/package-release.sh windows amd64 --variant ui CC=clang CXX=clang++
run: scripts/package-release.sh windows amd64 CC=clang CXX=clang++
- name: Attest release binary provenance
if: ${{ inputs.attest }}
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: codebase-memory-mcp-ui-windows-amd64.zip
subject-path: codebase-memory-mcp-windows-amd64.zip
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
@@ -183,13 +183,13 @@ jobs:
- name: Archive release binary (canonical package-release.sh)
shell: msys2 {0}
run: scripts/package-release.sh windows arm64 --variant ui CC=clang CXX=clang++
run: scripts/package-release.sh windows arm64 CC=clang CXX=clang++
- name: Attest release binary provenance
if: ${{ inputs.attest }}
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: codebase-memory-mcp-ui-windows-arm64.zip
subject-path: codebase-memory-mcp-windows-arm64.zip
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
@@ -235,13 +235,13 @@ jobs:
ldd build/c/codebase-memory-mcp 2>&1 | grep -q "not a dynamic executable" || ldd build/c/codebase-memory-mcp 2>&1 | grep -q "statically linked"
- name: Archive release binary (canonical package-release.sh)
run: scripts/package-release.sh linux ${{ matrix.arch }}-portable --variant ui
run: scripts/package-release.sh linux ${{ matrix.arch }}-portable
- name: Attest release binary provenance
if: ${{ inputs.attest }}
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: codebase-memory-mcp-ui-linux-${{ matrix.arch }}-portable.tar.gz
subject-path: codebase-memory-mcp-linux-${{ matrix.arch }}-portable.tar.gz
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
+23 -53
View File
@@ -358,19 +358,17 @@ GIT_SRCS = src/git/git_context.c
# CLI module (new)
CLI_SRCS = src/cli/cli.c src/cli/progress_sink.c src/cli/hook_augment.c src/cli/client_adapter.c \
src/cli/agent_clients.c src/cli/agent_profiles.c src/cli/integration_assets.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
# UI module (graph visualization). Production-standard builds deliberately link
# only the no-I/O pack stub. Test binaries substitute the real parser plus the
# empty manifest so parser/install behavior remains covered without a UI pack.
# UI module (graph visualization)
UI_SRCS = \
src/ui/config.c \
src/ui/http_server.c \
src/ui/layout3d.c \
src/ui/httpd.c \
src/ui/asset_pack_stub.c
src/ui/embedded_stub.c
# mimalloc (vendored, global allocator override)
#
@@ -458,7 +456,6 @@ YYJSON_SRC = vendored/yyjson/yyjson.c
# All production sources
PROD_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) $(GIT_SRCS) $(CLI_SRCS) $(UI_SRCS) $(YYJSON_SRC)
TEST_PROD_SRCS = $(subst src/ui/asset_pack_stub.c,src/ui/asset_pack.c src/ui/asset_manifest_stub.c,$(PROD_SRCS))
EXISTING_C_SRCS = $(EXTRACTION_SRCS) $(LSP_SRCS) $(TS_RUNTIME_SRC) \
$(GRAMMAR_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC)
@@ -476,17 +473,6 @@ EXISTING_C_SRCS = $(EXTRACTION_SRCS) $(LSP_SRCS) $(TS_RUNTIME_SRC) \
PROJECT_HDRS = $(wildcard src/*.h src/*/*.h $(CBM_DIR)/*.h $(CBM_DIR)/lsp/*.h tests/*.h \
tests/repro/*.h)
# ── Integration-asset integrity constant (generated, never hand-edited) ──
# The binary embeds exactly ONE fact about the shipped integration templates:
# the SHA-256 of assets/cbm-integrations.json, generated at build time (rule
# below the BUILD_DIR definition; target lines expand immediately). Appended
# to PROJECT_HDRS so every single-invocation target (test-runner,
# test-repro-runner, test-runner-tsan, codebase-memory-mcp) rebuilds when the
# asset changes — a stale constant would make every install fail verification.
INTEGRATIONS_ASSET = assets/cbm-integrations.json
INTEGRATIONS_HASH_HDR = $(BUILD_DIR)/generated/cbm_integrations_hash.h
PROJECT_HDRS += $(INTEGRATIONS_HASH_HDR)
# ── Test sources ─────────────────────────────────────────────────
TEST_FOUNDATION_SRCS = \
@@ -702,11 +688,6 @@ ALL_TEST_SRCS =$(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS
BUILD_DIR = build/c
# Integration-asset hash header (variables defined next to PROJECT_HDRS; the
# rule lives here because target lines expand $(BUILD_DIR) immediately).
$(INTEGRATIONS_HASH_HDR): $(INTEGRATIONS_ASSET) scripts/gen-integrations-hash.sh | $(BUILD_DIR)
sh scripts/gen-integrations-hash.sh $(INTEGRATIONS_ASSET) $@
# ── Object file compilation (grammars need relaxed warnings) ─────
# Grammar + tree-sitter runtime: compiled without -Werror (upstream code has warnings)
@@ -728,7 +709,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-with-ui frontend ui-pack 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)
@@ -871,9 +852,9 @@ $(UNIXCODER_OBJ): $(UNIXCODER_BLOB_SRC) vendored/nomic/code_vectors.bin | $(BUIL
OBJS_VENDORED_TEST = $(MIMALLOC_OBJ_TEST) $(SQLITE3_OBJ_TEST) $(TRE_OBJ_TEST) $(GRAMMAR_OBJS_TEST) $(TS_RUNTIME_OBJ_TEST) $(LSP_OBJ_TEST) $(PP_OBJ_TEST) $(LZ4_OBJ_TEST) $(ZSTD_OBJ_TEST) $(UNIXCODER_OBJ)
OBJS_VENDORED_TSAN = $(MIMALLOC_OBJ_TSAN) $(SQLITE3_OBJ_TSAN) $(TRE_OBJ_TSAN) $(GRAMMAR_OBJS_TSAN) $(TS_RUNTIME_OBJ_TSAN) $(LSP_OBJ_TSAN) $(PP_OBJ_TSAN) $(LZ4_OBJ_TSAN) $(ZSTD_OBJ_TSAN) $(UNIXCODER_OBJ)
$(BUILD_DIR)/test-runner: $(ALL_TEST_SRCS) $(TEST_PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) $(PROJECT_HDRS) | $(BUILD_DIR)
$(BUILD_DIR)/test-runner: $(ALL_TEST_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) $(PROJECT_HDRS) | $(BUILD_DIR)
$(CC) $(CFLAGS_TEST) -Itests -Itests/repro -o $@ \
$(ALL_TEST_SRCS) $(TEST_PROD_SRCS) \
$(ALL_TEST_SRCS) $(PROD_SRCS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
$(OBJS_VENDORED_TEST) \
$(LDFLAGS_TEST)
@@ -899,9 +880,9 @@ test-focused: $(BUILD_DIR)/test-runner
# Mirrors test-runner's link line but uses repro_main.c (own main + counters)
# and TEST_REPRO_SRCS instead of ALL_TEST_SRCS. Exits non-zero while any bug is
# still reproduced (the expected state); bug-repro.yml surfaces it as a board.
$(BUILD_DIR)/test-repro-runner: $(TEST_REPRO_SRCS) $(TEST_PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) $(PROJECT_HDRS) | $(BUILD_DIR)
$(BUILD_DIR)/test-repro-runner: $(TEST_REPRO_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) $(PROJECT_HDRS) | $(BUILD_DIR)
$(CC) $(CFLAGS_TEST) -Itests -o $@ \
$(TEST_REPRO_SRCS) $(TEST_PROD_SRCS) \
$(TEST_REPRO_SRCS) $(PROD_SRCS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
$(OBJS_VENDORED_TEST) \
$(LDFLAGS_TEST)
@@ -959,9 +940,9 @@ TSAN_OPTIONS ?= halt_on_error=1:report_thread_leaks=0
# Normal ASan/native/soak paths remain uncapped. High-worker TSan diagnostics
# invoke test-runner-tsan directly so release gates cannot drift accidentally.
$(BUILD_DIR)/test-runner-tsan: $(ALL_TEST_SRCS) $(TEST_PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TSAN) $(PROJECT_HDRS) | $(BUILD_DIR)
$(BUILD_DIR)/test-runner-tsan: $(ALL_TEST_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TSAN) $(PROJECT_HDRS) | $(BUILD_DIR)
$(CC) $(CFLAGS_TSAN) -Itests -Itests/repro -o $@ \
$(ALL_TEST_SRCS) $(TEST_PROD_SRCS) \
$(ALL_TEST_SRCS) $(PROD_SRCS) \
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
$(OBJS_VENDORED_TSAN) \
$(LDFLAGS_TSAN)
@@ -1048,40 +1029,29 @@ $(BUILD_DIR)/codebase-memory-mcp: $(MAIN_SRC) $(PROD_SRCS) $(EXTRACTION_SRCS) $(
cbm: $(BUILD_DIR)/codebase-memory-mcp
@echo "Built: $(BUILD_DIR)/codebase-memory-mcp"
# ── Build with external UI asset pack (requires Node.js) ────────
# ── Build with embedded UI (requires Node.js) ───────────────────
# The native image contains only a generated manifest (pack basename, SHA-256
# and byte length). Frontend bytes stay in a content-addressed sidecar next to
# the binary so release scanners can inspect them independently.
UI_ASSET_MANIFEST = $(BUILD_DIR)/generated/ui_asset_manifest.c
UI_ASSET_DIST ?= graph-ui/dist
UI_ASSET_PREBUILT ?= 0
ifeq ($(UI_ASSET_PREBUILT),1)
UI_ASSET_BUILD_PREREQ :=
else
UI_ASSET_BUILD_PREREQ := frontend
endif
PROD_SRCS_WITH_ASSETS = $(subst src/ui/asset_pack_stub.c,src/ui/asset_pack.c $(UI_ASSET_MANIFEST),$(PROD_SRCS))
# Swap embedded_stub.c for the generated embedded_assets.c
UI_SRCS_WITH_ASSETS = $(subst src/ui/embedded_stub.c,src/ui/embedded_assets.c,$(UI_SRCS))
PROD_SRCS_WITH_ASSETS = $(subst src/ui/embedded_stub.c,src/ui/embedded_assets.c,$(PROD_SRCS))
# Embedded asset object files (generated by embed script)
EMBED_OBJS = $(wildcard $(BUILD_DIR)/embedded/embed_*.o)
frontend:
cd graph-ui && npm ci && npm run build
$(UI_ASSET_MANIFEST): $(UI_ASSET_BUILD_PREREQ) scripts/pack-ui-assets.mjs | $(BUILD_DIR)
node scripts/pack-ui-assets.mjs $(UI_ASSET_DIST) $(BUILD_DIR) $@
embed: frontend
scripts/embed-frontend.sh graph-ui/dist $(BUILD_DIR)/embedded
ui-pack: $(UI_ASSET_MANIFEST)
# $(INTEGRATIONS_HASH_HDR): generated header included by integration_assets.c.
# The standard target gets it via $(PROJECT_HDRS); the UI target compiles in one
# shot and must list it too, or the build fails with "cbm_integrations_hash.h
# file not found" (seen on the Windows UI build).
cbm-with-ui: ui-pack $(INTEGRATIONS_HASH_HDR) $(OBJS_VENDORED_PROD)
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) \
$(OBJS_VENDORED_PROD) \
$(wildcard $(BUILD_DIR)/embedded/embed_*.o) \
$(LDFLAGS)
@echo "Built with UI asset pack: $(BUILD_DIR)/codebase-memory-mcp"
@echo "Built with UI: $(BUILD_DIR)/codebase-memory-mcp"
clean-c: export BUILD_DIR := $(BUILD_DIR)
clean-c:
@@ -1114,7 +1084,7 @@ lint-tidy:
@$(CLANG_TIDY) --quiet $(LINT_SRCS) -- $(CFLAGS_COMMON) $(SYSROOT_FLAG)
# cppcheck: complementary analysis (config in .cppcheck)
lint-cppcheck: $(INTEGRATIONS_HASH_HDR)
lint-cppcheck:
@echo "=== cppcheck ==="
@$(CPPCHECK) --enable=warning,style,performance,portability \
--std=c11 --language=c \
+8 -12
View File
@@ -66,7 +66,7 @@ License summary:
- **Copyright:** (c) 2025 InterSystems Corporation
- **Vendored at:** `internal/cbm/vendored/grammars/objectscript_udl/`, `internal/cbm/vendored/grammars/objectscript_routine/`
- **Pinned commit:** `a7ffcdf`
- **Notes:** InterSystems-maintained grammar for the ObjectScript language (InterSystems IRIS / Caché). Vendor-maintained; not in nvim-treesitter or Helix registries. Each `scanner.c`'s upstream `#include "../../common/scanner.h"` is repointed to a per-directory `objectscript_common.h` copied from upstream `common/scanner.h`; two loop counters in that copy are widened from `uint8_t` to `int` as documented in `internal/cbm/vendored/grammars/MANIFEST.md`.
- **Notes:** InterSystems-maintained grammar for the ObjectScript language (InterSystems IRIS / Caché). Vendor-maintained; not in nvim-treesitter or Helix registries. Each `scanner.c`'s upstream `#include "../../common/scanner.h"` is repointed to a per-directory `objectscript_common.h` (verbatim copy of upstream `common/scanner.h`).
## Vendored C/C++ Libraries
@@ -142,17 +142,13 @@ follows:
documentation and language specifications; no upstream source code was
extracted or transcribed.
## External Graph UI asset pack
## Embedded Graph UI
Release builds made with `--with-ui` ship the compiled `graph-ui/` frontend in
a deterministic, content-addressed `cbm-ui-<sha256>.pack` beside the native
executable. The executable contains only the expected pack name, size, and
SHA-256; it does not contain the HTML, JavaScript, or CSS payload.
The pack's npm dependencies (React, three.js, @react-three/*, radix-ui,
lucide-react, tailwindcss, and friends) are all under permissive licenses
(MIT / ISC / Apache-2.0 / Zlib). The exact set is recorded in
Release binaries built with `--with-ui` embed the compiled `graph-ui/`
frontend bundle. Its npm dependencies (React, three.js, @react-three/*,
radix-ui, lucide-react, tailwindcss, and friends) are all under permissive
licenses (MIT / ISC / Apache-2.0 / Zlib); the exact set is recorded in
`graph-ui/package.json` and `graph-ui/package-lock.json`, and the per-package
license texts of the production bundle are appended to the
`THIRD_PARTY_NOTICES.md` shipped inside each `-ui` release archive (generated
by `scripts/gen-ui-licenses.py`).
`THIRD_PARTY_NOTICES.md` shipped inside the `-ui` release archives
(generated by `scripts/gen-ui-licenses.py`).
File diff suppressed because one or more lines are too long
+34 -122
View File
@@ -16,12 +16,10 @@ $InstallDir = "$env:LOCALAPPDATA\Programs\codebase-memory-mcp"
$BinName = "codebase-memory-mcp.exe"
$WindowsArchiveNames = @(
$BinName,
"cbm-integrations.json",
"LICENSE",
"install.ps1",
"THIRD_PARTY_NOTICES.md"
)
$UiPackPattern = '^cbm-ui-[0-9a-f]{64}\.pack$'
$BaseUrl = if ($env:CBM_DOWNLOAD_URL) { $env:CBM_DOWNLOAD_URL } else { "https://github.com/$Repo/releases/latest/download" }
try { $BaseUri = [Uri]$BaseUrl } catch { $BaseUri = $null }
@@ -89,56 +87,8 @@ function Invoke-CbmDownload {
}
}
function New-CbmExclusiveSiblingTemp {
param([Parameter(Mandatory=$true)][string]$Destination)
$directory = [System.IO.Path]::GetDirectoryName($Destination)
$leaf = [System.IO.Path]::GetFileName($Destination)
for ($attempt = 0; $attempt -lt 32; $attempt++) {
$random = [System.IO.Path]::GetRandomFileName()
$candidate = Join-Path $directory ".$leaf.tmp-$random"
try {
$reservation = [System.IO.File]::Open(
$candidate,
[System.IO.FileMode]::CreateNew,
[System.IO.FileAccess]::Write,
[System.IO.FileShare]::None)
$reservation.Dispose()
return $candidate
} catch [System.IO.IOException] {
# A collision belongs to someone else. Never remove it; choose a
# fresh unpredictable sibling and reserve that path exclusively.
}
}
throw "could not reserve an exclusive temporary sibling for $Destination"
}
function New-CbmExclusiveTempDirectory {
param([Parameter(Mandatory=$true)][string]$ParentDirectory)
for ($attempt = 0; $attempt -lt 32; $attempt++) {
$candidate = Join-Path $ParentDirectory (
"cbm-install-" + [guid]::NewGuid().ToString("N")
)
try {
# Without -Force, an existing path is never adopted. Only return a
# directory successfully created by this installer invocation.
New-Item -ItemType Directory -Path $candidate -ErrorAction Stop | Out-Null
return $candidate
} catch [System.IO.IOException] {
# A collision belongs to someone else. Never remove it; choose a
# fresh unpredictable name and try again within the fixed bound.
}
}
throw "could not reserve an exclusive installer temporary directory"
}
# Detect variant from args (--ui or --standard)
$Variant = "standard"
$SkipConfig = $false
foreach ($arg in $args) {
if ($arg -eq "--ui") { $Variant = "ui" }
if ($arg -eq "--standard") { $Variant = "standard" }
if ($arg -eq "--skip-config") { $SkipConfig = $true }
if ($arg -like "--dir=*") { $InstallDir = $arg.Substring(6) }
}
@@ -166,21 +116,17 @@ if ($env:CBM_ARCH) {
}
Write-Host "codebase-memory-mcp installer (Windows)"
Write-Host " variant: $Variant"
Write-Host " arch: $Arch"
Write-Host " target: $InstallDir\$BinName"
Write-Host ""
# Build download URL
if ($Variant -eq "ui") {
$Archive = "codebase-memory-mcp-ui-windows-$Arch.zip"
} else {
$Archive = "codebase-memory-mcp-windows-$Arch.zip"
}
$Archive = "codebase-memory-mcp-windows-$Arch.zip"
$Url = "$BaseUrl/$Archive"
# Download
$TmpDir = New-CbmExclusiveTempDirectory -ParentDirectory ([System.IO.Path]::GetTempPath())
$TmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "cbm-install-$(Get-Random)"
New-Item -ItemType Directory -Path $TmpDir -Force | Out-Null
Write-Host "Downloading $Archive..."
try {
@@ -233,7 +179,7 @@ try {
# Validate the zip namespace before extraction. Windows paths are
# case-insensitive, so two entries that differ only in case are ambiguous and
# must never be allowed to overwrite each other. The official five entries are
# required at the archive root; UI adds exactly one hash-shaped pack.
# required at the archive root with their exact release names.
try {
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [System.IO.Compression.ZipFile]::OpenRead("$TmpDir\$Archive")
@@ -241,8 +187,6 @@ try {
$seen = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
$archiveCounts = @{}
foreach ($archiveName in $WindowsArchiveNames) { $archiveCounts[$archiveName] = 0 }
$uiPackName = $null
$uiPackCount = 0
foreach ($entry in $zip.Entries) {
$entryName = $entry.FullName.Replace('\', '/')
$isDirectory = $entryName.EndsWith('/')
@@ -260,28 +204,18 @@ try {
if (-not $seen.Add($pathForSegments)) {
throw "duplicate or case-conflicting zip entry: $($entry.FullName)"
}
if ($isDirectory) {
throw "archive contains an unexpected root entry: $($entry.FullName)"
}
if ($WindowsArchiveNames -ccontains $entryName) {
$archiveCounts[$entryName] = $archiveCounts[$entryName] + 1
} elseif ($Variant -eq "ui" -and $entryName -cmatch $UiPackPattern) {
$uiPackName = $entryName
$uiPackCount++
} else {
if (-not ($WindowsArchiveNames -ccontains $entryName) -or $isDirectory) {
throw "archive contains an unexpected root entry: $($entry.FullName)"
}
$archiveCounts[$entryName] = $archiveCounts[$entryName] + 1
}
foreach ($archiveName in $WindowsArchiveNames) {
if ($archiveCounts[$archiveName] -ne 1) {
throw "archive must contain exactly one $archiveName"
}
}
$expectedUiPackCount = if ($Variant -eq "ui") { 1 } else { 0 }
$expectedArchiveCount = $WindowsArchiveNames.Count + $expectedUiPackCount
if ($uiPackCount -ne $expectedUiPackCount -or
$seen.Count -ne $expectedArchiveCount) {
throw "archive does not match the exact $Variant Windows release allowlist"
if ($seen.Count -ne $WindowsArchiveNames.Count) {
throw "archive does not match the exact Windows release allowlist"
}
} finally {
$zip.Dispose()
@@ -298,21 +232,6 @@ try {
Write-Host "Extracting..."
Expand-Archive -Path "$TmpDir\$Archive" -DestinationPath $TmpDir -Force
foreach ($archiveName in $WindowsArchiveNames) {
$extractedMember = Join-Path $TmpDir $archiveName
if (-not (Test-Path -LiteralPath $extractedMember -PathType Leaf)) {
Write-Host "error: release member is not a regular file: $archiveName" -ForegroundColor Red
Remove-Item -Recurse -Force $TmpDir
exit 1
}
$extractedItem = Get-Item -LiteralPath $extractedMember
if ($extractedItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) {
Write-Host "error: refusing reparse-point release member: $archiveName" -ForegroundColor Red
Remove-Item -Recurse -Force $TmpDir
exit 1
}
}
$DownloadedBinary = Join-Path $TmpDir $BinName
if (-not (Test-Path -LiteralPath $DownloadedBinary -PathType Leaf)) {
Write-Host "error: $BinName not found after extraction" -ForegroundColor Red
@@ -326,29 +245,6 @@ if ($binaryItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) {
exit 1
}
$DownloadedUiPack = $null
if ($Variant -eq "ui") {
$DownloadedUiPack = Join-Path $TmpDir $uiPackName
if (-not (Test-Path -LiteralPath $DownloadedUiPack -PathType Leaf)) {
Write-Host "error: UI asset pack not found after extraction" -ForegroundColor Red
Remove-Item -Recurse -Force $TmpDir
exit 1
}
$packItem = Get-Item -LiteralPath $DownloadedUiPack
if ($packItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) {
Write-Host "error: refusing reparse-point UI asset pack" -ForegroundColor Red
Remove-Item -Recurse -Force $TmpDir
exit 1
}
$expectedPackDigest = $uiPackName.Substring(7, 64)
$actualPackDigest = (Get-FileHash -LiteralPath $DownloadedUiPack -Algorithm SHA256).Hash.ToLower()
if ($expectedPackDigest -cne $actualPackDigest) {
Write-Host "error: UI asset pack digest does not match its filename" -ForegroundColor Red
Remove-Item -Recurse -Force $TmpDir
exit 1
}
}
# Prove the downloaded binary runs before touching an existing installation.
try {
$candidateVersion = & $DownloadedBinary --version 2>&1
@@ -360,12 +256,32 @@ try {
exit 1
}
# The candidate publishes the runtime set under one native activation guard,
# with sidecars before the executable and retained per-file backups for
# cooperative rollback. This is recoverable/fail-closed ordering, not a claim
# that several filesystem entries change in one crash-atomic transaction.
$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" }
& $DownloadedBinary @InstallArgs
@@ -386,17 +302,13 @@ if ($LASTEXITCODE -ne 0) {
$DownloadedInstaller = Join-Path $TmpDir "install.ps1"
if (Test-Path -LiteralPath $DownloadedInstaller -PathType Leaf) {
$InstallerDest = Join-Path $InstallDir "install.ps1"
$InstallerTmp = $null
$InstallerTmp = "$InstallerDest.new"
try {
$InstallerTmp = New-CbmExclusiveSiblingTemp -Destination $InstallerDest
Copy-Item -LiteralPath $DownloadedInstaller -Destination $InstallerTmp -Force -ErrorAction Stop
Move-Item -LiteralPath $InstallerTmp -Destination $InstallerDest -Force -ErrorAction Stop
$InstallerTmp = $null
Write-Host "Installed updater -> $InstallerDest"
} catch {
if ($InstallerTmp) {
Remove-Item -LiteralPath $InstallerTmp -Force -ErrorAction SilentlyContinue
}
Remove-Item -LiteralPath $InstallerTmp -Force -ErrorAction SilentlyContinue
Write-Host "note: could not place install.ps1 in $InstallDir (update will explain where to find it)"
}
}
+7 -128
View File
@@ -5,7 +5,6 @@ set -euo pipefail
#
# Usage:
# curl -fsSL https://raw.githubusercontent.com/DeusData/codebase-memory-mcp/main/install.sh | bash
# curl -fsSL ... | bash -s -- --ui # Install the UI variant
# curl -fsSL ... | bash -s -- --dir /path # Custom install directory
#
# Environment:
@@ -19,7 +18,6 @@ main() {
REPO="DeusData/codebase-memory-mcp"
INSTALL_DIR="$HOME/.local/bin"
VARIANT="standard"
SKIP_CONFIG=false
CBM_DOWNLOAD_URL="${CBM_DOWNLOAD_URL:-https://github.com/${REPO}/releases/latest/download}"
@@ -83,14 +81,10 @@ download_file() {
for arg in "$@"; do
case "$arg" in
--ui) VARIANT="ui" ;;
--standard) VARIANT="standard" ;;
--dir=*) INSTALL_DIR="${arg#--dir=}" ;;
--skip-config) SKIP_CONFIG=true ;;
--help|-h)
echo "Usage: install.sh [--ui] [--dir=<path>] [--skip-config]"
echo " --ui Install the UI variant (with graph visualization)"
echo " --standard Install the standard variant (default)"
echo "Usage: install.sh [--dir=<path>] [--skip-config]"
echo " --dir PATH Install directory (default: ~/.local/bin)"
echo " --skip-config Skip automatic agent configuration"
exit 0
@@ -138,7 +132,6 @@ ARCH=$(detect_arch)
echo "codebase-memory-mcp installer"
echo " os: $OS"
echo " arch: $ARCH"
echo " variant: $VARIANT"
echo " target: $INSTALL_DIR/codebase-memory-mcp"
echo ""
@@ -155,11 +148,7 @@ fi
PORTABLE=""
[ "$OS" = "linux" ] && PORTABLE="-portable"
if [ "$VARIANT" = "ui" ]; then
ARCHIVE="codebase-memory-mcp-ui-${OS}-${ARCH}${PORTABLE}.${EXT}"
else
ARCHIVE="codebase-memory-mcp-${OS}-${ARCH}${PORTABLE}.${EXT}"
fi
ARCHIVE="codebase-memory-mcp-${OS}-${ARCH}${PORTABLE}.${EXT}"
URL="${CBM_DOWNLOAD_URL}/${ARCHIVE}"
@@ -231,71 +220,6 @@ if [ "$EXPECTED" != "$ACTUAL" ]; then
fi
echo "Checksum verified."
# Validate the complete archive namespace before extraction. Standard releases
# are the canonical five files; UI releases add exactly one root-level,
# content-addressed pack. Anything else is a release-integrity failure, not a
# sidecar to ignore.
if [ "$OS" = "windows" ]; then
ARCHIVE_BINARY="codebase-memory-mcp.exe"
ARCHIVE_INSTALLER="install.ps1"
else
ARCHIVE_BINARY="codebase-memory-mcp"
ARCHIVE_INSTALLER="install.sh"
fi
ARCHIVE_MEMBERS_FILE="$DLDIR/archive-members.txt"
if [ "$EXT" = "zip" ]; then
if ! unzip -Z1 "$DLDIR/$ARCHIVE" > "$ARCHIVE_MEMBERS_FILE"; then
echo "error: could not enumerate release archive" >&2
exit 1
fi
else
if ! tar -tzf "$DLDIR/$ARCHIVE" > "$ARCHIVE_MEMBERS_FILE"; then
echo "error: could not enumerate release archive" >&2
exit 1
fi
fi
BINARY_MEMBERS=0
INTEGRATION_MEMBERS=0
LICENSE_MEMBERS=0
INSTALLER_MEMBERS=0
NOTICE_MEMBERS=0
UI_PACK_MEMBERS=0
ARCHIVE_MEMBER_COUNT=0
UI_PACK_NAME=""
while IFS= read -r member || [ -n "$member" ]; do
ARCHIVE_MEMBER_COUNT=$((ARCHIVE_MEMBER_COUNT + 1))
case "$member" in
"$ARCHIVE_BINARY") BINARY_MEMBERS=$((BINARY_MEMBERS + 1)) ;;
cbm-integrations.json) INTEGRATION_MEMBERS=$((INTEGRATION_MEMBERS + 1)) ;;
LICENSE) LICENSE_MEMBERS=$((LICENSE_MEMBERS + 1)) ;;
"$ARCHIVE_INSTALLER") INSTALLER_MEMBERS=$((INSTALLER_MEMBERS + 1)) ;;
THIRD_PARTY_NOTICES.md) NOTICE_MEMBERS=$((NOTICE_MEMBERS + 1)) ;;
*)
if [ "$VARIANT" = "ui" ] &&
[[ "$member" =~ ^cbm-ui-[0-9a-f]{64}\.pack$ ]]; then
UI_PACK_MEMBERS=$((UI_PACK_MEMBERS + 1))
UI_PACK_NAME="$member"
else
echo "error: release archive contains unexpected member: $member" >&2
exit 1
fi
;;
esac
done < "$ARCHIVE_MEMBERS_FILE"
EXPECTED_MEMBER_COUNT=5
if [ "$VARIANT" = "ui" ]; then
EXPECTED_MEMBER_COUNT=6
fi
if [ "$BINARY_MEMBERS" -ne 1 ] || [ "$INTEGRATION_MEMBERS" -ne 1 ] ||
[ "$LICENSE_MEMBERS" -ne 1 ] || [ "$INSTALLER_MEMBERS" -ne 1 ] ||
[ "$NOTICE_MEMBERS" -ne 1 ] || [ "$UI_PACK_MEMBERS" -ne $((EXPECTED_MEMBER_COUNT - 5)) ] ||
[ "$ARCHIVE_MEMBER_COUNT" -ne "$EXPECTED_MEMBER_COUNT" ]; then
echo "error: release archive does not match the exact $VARIANT member set" >&2
exit 1
fi
# Extract
echo "Extracting..."
if [ "$EXT" = "zip" ]; then
@@ -304,41 +228,12 @@ else
tar -xzf "$DLDIR/$ARCHIVE" -C "$DLDIR"
fi
for extracted_member in "$ARCHIVE_BINARY" cbm-integrations.json LICENSE \
"$ARCHIVE_INSTALLER" THIRD_PARTY_NOTICES.md; do
if [ ! -f "$DLDIR/$extracted_member" ] || [ -L "$DLDIR/$extracted_member" ]; then
echo "error: release member is not a regular file: $extracted_member" >&2
exit 1
fi
done
DLBIN="$DLDIR/$ARCHIVE_BINARY"
if [ ! -f "$DLBIN" ] || [ -L "$DLBIN" ]; then
DLBIN="$DLDIR/codebase-memory-mcp"
if [ ! -f "$DLBIN" ]; then
echo "error: binary not found after extraction" >&2
exit 1
fi
DL_UI_PACK=""
if [ "$VARIANT" = "ui" ]; then
DL_UI_PACK="$DLDIR/$UI_PACK_NAME"
if [ ! -f "$DL_UI_PACK" ] || [ -L "$DL_UI_PACK" ]; then
echo "error: UI asset pack not found after extraction" >&2
exit 1
fi
PACK_EXPECTED="${UI_PACK_NAME#cbm-ui-}"
PACK_EXPECTED="${PACK_EXPECTED%.pack}"
if command -v sha256sum &>/dev/null; then
PACK_ACTUAL=$(sha256sum "$DL_UI_PACK" | awk '{print $1}')
else
PACK_ACTUAL=$(shasum -a 256 "$DL_UI_PACK" | awk '{print $1}')
fi
PACK_ACTUAL=$(printf '%s' "$PACK_ACTUAL" | tr 'A-F' 'a-f')
if [ "$PACK_EXPECTED" != "$PACK_ACTUAL" ]; then
echo "error: UI asset pack digest does not match its filename" >&2
exit 1
fi
fi
# macOS: fix signing
if [ "$OS" = "darwin" ]; then
echo "Fixing macOS code signing..."
@@ -355,20 +250,7 @@ if ! CANDIDATE_VERSION=$("$DLBIN" --version 2>&1); then
fi
echo "Verified candidate: $CANDIDATE_VERSION"
# The candidate publishes the runtime set under one native activation guard,
# with sidecars before the executable and retained per-file backups for
# cooperative rollback. The set is intentionally recoverable/fail-closed, not
# claimed to be a crash-atomic multi-file filesystem transaction.
new_exclusive_sibling_temp() {
local destination="$1"
local directory basename
directory="$(dirname "$destination")"
basename="$(basename "$destination")"
mktemp "$directory/.${basename}.tmp.XXXXXX"
}
DEST="$INSTALL_DIR/codebase-memory-mcp"
[ "$OS" = "windows" ] && DEST="$INSTALL_DIR/codebase-memory-mcp.exe"
INSTALL_ARGS=(-y --force "--dir=$INSTALL_DIR")
if [ "$SKIP_CONFIG" = true ]; then
INSTALL_ARGS+=(--skip-config)
@@ -390,16 +272,13 @@ fi
# working install, and `update` falls back to explaining where to find it.
DL_INSTALLER="$DLDIR/install.sh"
if [ -f "$DL_INSTALLER" ]; then
INSTALLER_TMP=""
if INSTALLER_TMP="$(new_exclusive_sibling_temp "$INSTALL_DIR/install.sh" 2>/dev/null)" &&
cp "$DL_INSTALLER" "$INSTALLER_TMP" 2>/dev/null &&
INSTALLER_TMP="$INSTALL_DIR/.install.sh.$$"
if cp "$DL_INSTALLER" "$INSTALLER_TMP" 2>/dev/null &&
chmod 755 "$INSTALLER_TMP" 2>/dev/null &&
mv -f "$INSTALLER_TMP" "$INSTALL_DIR/install.sh" 2>/dev/null; then
echo "Installed updater -> $INSTALL_DIR/install.sh"
else
# A non-empty path was exclusively created by mktemp above. If
# reservation failed, leave every pre-existing sibling untouched.
[ -z "$INSTALLER_TMP" ] || rm -f "$INSTALLER_TMP" 2>/dev/null || true
rm -f "$INSTALLER_TMP" 2>/dev/null || true
echo "note: could not place install.sh in $INSTALL_DIR (update will explain where to find it)"
fi
fi
+1 -3
View File
@@ -2,7 +2,7 @@
pkgname=codebase-memory-mcp
pkgver=0.8.1
pkgrel=1
pkgdesc="Fast code intelligence engine — native executable with verified integration asset"
pkgdesc="Fast code intelligence engine for AI coding agents — single static binary MCP server"
arch=('x86_64' 'aarch64')
url="https://github.com/DeusData/codebase-memory-mcp"
license=('MIT')
@@ -18,8 +18,6 @@ sha256sums_aarch64=('d2f842d1365da5c35d9c5796f57a821c9745267350994346735e1e6e04d
package() {
install -Dm755 "${srcdir}/codebase-memory-mcp" \
"${pkgdir}/usr/bin/codebase-memory-mcp"
install -Dm644 "${srcdir}/cbm-integrations.json" \
"${pkgdir}/usr/share/codebase-memory-mcp/cbm-integrations.json"
install -Dm644 "${srcdir}/LICENSE" \
"${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
# Third-party attribution bundle (present in archives since v0.8.1)
+1 -2
View File
@@ -15,13 +15,12 @@ ARG TARGETARCH
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& curl -fsSL "https://github.com/DeusData/codebase-memory-mcp/releases/latest/download/codebase-memory-mcp-linux-${TARGETARCH}-portable.tar.gz" \
| tar -xz -C /tmp codebase-memory-mcp cbm-integrations.json LICENSE THIRD_PARTY_NOTICES.md \
| tar -xz -C /tmp codebase-memory-mcp LICENSE THIRD_PARTY_NOTICES.md \
&& chmod +x /tmp/codebase-memory-mcp \
&& rm -rf /var/lib/apt/lists/*
FROM debian:bookworm-slim
COPY --from=fetch /tmp/codebase-memory-mcp /usr/local/bin/codebase-memory-mcp
COPY --from=fetch /tmp/cbm-integrations.json /usr/local/share/codebase-memory-mcp/cbm-integrations.json
COPY --from=fetch /tmp/LICENSE /tmp/THIRD_PARTY_NOTICES.md /usr/share/doc/codebase-memory-mcp/
ENV CBM_CACHE_DIR=/tmp/cbm
ENTRYPOINT ["codebase-memory-mcp"]
File diff suppressed because it is too large Load Diff
@@ -28,7 +28,6 @@ class CodebaseMemoryMcp < Formula
def install
bin.install "codebase-memory-mcp"
(share/"codebase-memory-mcp").install "cbm-integrations.json"
# Third-party attribution bundle (present in archives since v0.8.1)
doc.install "THIRD_PARTY_NOTICES.md" if File.exist?("THIRD_PARTY_NOTICES.md")
end
@@ -46,6 +45,5 @@ class CodebaseMemoryMcp < Formula
test do
assert_match "codebase-memory-mcp", shell_output("#{bin}/codebase-memory-mcp --version")
assert_path_exists share/"codebase-memory-mcp/cbm-integrations.json"
end
end
+185 -655
View File
File diff suppressed because it is too large Load Diff
+15 -427
View File
@@ -1,27 +1,17 @@
'use strict';
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const { spawn } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
const {
INTEGRATIONS_NAME,
UNIX_ARCHIVE_NAMES,
WINDOWS_BINARY_NAME,
acquireRuntimeLock,
cacheDirForVariant,
extractExactTarArchive,
publishRuntimeSetWithRecovery,
runtimeSetReady,
runtimeSetReadyLocked,
releaseRuntimeLock,
tryReclaimRuntimeLock,
installWindowsBinaryAtomically,
validateExactTarMemberListing,
validateRegularTarMemberListing,
} = require('../install.js');
function exactUnixListing(extra = []) {
@@ -43,81 +33,28 @@ test('Unix archive validation rejects traversal and unexpected members', () => {
);
});
test('release archives require the integrations sidecar', () => {
assert.ok(
UNIX_ARCHIVE_NAMES.includes(INTEGRATIONS_NAME),
'the authenticated archive namespace must include the runtime integrations sidecar',
);
});
test('UI Unix archive validation accepts exactly one content-addressed pack', () => {
const pack = `cbm-ui-${'a'.repeat(64)}.pack`;
assert.doesNotThrow(() => validateExactTarMemberListing(
exactUnixListing([pack]), UNIX_ARCHIVE_NAMES, true,
));
assert.throws(
() => validateExactTarMemberListing(
exactUnixListing([pack, `cbm-ui-${'b'.repeat(64)}.pack`]),
UNIX_ARCHIVE_NAMES,
true,
),
/exactly one|unexpected or duplicate/,
);
});
test('Unix extraction requests only the validated runtime set', () => {
test('Unix extraction requests only the validated root executable', () => {
const calls = [];
const runner = (command, args) => {
calls.push({ command, args: [...args] });
if (args[0] === '-tzf') return exactUnixListing();
if (args[0] === '-tvzf') {
return UNIX_ARCHIVE_NAMES.map(
(name) => `-rw-r--r-- 0 owner group 1 Jan 1 00:00 ${name}`,
).join('\n') + '\n';
}
return Buffer.alloc(0);
return calls.length === 1 ? exactUnixListing() : Buffer.alloc(0);
};
extractExactTarArchive(
'/tmp/release.tar.gz', '/tmp/extract', UNIX_ARCHIVE_NAMES,
['codebase-memory-mcp', INTEGRATIONS_NAME], false, runner,
'codebase-memory-mcp', runner,
);
assert.deepEqual(calls[0].args, ['-tzf', '/tmp/release.tar.gz']);
assert.deepEqual(calls[1].args, ['-tvzf', '/tmp/release.tar.gz']);
assert.deepEqual(
calls[2].args,
[
'-xzf', '/tmp/release.tar.gz', '-C', '/tmp/extract',
'codebase-memory-mcp', INTEGRATIONS_NAME,
],
calls[1].args,
['-xzf', '/tmp/release.tar.gz', '-C', '/tmp/extract', 'codebase-memory-mcp'],
);
});
test('Unix archive validation rejects hardlink members', () => {
const listing = UNIX_ARCHIVE_NAMES.map(
(name, index) => `${index === 0 ? 'h' : '-'}rw-r--r-- ${name}`,
).join('\n') + '\n';
assert.throws(
() => validateRegularTarMemberListing(listing, UNIX_ARCHIVE_NAMES.length),
/independent regular files/,
);
});
function writeRuntimeSet(directory, tag, variant = 'standard') {
function writeBinary(directory, tag) {
fs.mkdirSync(directory, { recursive: true });
fs.writeFileSync(path.join(directory, WINDOWS_BINARY_NAME), `binary:${tag}`);
fs.writeFileSync(path.join(directory, INTEGRATIONS_NAME), `integrations:${tag}`);
if (variant === 'ui') {
const contents = Buffer.from(`pack:${tag}`);
const digest = crypto.createHash('sha256').update(contents).digest('hex');
fs.writeFileSync(
path.join(directory, `cbm-ui-${digest}.pack`),
contents,
);
return `cbm-ui-${digest}.pack`;
}
return null;
}
function fakeBinaryVerifier(binaryPath) {
@@ -144,39 +81,15 @@ function assertBinary(directory, tag) {
fs.readFileSync(path.join(directory, WINDOWS_BINARY_NAME), 'utf8'),
`binary:${tag}`,
);
assert.equal(
fs.readFileSync(path.join(directory, INTEGRATIONS_NAME), 'utf8'),
`integrations:${tag}`,
);
}
async function waitForCrashMarker(marker, child, stderr) {
const deadline = Date.now() + 10_000;
while (Date.now() < deadline) {
if (fs.existsSync(marker)) return;
if (child.exitCode !== null || child.signalCode !== null) {
throw new Error(`publication helper exited before crash gate: ${stderr()}`);
}
await new Promise((resolve) => setTimeout(resolve, 10));
}
throw new Error(`publication helper did not reach crash gate: ${stderr()}`);
}
function waitForChildExit(child) {
return new Promise((resolve) => {
child.once('exit', (code, signal) => resolve({ code, signal }));
});
}
test('Windows publication repairs a corrupt cached binary', () => {
withBinaryDirectories(({ source, destination }) => {
writeRuntimeSet(source, 'candidate');
writeRuntimeSet(destination, 'old');
writeBinary(source, 'candidate');
writeBinary(destination, 'old');
fs.writeFileSync(path.join(destination, WINDOWS_BINARY_NAME), 'corrupt');
publishRuntimeSetWithRecovery(
source, destination, WINDOWS_BINARY_NAME, 'standard', fakeBinaryVerifier,
);
installWindowsBinaryAtomically(source, destination, fakeBinaryVerifier);
assertBinary(destination, 'candidate');
});
@@ -184,11 +97,9 @@ test('Windows publication repairs a corrupt cached binary', () => {
test('Windows publication installs into an empty cache', () => {
withBinaryDirectories(({ source, destination }) => {
writeRuntimeSet(source, 'candidate');
writeBinary(source, 'candidate');
publishRuntimeSetWithRecovery(
source, destination, WINDOWS_BINARY_NAME, 'standard', fakeBinaryVerifier,
);
installWindowsBinaryAtomically(source, destination, fakeBinaryVerifier);
assertBinary(destination, 'candidate');
});
@@ -196,334 +107,11 @@ test('Windows publication installs into an empty cache', () => {
test('Windows publication preserves a valid concurrent winner', () => {
withBinaryDirectories(({ source, destination }) => {
writeRuntimeSet(source, 'loser');
writeRuntimeSet(destination, 'winner');
writeBinary(source, 'loser');
writeBinary(destination, 'winner');
publishRuntimeSetWithRecovery(
source, destination, WINDOWS_BINARY_NAME, 'standard', fakeBinaryVerifier,
);
installWindowsBinaryAtomically(source, destination, fakeBinaryVerifier);
assertBinary(destination, 'winner');
});
});
test('runtime readiness requires the complete variant-specific set', () => {
withBinaryDirectories(({ destination }) => {
writeRuntimeSet(destination, 'standard');
assert.equal(
runtimeSetReady(destination, WINDOWS_BINARY_NAME, 'standard', fakeBinaryVerifier),
true,
);
fs.unlinkSync(path.join(destination, INTEGRATIONS_NAME));
assert.equal(
runtimeSetReady(destination, WINDOWS_BINARY_NAME, 'standard', fakeBinaryVerifier),
false,
);
writeRuntimeSet(destination, 'ui', 'ui');
assert.equal(
runtimeSetReady(destination, WINDOWS_BINARY_NAME, 'ui', fakeBinaryVerifier),
true,
);
fs.writeFileSync(
path.join(destination, `cbm-ui-${'b'.repeat(64)}.pack`),
'second pack',
);
assert.equal(
runtimeSetReady(destination, WINDOWS_BINARY_NAME, 'ui', fakeBinaryVerifier),
false,
);
});
});
test('UI readiness rejects and publication repairs a digest-mismatched pack', () => {
withBinaryDirectories(({ source, destination }) => {
const sourcePack = writeRuntimeSet(source, 'candidate', 'ui');
const corruptPack = writeRuntimeSet(destination, 'old', 'ui');
fs.writeFileSync(path.join(destination, corruptPack), 'corrupt pack bytes');
assert.equal(
runtimeSetReady(destination, WINDOWS_BINARY_NAME, 'ui', fakeBinaryVerifier),
false,
'a well-shaped pack filename must not authenticate different bytes',
);
publishRuntimeSetWithRecovery(
source, destination, WINDOWS_BINARY_NAME, 'ui', fakeBinaryVerifier,
);
assertBinary(destination, 'candidate');
assert.equal(fs.readFileSync(path.join(destination, sourcePack), 'utf8'), 'pack:candidate');
assert.equal(fs.existsSync(path.join(destination, corruptPack)), false);
});
});
test('standard and UI cache directories do not collide', () => {
assert.notEqual(cacheDirForVariant('standard'), cacheDirForVariant('ui'));
});
test('runtime publication commits sidecars before the binary', () => {
withBinaryDirectories(({ source, destination }) => {
writeRuntimeSet(source, 'candidate', 'ui');
const committed = [];
const renameSync = fs.renameSync;
fs.renameSync = (sourcePath, destinationPath) => {
const name = path.basename(destinationPath);
if (path.dirname(destinationPath) === destination &&
(name === WINDOWS_BINARY_NAME || name === INTEGRATIONS_NAME ||
/^cbm-ui-[0-9a-f]{64}\.pack$/.test(name))) {
committed.push(name);
}
return renameSync(sourcePath, destinationPath);
};
try {
publishRuntimeSetWithRecovery(
source, destination, WINDOWS_BINARY_NAME, 'ui', fakeBinaryVerifier,
);
} finally {
fs.renameSync = renameSync;
}
assert.equal(committed[0], INTEGRATIONS_NAME);
assert.match(committed[1], /^cbm-ui-[0-9a-f]{64}\.pack$/);
assert.equal(committed[2], WINDOWS_BINARY_NAME);
});
});
test('a killed publisher is reconciled on the next locked readiness check', async () => {
const helperSource = String.raw`
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const [installPath, source, destination, marker, crashName] = process.argv.slice(1);
const install = require(installPath);
const renameSync = fs.renameSync;
fs.renameSync = (sourcePath, destinationPath) => {
const result = renameSync(sourcePath, destinationPath);
if (destinationPath === path.join(destination, crashName)) {
fs.writeFileSync(marker, 'reached\n');
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 60_000);
}
return result;
};
const verifier = (binaryPath) => {
if (!/^binary:(.+)$/.test(fs.readFileSync(binaryPath, 'utf8'))) {
throw new Error('test binary failed verification');
}
};
install.publishRuntimeSetWithRecovery(
source, destination, install.WINDOWS_BINARY_NAME, 'standard', verifier,
);
`;
for (const [crashName, expectedReady] of [
[INTEGRATIONS_NAME, false],
[WINDOWS_BINARY_NAME, true],
]) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cbm-npm-crash-test-'));
const source = path.join(root, 'source');
const destination = path.join(root, 'destination');
const marker = path.join(root, 'crash-reached');
fs.mkdirSync(destination);
writeRuntimeSet(source, 'candidate');
writeRuntimeSet(destination, 'old');
fs.writeFileSync(
path.join(destination, WINDOWS_BINARY_NAME), 'corrupt:old',
);
let stderr = '';
const child = spawn(process.execPath, [
'-e',
helperSource,
path.join(__dirname, '..', 'install.js'),
source,
destination,
marker,
crashName,
], { stdio: ['ignore', 'ignore', 'pipe'] });
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
const exitPromise = waitForChildExit(child);
try {
await waitForCrashMarker(marker, child, () => stderr);
const backups = fs.readdirSync(destination)
.filter((name) => name.startsWith('.cbm-runtime-backup-'));
assert.equal(backups.length, 1, 'killed publisher did not retain one transaction');
assert.equal(
fs.lstatSync(path.join(destination, backups[0])).isDirectory(),
true,
);
assert.equal(
fs.existsSync(path.join(
destination, backups[0], '.retirement-complete',
)),
true,
);
assert.equal(child.kill('SIGKILL'), true);
const exit = await exitPromise;
assert.ok(
exit.signal !== null || exit.code !== 0,
'publication helper was not killed',
);
const ready = runtimeSetReadyLocked(
destination,
WINDOWS_BINARY_NAME,
'standard',
fakeBinaryVerifier,
);
assert.equal(ready, expectedReady);
if (!ready) {
assert.equal(
fs.readFileSync(path.join(destination, WINDOWS_BINARY_NAME), 'utf8'),
'corrupt:old',
);
assert.equal(
fs.readFileSync(path.join(destination, INTEGRATIONS_NAME), 'utf8'),
'integrations:old',
);
publishRuntimeSetWithRecovery(
source,
destination,
WINDOWS_BINARY_NAME,
'standard',
fakeBinaryVerifier,
);
}
assertBinary(destination, 'candidate');
assert.equal(
fs.readdirSync(destination).some(
(name) => name.startsWith('.cbm-runtime-backup-'),
),
false,
);
assert.equal(
fs.existsSync(path.join(destination, '.codebase-memory-mcp-runtime.lock')),
false,
);
} finally {
if (child.exitCode === null && child.signalCode === null) {
child.kill('SIGKILL');
await exitPromise;
}
fs.rmSync(root, { recursive: true, force: true });
}
}
});
test('a stalled creator never deletes a successor runtime lock', () => {
withBinaryDirectories(({ destination }) => {
let successor;
let injected = false;
assert.throws(
() => acquireRuntimeLock(destination, () => {
if (injected) return;
injected = true;
successor = acquireRuntimeLock(destination);
throw new Error('injected stalled creator abort');
}),
/injected stalled creator abort/,
);
assert.ok(successor, 'the successor must acquire the reclaimed lock');
assert.ok(
fs.existsSync(successor.lockPath),
'the failed creator must not recursively remove its successor',
);
releaseRuntimeLock(successor);
});
});
test('runtime readiness rejects multiply-linked cache leaves', () => {
withBinaryDirectories(({ destination }) => {
writeRuntimeSet(destination, 'linked');
fs.linkSync(
path.join(destination, INTEGRATIONS_NAME),
path.join(destination, 'integrations-hardlink'),
);
assert.equal(
runtimeSetReady(destination, WINDOWS_BINARY_NAME, 'standard', fakeBinaryVerifier),
false,
);
});
});
test('orphan reconciliation rejects multiply-linked backup members', () => {
withBinaryDirectories(({ destination }) => {
const backup = path.join(
destination, `.cbm-runtime-backup-${'a'.repeat(32)}`,
);
fs.mkdirSync(backup);
fs.writeFileSync(path.join(backup, '.retirement-complete'), '');
const member = path.join(backup, WINDOWS_BINARY_NAME);
fs.writeFileSync(member, 'binary:old');
fs.linkSync(member, path.join(destination, 'backup-hardlink-copy'));
assert.throws(
() => runtimeSetReadyLocked(
destination,
WINDOWS_BINARY_NAME,
'standard',
fakeBinaryVerifier,
),
/unsafe package-cache backup member/,
);
assert.equal(fs.existsSync(backup), true, 'unsafe backup was mutated');
assert.equal(
fs.existsSync(path.join(destination, '.codebase-memory-mcp-runtime.lock')),
false,
);
});
});
test('an expired lease never reclaims a live owner', () => {
withBinaryDirectories(({ destination }) => {
const lockPath = path.join(destination, '.codebase-memory-mcp-runtime.lock');
const owner = {
pid: process.pid,
token: 'a'.repeat(32),
lease_expires_ms: Date.now() - 1,
};
const ownerRecord = `${JSON.stringify(owner)}\n`;
fs.writeFileSync(lockPath, ownerRecord);
const before = fs.lstatSync(lockPath, { bigint: true });
assert.equal(tryReclaimRuntimeLock(lockPath, 'b'.repeat(32)), false);
const after = fs.lstatSync(lockPath, { bigint: true });
assert.equal(fs.readFileSync(lockPath, 'utf8'), ownerRecord);
assert.equal(JSON.parse(fs.readFileSync(lockPath, 'utf8')).token, owner.token);
assert.equal(after.dev, before.dev);
assert.equal(after.ino, before.ino);
});
});
test('a stale legacy lock never reclaims a live owner', () => {
withBinaryDirectories(({ destination }) => {
const lockPath = path.join(destination, '.codebase-memory-mcp-runtime.lock');
const ownerRecord = `${JSON.stringify({
pid: process.pid,
token: 'a'.repeat(32),
})}\n`;
fs.mkdirSync(lockPath);
fs.writeFileSync(path.join(lockPath, 'owner.json'), ownerRecord);
const stale = new Date(Date.now() - 3_600_001);
fs.utimesSync(lockPath, stale, stale);
const before = fs.lstatSync(lockPath, { bigint: true });
assert.equal(tryReclaimRuntimeLock(lockPath, 'b'.repeat(32)), false);
const after = fs.lstatSync(lockPath, { bigint: true });
assert.equal(fs.readFileSync(path.join(lockPath, 'owner.json'), 'utf8'), ownerRecord);
assert.equal(after.dev, before.dev);
assert.equal(after.ino, before.ino);
});
});
test('a stale ownerless lock is reclaimed', () => {
withBinaryDirectories(({ destination }) => {
const lockPath = path.join(destination, '.codebase-memory-mcp-runtime.lock');
fs.writeFileSync(lockPath, '');
const stale = new Date(Date.now() - 30_001);
fs.utimesSync(lockPath, stale, stale);
assert.equal(tryReclaimRuntimeLock(lockPath, 'b'.repeat(32)), true);
assert.equal(fs.existsSync(lockPath), false);
});
});
+16 -139
View File
@@ -1,15 +1,11 @@
'use strict';
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const vm = require('node:vm');
const UI_PACK_BYTES = Buffer.from('authenticated UI pack');
const UI_PACK_NAME = `cbm-ui-${crypto.createHash('sha256').update(UI_PACK_BYTES).digest('hex')}.pack`;
class ExitSignal extends Error {
constructor(code) {
super(`process.exit(${code})`);
@@ -17,77 +13,26 @@ class ExitSignal extends Error {
}
}
function runShim(targetPlatform, arguments_, childStatus, options = {}) {
function runShim(targetPlatform, arguments_, childStatus) {
const shimPath = path.join(__dirname, '..', 'bin.js');
const source = fs.readFileSync(shimPath, 'utf8').replace(/^#![^\n]*\n/, '');
const calls = [];
const lockEvents = [];
let stderr = '';
let installed = false;
const fakeProcess = {
platform: targetPlatform,
env: {
CBM_VARIANT: options.variant || '',
LOCALAPPDATA: 'C:\\Users\\test\\AppData\\Local',
},
argv: ['node.exe', shimPath, ...arguments_],
execPath: 'node.exe',
stderr: { write: (text) => { stderr += String(text); } },
exit: (code) => { throw new ExitSignal(code); },
};
const fakeFs = {
lstatSync: (candidate) => {
if (options.missingSidecar && !installed &&
path.basename(candidate) === 'cbm-integrations.json') {
const error = new Error('missing sidecar');
error.code = 'ENOENT';
throw error;
}
return { isFile: () => true, isSymbolicLink: () => false, nlink: 1 };
},
readFileSync: () => options.corruptPack && !installed
? Buffer.from('corrupt UI pack')
: UI_PACK_BYTES,
readdirSync: () => options.variant === 'ui'
? [UI_PACK_NAME]
: [],
existsSync: () => true,
};
const fakeChildProcess = {
spawn: (executable, args, options) => {
calls.push({ executable, args, options });
return {
on: (event, callback) => {
if (event === 'close') callback(childStatus);
return this;
},
};
},
spawnSync: (executable, args, options) => {
calls.push({ executable, args, options });
if (executable === fakeProcess.execPath) {
installed = true;
return { status: 0 };
}
if (args[0] === '--verify-runtime-assets') return { status: 0 };
return { status: childStatus };
},
};
const fakeLifecycle = {
acquireRuntimeLock: () => {
lockEvents.push('acquire');
return { lockPath: 'lock', token: 'token', fd: 1 };
},
refreshRuntimeLock: () => { lockEvents.push('refresh'); },
releaseRuntimeLock: () => { lockEvents.push('release'); },
runtimeSetReady: () => true,
runtimeSetReadyLocked: (directory, binaryName) => {
const structurallyReady = installed || (!options.missingSidecar && !options.corruptPack);
if (!structurallyReady) return false;
const probe = fakeChildProcess.spawnSync(
path.join(directory, binaryName), ['--verify-runtime-assets'], { stdio: 'ignore' },
);
return probe.status === 0;
return { status: calls.length === 1 ? 0 : childStatus };
},
};
const sandbox = {
@@ -102,11 +47,8 @@ function runShim(targetPlatform, arguments_, childStatus, options = {}) {
require: (specifier) => {
if (specifier === 'fs') return fakeFs;
if (specifier === 'child_process') return fakeChildProcess;
if (specifier === './install.js') return fakeLifecycle;
return require(specifier);
},
clearInterval: () => {},
setInterval: () => 1,
setTimeout,
};
@@ -117,10 +59,10 @@ function runShim(targetPlatform, arguments_, childStatus, options = {}) {
if (!(error instanceof ExitSignal)) throw error;
exitCode = error.code;
}
return { calls, exitCode, lockEvents, stderr };
return { calls, exitCode, stderr };
}
test('Windows npm shim authenticates assets and executes the cached binary', () => {
test('Windows npm shim probes and executes the cached launcher', () => {
const observed = runShim('win32', ['--version'], 0);
assert.equal(observed.exitCode, 0);
@@ -132,86 +74,25 @@ test('Windows npm shim authenticates assets and executes the cached binary', ()
'codebase-memory-mcp.payload.exe',
);
}
assert.deepEqual(Array.from(observed.calls[0].args), ['--verify-runtime-assets']);
assert.deepEqual(Array.from(observed.calls[1].args), ['--version']);
assert.equal(observed.calls[1].options.stdio, 'inherit');
});
test('npm update is routed to the package manager without invoking absent install.sh', () => {
test('Windows npm shim keeps package-manager guidance after launcher refusal', () => {
const observed = runShim('win32', ['update'], 1);
assert.equal(observed.exitCode, 2);
assert.equal(observed.calls.length, 0, 'update guidance must not launch cached code');
assert.equal(observed.exitCode, 1);
assert.equal(path.basename(observed.calls[1].executable), 'codebase-memory-mcp.exe');
assert.match(observed.stderr, /npm install codebase-memory-mcp@latest/);
assert.match(observed.stderr, /codebase-memory-mcp install --yes/);
assert.doesNotMatch(observed.stderr, /install\.sh/);
assert.match(observed.stderr, /install --yes/);
});
test('nested update token is forwarded instead of treated as package update', () => {
const observed = runShim('win32', ['daemon', 'update'], 0);
assert.equal(observed.exitCode, 0);
assert.equal(observed.calls.length, 2);
assert.deepEqual(Array.from(observed.calls[1].args), ['daemon', 'update']);
assert.doesNotMatch(observed.stderr, /npm install codebase-memory-mcp@latest/);
});
test('non-Windows npm shim executes its cached binary directly', () => {
test('non-Windows npm shim keeps its native payload execution path', () => {
const observed = runShim('darwin', ['--version'], 0);
assert.equal(observed.exitCode, 0);
assert.equal(observed.calls.length, 2);
assert.deepEqual(Array.from(observed.calls[0].args), ['--verify-runtime-assets']);
assert.equal(path.basename(observed.calls[1].executable), 'codebase-memory-mcp');
});
test('npm shim selects a separate UI runtime set', () => {
const observed = runShim('darwin', ['--version'], 0, { variant: 'ui' });
assert.equal(observed.exitCode, 0);
assert.equal(path.basename(path.dirname(observed.calls[1].executable)), 'ui');
});
test('npm shim repairs a cache whose integrations sidecar is missing', () => {
const observed = runShim(
'darwin', ['--version'], 0, { missingSidecar: true },
);
assert.equal(observed.exitCode, 0);
assert.equal(observed.calls[0].executable, 'node.exe');
assert.equal(path.basename(observed.calls[0].args[0]), 'install.js');
assert.deepEqual(Array.from(observed.calls[1].args), ['--verify-runtime-assets']);
assert.equal(path.basename(observed.calls[2].executable), 'codebase-memory-mcp');
});
test('npm shim repairs a UI cache whose pack digest does not match its name', () => {
const observed = runShim(
'darwin', ['--version'], 0, { variant: 'ui', corruptPack: true },
);
assert.equal(observed.exitCode, 0);
assert.equal(observed.calls[0].executable, 'node.exe');
assert.equal(path.basename(observed.calls[0].args[0]), 'install.js');
assert.deepEqual(Array.from(observed.calls[1].args), ['--verify-runtime-assets']);
assert.equal(path.basename(observed.calls[2].executable), 'codebase-memory-mcp');
});
test('wrapper uninstall locks the cache and targets the managed install by default', () => {
const observed = runShim('win32', ['uninstall', '--yes'], 0);
assert.equal(observed.exitCode, 0);
assert.deepEqual(observed.lockEvents, ['acquire', 'refresh', 'release']);
assert.deepEqual(
Array.from(observed.calls[1].args),
[
'uninstall', '--yes', '--dir',
path.join(
'C:\\Users\\test\\AppData\\Local',
'Programs',
'codebase-memory-mcp',
),
],
);
assert.equal(observed.calls.length, 1);
assert.equal(path.basename(observed.calls[0].executable), 'codebase-memory-mcp');
});
test('PowerShell install mutation runs through the downloaded binary', () => {
@@ -230,12 +111,8 @@ test('PowerShell install mutation runs through the downloaded binary', () => {
);
// One binary ships per platform — no launcher/payload pair to resolve.
assert.doesNotMatch(installer, /payload/i);
// The candidate owns activation and rollback under the native guard. The
// script must not mutate the installed binary before handing it the request.
const handoff = installer.indexOf('& $DownloadedBinary @InstallArgs');
assert.ok(handoff > 0, 'downloaded candidate handoff is missing');
assert.doesNotMatch(
installer.slice(0, handoff),
/(?:Move-Item|Copy-Item|Remove-Item)[^\n]*(?:\$Dest|\$InstallDir[^\n]*\$BinName)/,
);
// 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/);
});
File diff suppressed because it is too large Load Diff
+1 -27
View File
@@ -3,7 +3,7 @@
#
# Usage:
# scripts/build.sh # Standard binary
# scripts/build.sh --with-ui # Binary + external UI asset pack
# scripts/build.sh --with-ui # Binary with the UI embedded
# scripts/build.sh --help # Full usage
# scripts/build.sh --version v0.8.0 # With version stamp
# scripts/build.sh --arch x86_64 # Force x86_64 build
@@ -156,30 +156,4 @@ else
CFLAGS_EXTRA="$CFLAGS_EXTRA" "${EXTRA_MAKE_ARGS[@]+"${EXTRA_MAKE_ARGS[@]}"}"
fi
# Stage the integration-template asset next to the binary so the dev/CI build
# mirrors the release archive layout. The binary resolves cbm-integrations.json
# next to itself, in $CBM_ASSETS_DIR, or under ~/.cbm/assets/<sha256>/; a plain
# build/c binary has none of those, so install/uninstall (which render the
# templates to verify hash and ownership) would fail closed exactly as they do
# for a user who deleted the file. Copying it here keeps `install` working
# straight out of a build tree — smoke, local dev, and the release packaging all
# then see the same adjacency.
cp "$ROOT/assets/cbm-integrations.json" "$BUILD_DIR/cbm-integrations.json"
if $WITH_UI; then
shopt -s nullglob
UI_PACKS=("$BUILD_DIR"/cbm-ui-*.pack)
shopt -u nullglob
if [ "${#UI_PACKS[@]}" -ne 1 ]; then
echo "build.sh: UI build did not produce exactly one content-addressed asset pack" >&2
exit 1
fi
UI_PACK_NAME="$(basename "${UI_PACKS[0]}")"
if ! [[ "$UI_PACK_NAME" =~ ^cbm-ui-[0-9a-f]{64}\.pack$ ]]; then
echo "build.sh: invalid UI asset pack name: $UI_PACK_NAME" >&2
exit 1
fi
echo "=== UI assets: ${UI_PACKS[0]} ==="
fi
echo "=== Build complete: ${BUILD_DIR}/codebase-memory-mcp ==="
+10 -92
View File
@@ -20,7 +20,7 @@
# A missing tool is a hard error for the same reason: a skipped assertion must
# never look like a satisfied one.
#
# Usage: scripts/ci/check-binary-composition.sh [--variant=auto|standard|ui] <binary-or-dir>...
# Usage: scripts/ci/check-binary-composition.sh <binary-or-dir>...
# Directories are scanned recursively; format (ELF / Mach-O / PE) is detected
# per file from its magic bytes and each assertion runs where it is
# meaningful. Exit 0 = every assertion passed, 1 = at least one failed,
@@ -98,44 +98,15 @@ UI_HTTP_NEEDLES=(
'[c]odebase-memory-mcp'
)
# The binary used to embed nine complete shebang'd shell scripts, their
# PowerShell/.cmd twins and two node:child_process client modules — script
# bodies written to disk at 0755. Keeping those programs outside the native
# image narrows executable surface and lets each component be inspected and
# scanned independently. They now ship in
# cbm-integrations.json (the binary embeds only its SHA-256); these needles
# prove the removal stayed removed. Source-level enforcement lives in
# tests/test_no_embedded_scripts_contract.sh; this is the shipped-artifact
# half of the same property.
SCRIPT_NEEDLES=(
'#!/usr/bin/env bash'
'#!/bin/bash'
'#!/bin/sh'
'node:child_process'
)
# UI frontend bytes belong exclusively in the independently scannable pack.
# These are structural bytes from the pack header and Vite's emitted index,
# selected to avoid generic HTML grammar strings that legitimately occur in the
# language parsers linked into every binary.
UI_ASSET_BYTE_NEEDLES=(
'CBMUIPK'
'<!doctype html>'
'<html lang="en" class="dark">'
'<script type="module" crossorigin'
)
# Canary: proves the needle scan can actually see this file's strings. Without
# it, handing the gate a gzip, a stub or a 0-byte file would pass every
# absence assertion. 168+ occurrences in a real artifact, 0 in anything else.
CANARY_NEEDLE='codebase-memory-mcp'
# ── Args ────────────────────────────────────────────────────────────
VARIANT=auto
TARGETS=()
for arg in "$@"; do
case "$arg" in
--variant=*) VARIANT="${arg#--variant=}" ;;
-*)
echo "FAIL: unknown flag $arg (see --help)" >&2
exit 2
@@ -143,13 +114,6 @@ for arg in "$@"; do
*) TARGETS+=("$arg") ;;
esac
done
case "$VARIANT" in
auto | standard | ui) ;;
*)
echo "FAIL: --variant must be auto, standard or ui (got '$VARIANT')" >&2
exit 2
;;
esac
if [ "${#TARGETS[@]}" -eq 0 ]; then
echo "FAIL: no binaries or directories given (see --help)" >&2
exit 2
@@ -249,8 +213,8 @@ skipped_files=0
check_file() {
file="$1"
# Two path components: both variants ship a binary literally named
# "codebase-memory-mcp", so the parent directory is what tells them apart.
# Two path components: the binary is literally named
# "codebase-memory-mcp", so the parent directory disambiguates the log.
token=$(printf '%s' "$file" | awk -F/ '{ if (NF > 1) print $(NF - 1) "/" $NF; else print $NF }')
fmt=$(detect_format "$file")
if [ "$fmt" = other ]; then
@@ -259,24 +223,7 @@ check_file() {
return 0
fi
is_ui=0
case "$VARIANT" in
ui) is_ui=1 ;;
standard) is_ui=0 ;;
auto)
# The UI archive is codebase-memory-mcp-ui-<os>-<arch>, but the binary
# inside it is just "codebase-memory-mcp" — so the whole path decides,
# not the basename. Printed below so a misclassified path is visible in
# the log instead of silently disarming the UI assertion.
case "$file" in
*-ui | *-ui.* | *-ui-* | *-ui/*) is_ui=1 ;;
esac
;;
esac
variant_label=standard
[ "$is_ui" -eq 1 ] && variant_label='UI (A5-no-ui-http not applicable)'
printf '\n── %s [%s, %s] ──\n' "$token" "$fmt" "$variant_label"
printf '\n── %s [%s] ──\n' "$token" "$fmt"
checked_files=$((checked_files + 1))
# A0 — anti-vacuity canary; every assertion below is an absence check.
@@ -362,46 +309,17 @@ check_file() {
"sqlite compile-option table absent; A4 rests on absence needles only"
fi
# A5 — UI/HTTP subsystem, standard artifacts only.
#
# The temporary standard variant still links the UI control/HTTP code but
# substitutes the no-I/O asset-pack stub, so it cannot publish a UI. That
# variant remains only for before/after VirusTotal verification and is then
# deprecated in favor of one UI-capable product composition. Until that
# evidence gate is complete, this assertion remains an explicit diagnostic;
# CBM_CHECK_UI_ABSENT=1 is available for any future true no-UI composition.
if [ "$is_ui" -eq 1 ]; then
printf 'n/a %-22s %s: UI artifact, HTTP server ships here by design\n' \
A5-no-ui-http "$token"
elif [ "${CBM_CHECK_UI_ABSENT:-0}" = "1" ]; then
# A5 — UI/HTTP subsystem. One composition ships and it serves the graph UI
# from embedded assets, so the HTTP server belongs here by construction.
# CBM_CHECK_UI_ABSENT=1 still enforces absence for any future headless build.
if [ "${CBM_CHECK_UI_ABSENT:-0}" = "1" ]; then
for needle in "${UI_HTTP_NEEDLES[@]}"; do
assert_absent "$file" "$token" A5-no-ui-http "$needle"
done
else
ui_hits=0
for needle in "${UI_HTTP_NEEDLES[@]}"; do
if LC_ALL=C grep -a -q -F -e "$needle" "$file"; then
ui_hits=$((ui_hits + 1))
fi
done
printf 'INFO %-22s %s: %d/%d UI/HTTP needles present (temporary standard variant; set CBM_CHECK_UI_ABSENT=1 to enforce a true no-UI composition)\n' \
A5-no-ui-http "$token" "$ui_hits" "${#UI_HTTP_NEEDLES[@]}"
printf 'n/a %-22s %s: UI-capable artifact, HTTP server ships here by design\n' \
A5-no-ui-http "$token"
fi
# A6 — embedded integration-script text. Every variant is now subject to
# the same hard gate: the UI frontend is external, so it can no longer mask
# a returned shebang or node:child_process body in the native image.
for needle in "${SCRIPT_NEEDLES[@]}"; do
assert_absent "$file" "$token" A6-no-embedded-scripts "$needle"
done
# A7 — UI pack/header and frontend HTML/script bytes. The pack is scanned as
# its own release file; none of its raw frontend bytes belongs in any
# ELF/Mach-O/PE variant. Applying this to standard too makes the boundary a
# product-wide invariant rather than relying on path classification.
for needle in "${UI_ASSET_BYTE_NEEDLES[@]}"; do
assert_absent "$file" "$token" A7-no-ui-asset-bytes "$needle"
done
}
# ── Walk the targets ────────────────────────────────────────────────
+3 -3
View File
@@ -26,7 +26,7 @@ artifact through the canonical wrapper, exactly like the release venue:
windows: test-infrastructure/vm/vm-smoke.sh with CBM_SMOKE_ARTIFACT_DIR
(run inside the VM/CI msys2 shell)
--variant ui builds --with-ui and smokes the ui archive (Phase 15 external
--variant ui builds --with-ui and smokes the ui archive (Phase 15 embedded
assets become mandatory — a standard binary cannot pass a ui run).
Make passthrough (VAR=VAL): CC= CXX= STATIC=1 ... forwarded to build steps.
@@ -104,8 +104,8 @@ 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"
# ONE executable per runtime set: a payload sibling means the removed
# launcher split came back.
# 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" \
+3
View File
@@ -30,6 +30,9 @@ rm -rf "$ROOT/graph-ui/node_modules"
# Root-level node artifacts (if any)
rm -rf "$ROOT/node_modules"
# Generated embedded assets (regenerated by embed-frontend.sh)
rm -f "$ROOT/src/ui/embedded_assets.c"
# Leftover test fixture dirs (C test suite sometimes creates these in CWD)
find "$ROOT" -maxdepth 1 -type d \( -name 'cbm_*' -o -name 'cli-*' \) -exec rm -rf {} + 2>/dev/null || true
+194
View File
@@ -0,0 +1,194 @@
#!/usr/bin/env bash
# embed-frontend.sh — Convert built frontend assets into linkable object files.
#
# Usage: scripts/embed-frontend.sh <dist_dir> <output_dir>
#
# For each file in dist_dir, creates:
# 1. An object file via `ld -r -b binary` (raw bytes, zero bloat)
# 2. A generated embedded_assets.c with a lookup table
#
# Symbols created per file: _binary_<mangled_name>_start, _binary_<mangled_name>_end
# On macOS, these get a leading underscore: __binary_<mangled_name>_start
set -euo pipefail
DIST_DIR="${1:?Usage: embed-frontend.sh <dist_dir> <output_dir>}"
OUTPUT_DIR="${2:?Usage: embed-frontend.sh <dist_dir> <output_dir>}"
# Clean old embedded objects (asset hashes change on each build)
rm -rf "$OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR"
# Detect platform — Linux uses ld -r -b binary, everything else uses xxd+cc
IS_LINUX=false
if [[ "$(uname -s)" == "Linux" ]] && ! [[ "$(uname -s)" =~ MINGW|MSYS ]]; then
IS_LINUX=true
fi
# Content-type detection
content_type_for() {
local f="$1"
case "$f" in
*.html) echo "text/html" ;;
*.js) echo "application/javascript" ;;
*.css) echo "text/css" ;;
*.json) echo "application/json" ;;
*.svg) echo "image/svg+xml" ;;
*.png) echo "image/png" ;;
*.ico) echo "image/x-icon" ;;
*.woff2) echo "font/woff2" ;;
*.woff) echo "font/woff" ;;
*.map) echo "application/json" ;;
*) echo "application/octet-stream" ;;
esac
}
# Mangle filename to valid C symbol: replace non-alnum with _
mangle() {
echo "$1" | sed 's/[^a-zA-Z0-9]/_/g'
}
# Collect all files
FILES=()
while IFS= read -r -d '' file; do
FILES+=("$file")
done < <(find "$DIST_DIR" -type f -print0 | sort -z)
if [[ ${#FILES[@]} -eq 0 ]]; then
echo "Error: no files found in $DIST_DIR" >&2
exit 1
fi
echo "Embedding ${#FILES[@]} files from $DIST_DIR"
# Generate object files
OBJ_FILES=()
for file in "${FILES[@]}"; do
rel="${file#$DIST_DIR/}"
mangled=$(mangle "$rel")
obj="$OUTPUT_DIR/embed_${mangled}.o"
if $IS_LINUX; then
# Linux: ld -r -b binary (zero bloat, ELF only)
abs_obj="$(cd "$(dirname "$0")/.." && pwd)/$obj"
(cd "$DIST_DIR" && ld -r -b binary -o "$abs_obj" "$rel")
else
# macOS/Windows/MSYS2: generate C byte array + cc (no xxd dependency)
local_c="$OUTPUT_DIR/embed_${mangled}.c"
local_sym="_binary_${mangled}"
echo "/* Generated from $rel */" > "$local_c"
echo "const unsigned char ${local_sym}_data[] = {" >> "$local_c"
# Use od (POSIX) to generate hex bytes — works everywhere without xxd/vim
od -An -tx1 -v < "$file" | tr -s ' ' '\n' | grep -v '^$' | sed 's/^/0x/; s/$/,/' | paste -sd' ' - | fold -s -w 76 | sed 's/^/ /' >> "$local_c"
echo "};" >> "$local_c"
echo "const unsigned int ${local_sym}_size = sizeof(${local_sym}_data);" >> "$local_c"
${CC:-cc} -c -O2 -o "$obj" "$local_c"
rm -f "$local_c"
fi
OBJ_FILES+=("$obj")
echo " $rel -> $obj"
done
# Generate embedded_assets.c
ASSETS_C="src/ui/embedded_assets.c"
cat > "$ASSETS_C" <<'HEADER'
/*
* embedded_assets.c — Generated file mapping URL paths to embedded bytes.
* DO NOT EDIT — regenerated by scripts/embed-frontend.sh
*/
#include "ui/embedded_assets.h"
#include <string.h>
HEADER
# Declare extern symbols
for file in "${FILES[@]}"; do
rel="${file#$DIST_DIR/}"
mangled=$(mangle "$rel")
sym="_binary_${mangled}"
if ! $IS_LINUX; then
echo "extern const unsigned char ${sym}_data[];" >> "$ASSETS_C"
echo "extern const unsigned int ${sym}_size;" >> "$ASSETS_C"
else
echo "extern const unsigned char ${sym}_start[];" >> "$ASSETS_C"
echo "extern const unsigned char ${sym}_end[];" >> "$ASSETS_C"
fi
done
echo "" >> "$ASSETS_C"
echo "cbm_embedded_file_t CBM_EMBEDDED_FILES[] = {" >> "$ASSETS_C"
for file in "${FILES[@]}"; do
rel="${file#$DIST_DIR/}"
mangled=$(mangle "$rel")
sym="_binary_${mangled}"
ct=$(content_type_for "$rel")
# URL path: /index.html for root, /assets/... for assets
url_path="/$rel"
if ! $IS_LINUX; then
echo " {\"$url_path\", ${sym}_data, 0, \"$ct\"}," >> "$ASSETS_C"
else
echo " {\"$url_path\", ${sym}_start, 0, \"$ct\"}," >> "$ASSETS_C"
fi
done
echo "};" >> "$ASSETS_C"
echo "const int CBM_EMBEDDED_FILE_COUNT = ${#FILES[@]};" >> "$ASSETS_C"
# Generate size fixup
if $IS_LINUX; then
# Linux: compute size from start/end pointers (ld -r -b binary symbols)
cat >> "$ASSETS_C" <<'SIZEINIT'
static void __attribute__((constructor)) init_embedded_sizes(void) {
cbm_embedded_file_t *files = CBM_EMBEDDED_FILES;
SIZEINIT
for i in "${!FILES[@]}"; do
rel="${FILES[$i]#$DIST_DIR/}"
mangled=$(mangle "$rel")
sym="_binary_${mangled}"
echo " files[$i].size = (unsigned int)(${sym}_end - ${sym}_start);" >> "$ASSETS_C"
done
echo "}" >> "$ASSETS_C"
else
# macOS/Windows: use explicit _size vars from xxd-generated C arrays
cat >> "$ASSETS_C" <<'SIZEINIT'
static void __attribute__((constructor)) init_embedded_sizes(void) {
cbm_embedded_file_t *files = CBM_EMBEDDED_FILES;
SIZEINIT
for i in "${!FILES[@]}"; do
rel="${FILES[$i]#$DIST_DIR/}"
mangled=$(mangle "$rel")
sym="_binary_${mangled}"
echo " files[$i].size = ${sym}_size;" >> "$ASSETS_C"
done
echo "}" >> "$ASSETS_C"
fi
# Add lookup function
cat >> "$ASSETS_C" <<'LOOKUP'
const cbm_embedded_file_t *cbm_embedded_lookup(const char *path) {
for (int i = 0; i < CBM_EMBEDDED_FILE_COUNT; i++) {
if (strcmp(CBM_EMBEDDED_FILES[i].path, path) == 0) {
return &CBM_EMBEDDED_FILES[i];
}
}
return NULL;
}
LOOKUP
echo "Generated $ASSETS_C with ${#FILES[@]} embedded files"
echo "Object files in $OUTPUT_DIR:"
printf ' %s\n' "${OBJ_FILES[@]}"
-54
View File
@@ -1,54 +0,0 @@
#!/bin/sh
# gen-integrations-hash.sh — emit the build-time integrity constant for
# assets/cbm-integrations.json.
#
# The binary embeds exactly ONE fact about the integration templates: the
# SHA-256 of the shipped asset file. Verifying the file against this constant
# before use preserves the tamper-evidence the templates had while they were
# compiled in — moving them out of the binary (AV-surface reduction) must not
# turn them into unauthenticated code the installer happily writes to disk.
#
# Generated, never hand-maintained: a stale constant is exactly the silent
# drift this step exists to prevent, so the header is a build product with a
# real dependency on the JSON file.
set -eu
if [ "$#" -ne 2 ]; then
echo "usage: gen-integrations-hash.sh <asset.json> <out-header.h>" >&2
exit 2
fi
asset="$1"
out="$2"
[ -f "$asset" ] || { echo "gen-integrations-hash: missing $asset" >&2; exit 1; }
if command -v sha256sum >/dev/null 2>&1; then
hash=$(sha256sum "$asset" | cut -d' ' -f1)
elif command -v shasum >/dev/null 2>&1; then
hash=$(shasum -a 256 "$asset" | cut -d' ' -f1)
else
echo "gen-integrations-hash: neither sha256sum nor shasum available" >&2
exit 1
fi
case "$hash" in
*[!0-9a-f]* | "")
echo "gen-integrations-hash: bad digest '$hash'" >&2
exit 1
;;
esac
if [ "${#hash}" -ne 64 ]; then
echo "gen-integrations-hash: bad digest length for '$hash'" >&2
exit 1
fi
mkdir -p "$(dirname "$out")"
tmp="$out.tmp.$$"
{
echo "/* Generated by scripts/gen-integrations-hash.sh — do not edit. */"
echo "#ifndef CBM_INTEGRATIONS_HASH_H"
echo "#define CBM_INTEGRATIONS_HASH_H"
echo "#define CBM_INTEGRATIONS_SHA256 \"$hash\""
echo "#endif"
} >"$tmp"
mv "$tmp" "$out"
+3 -5
View File
@@ -1,5 +1,5 @@
{
"_comment": "License allow-list for the CI license gate (scripts/license-gate.sh). A single detected SPDX id outside this list fails the pipeline. Additions to this list are deliberate, reviewed changes \u2014 never add copyleft (GPL/LGPL/AGPL/EPL/SSPL) without a maintainer decision.",
"_comment": "License allow-list for the CI license gate (scripts/license-gate.sh). A single detected SPDX id outside this list fails the pipeline. Additions to this list are deliberate, reviewed changes never add copyleft (GPL/LGPL/AGPL/EPL/SSPL) without a maintainer decision.",
"allowed_spdx_ids": [
"MIT",
"MIT-0",
@@ -16,15 +16,13 @@
"LicenseRef-scancode-public-domain",
"LicenseRef-scancode-public-domain-disclaimer"
],
"_ignored_paths_comment": "Path prefixes (relative to the staged scan tree) excluded from the gate. Use ONLY for documented false positives. Justifications: the license tooling itself (gate scripts + this policy file) necessarily names prohibited licenses; gen-third-party-notices.sh echoes license terminology; audit-license-provenance.py names licenses in its verdict maps; src/discover/discover.c contains a license-FILENAME classification list (LICENSE-MIT, LICENSE-APACHE, ...) for file discovery, which ScanCode reads as license references; package-release.sh and smoke-local.sh list the archive members 'cbm-integrations.json LICENSE ...' adjacently (the order the Windows one-executable runtime-set contract locks, mirrored by the smoke fixture), and ScanCode reads the '.json LICENSE' token adjacency as a reference to the SPDX 'JSON' license — first-party MIT build scripts, not vendored code.",
"_ignored_paths_comment": "Path prefixes (relative to the staged scan tree) excluded from the gate. Use ONLY for documented false positives. Justifications: the license tooling itself (gate scripts + this policy file) necessarily names prohibited licenses; gen-third-party-notices.sh echoes license terminology; audit-license-provenance.py names licenses in its verdict maps; src/discover/discover.c contains a license-FILENAME classification list (LICENSE-MIT, LICENSE-APACHE, ...) for file discovery, which ScanCode reads as license references, which ScanCode reads as license references.",
"ignored_paths": [
"tree/scripts/license-policy.json",
"tree/scripts/license-gate-check.py",
"tree/scripts/license-gate.sh",
"tree/scripts/gen-third-party-notices.sh",
"tree/src/discover/discover.c",
"tree/scripts/audit-license-provenance.py",
"tree/scripts/package-release.sh",
"tree/scripts/smoke-local.sh"
"tree/scripts/audit-license-provenance.py"
]
}
-211
View File
@@ -1,211 +0,0 @@
#!/usr/bin/env node
/*
* Build a deterministic, uncompressed, content-addressed UI asset pack.
*
* The native binary receives only the pack name, exact byte length and
* SHA-256. Frontend bytes stay outside ELF/Mach-O/PE and stay unobscured for
* scanners, SBOM tools and human inspection.
*/
import crypto from 'node:crypto';
import fs from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
const HEADER_BYTES = 80;
const ENTRY_BYTES = 24;
const FORMAT_VERSION = 1;
const MAX_FILES = 1024;
const MAX_PATH_BYTES = 255;
const MAX_PACK_BYTES = 64 * 1024 * 1024;
const MIME_BY_EXTENSION = new Map([
['.html', 1],
['.js', 2],
['.mjs', 2],
['.css', 3],
['.json', 4],
['.svg', 5],
['.png', 6],
['.jpg', 7],
['.jpeg', 7],
['.webp', 8],
['.avif', 9],
['.ico', 10],
['.woff2', 11],
['.woff', 12],
['.wasm', 13],
]);
function fail(message) {
throw new Error(`pack-ui-assets: ${message}`);
}
function compareBytewise(left, right) {
return Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8'));
}
function validateUrlPath(urlPath) {
const bytes = Buffer.from(urlPath, 'utf8');
if (bytes.length < 2 || bytes.length > MAX_PATH_BYTES ||
!/^[A-Za-z0-9._/-]+$/.test(urlPath) || urlPath.includes('//') ||
urlPath.endsWith('/') || urlPath.includes('/./') || urlPath.includes('/../') ||
urlPath.endsWith('/.') || urlPath.endsWith('/..')) {
fail(`unsupported frontend path ${JSON.stringify(urlPath)}`);
}
if (urlPath !== '/index.html' && !urlPath.startsWith('/assets/')) {
fail(`frontend output must contain only /index.html and /assets/* (got ${urlPath})`);
}
return bytes;
}
async function collectFiles(root, directory = root, relative = '') {
const dirEntries = await fs.readdir(directory, { withFileTypes: true });
dirEntries.sort((left, right) => compareBytewise(left.name, right.name));
const files = [];
for (const entry of dirEntries) {
const absolute = path.join(directory, entry.name);
const childRelative = relative ? path.posix.join(relative, entry.name) : entry.name;
if (entry.isSymbolicLink()) {
fail(`symbolic links are not allowed (${childRelative})`);
}
if (entry.isDirectory()) {
files.push(...await collectFiles(root, absolute, childRelative));
} else if (entry.isFile()) {
const urlPath = `/${childRelative}`;
const pathBytes = validateUrlPath(urlPath);
const extension = path.posix.extname(childRelative).toLowerCase();
const mimeId = MIME_BY_EXTENSION.get(extension);
if (!mimeId) {
fail(`no closed MIME mapping for ${urlPath}`);
}
const data = await fs.readFile(absolute);
if (data.length === 0) {
fail(`zero-length frontend assets are not supported (${urlPath})`);
}
files.push({
urlPath,
pathBytes,
mimeId,
cacheId: urlPath === '/index.html' ? 1 : 2,
data,
});
} else {
fail(`unsupported filesystem entry (${childRelative})`);
}
}
return files;
}
function writeU64(buffer, value, offset) {
if (!Number.isSafeInteger(value) || value < 0) fail('pack size exceeds safe integer range');
buffer.writeBigUInt64LE(BigInt(value), offset);
}
function buildPack(files) {
files.sort((left, right) => compareBytewise(left.urlPath, right.urlPath));
if (files.length === 0 || files.length > MAX_FILES) {
fail(`frontend file count must be between 1 and ${MAX_FILES}`);
}
if (files.filter((file) => file.urlPath === '/index.html').length !== 1) {
fail('frontend output must contain exactly one /index.html');
}
for (let index = 1; index < files.length; index += 1) {
if (compareBytewise(files[index - 1].urlPath, files[index].urlPath) >= 0) {
fail(`duplicate or unsorted path ${files[index].urlPath}`);
}
}
const indexBytes = files.length * ENTRY_BYTES;
const pathsBytes = files.reduce((total, file) => total + file.pathBytes.length, 0);
const payloadBytes = files.reduce((total, file) => total + file.data.length, 0);
const indexOffset = HEADER_BYTES;
const pathsOffset = indexOffset + indexBytes;
const payloadOffset = pathsOffset + pathsBytes;
const totalBytes = payloadOffset + payloadBytes;
if (totalBytes > MAX_PACK_BYTES) {
fail(`pack exceeds ${MAX_PACK_BYTES} bytes`);
}
const pack = Buffer.alloc(totalBytes);
pack.write('CBMUIPK', 0, 7, 'ascii');
pack[7] = 0;
pack.writeUInt16LE(FORMAT_VERSION, 8);
pack.writeUInt16LE(HEADER_BYTES, 10);
pack.writeUInt32LE(0, 12);
pack.writeUInt32LE(files.length, 16);
pack.writeUInt32LE(ENTRY_BYTES, 20);
writeU64(pack, indexOffset, 24);
writeU64(pack, indexBytes, 32);
writeU64(pack, pathsOffset, 40);
writeU64(pack, pathsBytes, 48);
writeU64(pack, payloadOffset, 56);
writeU64(pack, payloadBytes, 64);
writeU64(pack, totalBytes, 72);
let pathCursor = 0;
let payloadCursor = 0;
files.forEach((file, index) => {
const entryOffset = indexOffset + index * ENTRY_BYTES;
pack.writeUInt32LE(pathCursor, entryOffset);
pack.writeUInt16LE(file.pathBytes.length, entryOffset + 4);
pack.writeUInt8(file.mimeId, entryOffset + 6);
pack.writeUInt8(file.cacheId, entryOffset + 7);
writeU64(pack, payloadCursor, entryOffset + 8);
writeU64(pack, file.data.length, entryOffset + 16);
file.pathBytes.copy(pack, pathsOffset + pathCursor);
file.data.copy(pack, payloadOffset + payloadCursor);
pathCursor += file.pathBytes.length;
payloadCursor += file.data.length;
});
return pack;
}
async function writeAtomically(destination, bytes) {
const temporary = `${destination}.tmp-${process.pid}`;
try {
await fs.writeFile(temporary, bytes, { flag: 'wx', mode: 0o644 });
await fs.rename(temporary, destination);
} catch (error) {
await fs.rm(temporary, { force: true });
throw error;
}
}
async function main() {
const [sourceRoot, outputDirectory, manifestPath, ...extra] = process.argv.slice(2);
if (!sourceRoot || !outputDirectory || !manifestPath || extra.length !== 0) {
fail('usage: pack-ui-assets.mjs <dist-dir> <output-dir> <manifest.c>');
}
const rootStat = await fs.lstat(sourceRoot);
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
fail('dist-dir must be a real directory, not a symlink');
}
const files = await collectFiles(sourceRoot);
const pack = buildPack(files);
const digest = crypto.createHash('sha256').update(pack).digest('hex');
const packName = `cbm-ui-${digest}.pack`;
const manifest = Buffer.from(
'/* Generated by scripts/pack-ui-assets.mjs; do not edit. */\n' +
'#include <stdint.h>\n\n' +
`const char CBM_UI_ASSET_PACK_NAME[] = "${packName}";\n` +
`const char CBM_UI_ASSET_SHA256[] = "${digest}";\n` +
`const uint64_t CBM_UI_ASSET_SIZE = UINT64_C(${pack.length});\n`,
'utf8',
);
await fs.mkdir(outputDirectory, { recursive: true });
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
const packPath = path.join(outputDirectory, packName);
await writeAtomically(packPath, pack);
await writeAtomically(manifestPath, manifest);
const oldPacks = (await fs.readdir(outputDirectory))
.filter((name) => /^cbm-ui-[0-9a-f]{64}\.pack$/.test(name) && name !== packName);
await Promise.all(oldPacks.map((name) => fs.rm(path.join(outputDirectory, name))));
process.stdout.write(`${packName} ${pack.length} bytes ${files.length} files\n`);
}
main().catch((error) => {
process.stderr.write(`${error.message}\n`);
process.exitCode = 1;
});
+12 -101
View File
@@ -16,8 +16,7 @@ cd "$ROOT"
usage() {
cat <<'EOF'
Usage: scripts/package-release.sh <goos> <goarch> [--variant standard|ui]
[--out-dir DIR] [VAR=VAL ...]
Usage: scripts/package-release.sh <goos> <goarch> [--out-dir DIR] [VAR=VAL ...]
The canonical release-archive step: identical in the release build and the
local artifact-flow smoke lane.
@@ -25,9 +24,6 @@ local artifact-flow smoke lane.
goos linux | darwin | windows
goarch arch label used verbatim in the archive name (amd64, arm64,
arm64-portable, ...)
--variant standard (default) | ui — selects the archive NAME prefix; the
matching binary must already have been built (--with-ui for ui).
UI archives add exactly one root-level cbm-ui-<sha256>.pack.
--out-dir where to place the archive (default: repository root).
Make passthrough (VAR=VAL, forwarded to the build):
@@ -36,40 +32,27 @@ Make passthrough (VAR=VAL, forwarded to the build):
Environment:
BUILD_DIR build tree to archive from (default build/c).
Archive contents (defined here, canonical) — ONE executable per runtime set:
unix: codebase-memory-mcp cbm-integrations.json LICENSE install.sh
THIRD_PARTY_NOTICES.md [cbm-ui-<sha256>.pack] (.tar.gz)
windows: codebase-memory-mcp.exe cbm-integrations.json LICENSE install.ps1
THIRD_PARTY_NOTICES.md [cbm-ui-<sha256>.pack] (.zip)
Archive contents (defined here, canonical) — ONE executable, no sidecars:
unix: codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md (.tar.gz)
windows: codebase-memory-mcp.exe LICENSE install.ps1 THIRD_PARTY_NOTICES.md (.zip)
The bracketed pack is required only for the ui variant and forbidden from the
standard archive. The pack FORMAT is uncompressed before archiving; the tar/zip
container may compress it. Release extraction retains it as a standalone input
so scanners can inspect the frontend independently of the native image.
cbm-integrations.json is the integration-template data file: the binary
embeds only its SHA-256 and refuses to install integrations without a
verified copy, so an archive without it produces a binary that cannot
install. It ships NEXT TO the binary — the resolution path install.sh /
install.ps1 rely on when they run `install` from the extracted archive.
Only one build variant ships: the binary carries the graph UI and the agent
integration templates inside itself, so an extracted archive is immediately
complete — no adjacent data file has to resolve for `install` to work.
EOF
}
GOOS=""
GOARCH=""
VARIANT="standard"
OUT_DIR="$ROOT"
MAKE_ARGS=()
expect_value=""
for arg in "$@"; do
case "$expect_value" in
variant) VARIANT="$arg"; expect_value=""; continue ;;
out-dir) OUT_DIR="$arg"; expect_value=""; continue ;;
esac
case "$arg" in
-h | --help) usage; exit 0 ;;
--variant) expect_value="variant" ;;
--variant=*) VARIANT="${arg#--variant=}" ;;
--out-dir) expect_value="out-dir" ;;
--out-dir=*) OUT_DIR="${arg#--out-dir=}" ;;
-*)
@@ -92,66 +75,11 @@ case "$GOOS" in
linux | darwin | windows) ;;
*) echo "package-release: goos must be linux, darwin or windows." >&2; exit 2 ;;
esac
case "$VARIANT" in
standard) SUFFIX="" ;;
ui) SUFFIX="-ui" ;;
*) echo "package-release: variant must be 'standard' or 'ui'." >&2; exit 2 ;;
esac
[ -n "$expect_value" ] && { echo "package-release: --$expect_value needs a value." >&2; exit 2; }
BUILD_DIR="${BUILD_DIR:-build/c}"
OUT_DIR="$(mkdir -p "$OUT_DIR" && cd "$OUT_DIR" && pwd)"
NAME="codebase-memory-mcp${SUFFIX}-${GOOS}-${GOARCH}"
sha256_file() {
local path="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$path" | awk '{print $1}'
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$path" | awk '{print $1}'
else
echo "package-release: sha256sum or shasum is required to verify UI packs" >&2
return 1
fi
}
verify_ui_pack_digest() {
local path="$1"
local name expected actual
name="$(basename "$path")"
expected="${name#cbm-ui-}"
expected="${expected%.pack}"
if ! actual="$(sha256_file "$path")"; then
return 1
fi
actual="$(printf '%s' "$actual" | tr 'A-F' 'a-f')"
if ! [[ "$actual" =~ ^[0-9a-f]{64}$ ]] || [ "$actual" != "$expected" ]; then
echo "package-release: UI pack digest does not match its filename: $name" >&2
return 1
fi
}
# Resolve the content-addressed UI sidecar before touching the binary. A loose
# glob is not the contract: every matching candidate must have the exact owned
# name shape, and a UI archive must contain exactly one of them.
UI_PACK_NAME=""
UI_PACK_SOURCE=""
if [ "$VARIANT" = "ui" ]; then
shopt -s nullglob
UI_PACK_CANDIDATES=("$BUILD_DIR"/cbm-ui-*.pack)
shopt -u nullglob
if [ "${#UI_PACK_CANDIDATES[@]}" -ne 1 ]; then
echo "package-release: ui build must contain exactly one cbm-ui-<sha256>.pack" >&2
exit 2
fi
UI_PACK_SOURCE="${UI_PACK_CANDIDATES[0]}"
UI_PACK_NAME="$(basename "$UI_PACK_SOURCE")"
if ! [[ "$UI_PACK_NAME" =~ ^cbm-ui-[0-9a-f]{64}\.pack$ ]] || [ ! -s "$UI_PACK_SOURCE" ]; then
echo "package-release: invalid UI asset pack: $UI_PACK_SOURCE" >&2
exit 2
fi
verify_ui_pack_digest "$UI_PACK_SOURCE" || exit 2
fi
NAME="codebase-memory-mcp-${GOOS}-${GOARCH}"
# Ship every release binary stripped. Production already builds without -g, but
# the linker still keeps a ~536 KB .symtab, so releases carried their full
@@ -246,46 +174,29 @@ strip_release_binary "$STAGED_BINARY" || exit 2
# Gate the artifact AFTER strip/re-sign: this is the final executable image and
# no later step may mutate it.
bash scripts/ci/check-binary-composition.sh --variant="$VARIANT" \
"$STAGED_BINARY" || exit 2
bash scripts/ci/check-binary-composition.sh "$STAGED_BINARY" || exit 2
# Stage the exact runtime association before any archive is created. Both
# sidecars are regular copies in an otherwise empty private directory. The UI
# pack's filename is self-authenticating, but only the executable knows whether
# that otherwise-valid pack (and integration manifest) belong to THIS build.
cp assets/cbm-integrations.json "$PACK_DIR/cbm-integrations.json"
if [ "$VARIANT" = "ui" ]; then
cp "$UI_PACK_SOURCE" "$PACK_DIR/$UI_PACK_NAME"
fi
cp LICENSE "$INSTALLER" "$PACK_DIR/"
scripts/gen-third-party-notices.sh "$PACK_DIR/THIRD_PARTY_NOTICES.md"
if ! "$STAGED_BINARY" --verify-runtime-assets; then
echo "package-release: staged binary rejected its adjacent runtime assets; refusing archive" >&2
exit 2
fi
echo "=== package-release: staged runtime assets match $STAGED_BINARY_NAME ==="
if [ "$GOOS" = "windows" ]; then
(
cd "$PACK_DIR"
rm -f "$OUT_DIR/$NAME.zip"
ARCHIVE_MEMBERS=(
codebase-memory-mcp.exe cbm-integrations.json LICENSE install.ps1
codebase-memory-mcp.exe LICENSE install.ps1
THIRD_PARTY_NOTICES.md
)
[ "$VARIANT" = "ui" ] && ARCHIVE_MEMBERS+=("$UI_PACK_NAME")
zip -q "$OUT_DIR/$NAME.zip" "${ARCHIVE_MEMBERS[@]}"
)
echo "=== package-release: $OUT_DIR/$NAME.zip ==="
else
ARCHIVE_MEMBERS=(
codebase-memory-mcp cbm-integrations.json LICENSE install.sh
codebase-memory-mcp LICENSE install.sh
THIRD_PARTY_NOTICES.md
)
[ "$VARIANT" = "ui" ] && ARCHIVE_MEMBERS+=("$UI_PACK_NAME")
# BSD tar otherwise materializes macOS extended attributes as hidden
# AppleDouble `._*` members, violating the exact five/six-file inventory.
# AppleDouble `._*` members, violating the exact four-file inventory.
COPYFILE_DISABLE=1 tar -czf "$OUT_DIR/$NAME.tar.gz" -C "$PACK_DIR" \
"${ARCHIVE_MEMBERS[@]}"
echo "=== package-release: $OUT_DIR/$NAME.tar.gz ==="
+2 -2
View File
@@ -213,7 +213,7 @@ if ($FromSource) {
Write-Host ""
Write-Host "Building binary (this may take a minute)..." -ForegroundColor White
$wslBinaryPath = "/home/$wslUser/.local/bin/$BinaryName"
Invoke-WSL "mkdir -p /home/$wslUser/.local/bin && cd $sourceDir && scripts/build.sh && cp build/c/codebase-memory-mcp build/c/cbm-integrations.json /home/$wslUser/.local/bin/"
Invoke-WSL "mkdir -p /home/$wslUser/.local/bin && cd $sourceDir && scripts/build.sh && cp build/c/codebase-memory-mcp $wslBinaryPath"
Write-Ok "Built to $wslBinaryPath (inside WSL)"
# Verify
@@ -238,7 +238,7 @@ if ($FromSource) {
Write-Ok "Done! Restart Claude Code and verify with /mcp"
Write-Host ""
Write-Host " To uninstall:" -ForegroundColor White
Write-Host " wsl.exe -- rm $wslBinaryPath /home/$wslUser/.local/bin/cbm-integrations.json"
Write-Host " wsl.exe -- rm $wslBinaryPath"
Write-Host " wsl.exe -- rm -rf $sourceDir"
Write-Host " wsl.exe -- rm -rf ~/.cache/codebase-memory-mcp/"
+1 -18
View File
@@ -144,18 +144,6 @@ fetch() {
fi
}
install_integration_asset() {
local source_dir="$1"
local source_path="${source_dir}/cbm-integrations.json"
if [ ! -f "$source_path" ]; then
die "Runtime asset missing from build or release: cbm-integrations.json"
fi
cp "$source_path" "${INSTALL_DIR}/cbm-integrations.json"
chmod 644 "${INSTALL_DIR}/cbm-integrations.json"
}
download_binary() {
local platform="$1" tool="$2"
@@ -183,7 +171,6 @@ download_binary() {
mkdir -p "$INSTALL_DIR"
mv "${tmpdir}/${BINARY_NAME}" "${INSTALL_DIR}/${BINARY_NAME}"
chmod +x "${INSTALL_DIR}/${BINARY_NAME}"
install_integration_asset "$tmpdir"
ok "Installed to ${INSTALL_DIR}/${BINARY_NAME}"
}
@@ -212,10 +199,7 @@ build_from_source() {
echo "${BOLD}Building binary (this may take a minute)...${RESET}"
mkdir -p "$INSTALL_DIR"
(cd "$SOURCE_DIR" && scripts/build.sh)
cp "${SOURCE_DIR}/build/c/codebase-memory-mcp" "${INSTALL_DIR}/${BINARY_NAME}"
chmod +x "${INSTALL_DIR}/${BINARY_NAME}"
install_integration_asset "${SOURCE_DIR}/build/c"
(cd "$SOURCE_DIR" && scripts/build.sh && cp build/c/codebase-memory-mcp "${INSTALL_DIR}/${BINARY_NAME}")
ok "Built and installed to ${INSTALL_DIR}/${BINARY_NAME}"
}
@@ -347,6 +331,5 @@ ok "Done! Restart Claude Code and verify with /mcp"
echo ""
info "To uninstall:"
info " rm ${INSTALL_DIR}/${BINARY_NAME}"
info " rm ${INSTALL_DIR}/cbm-integrations.json"
info " rm -rf ${SOURCE_DIR} # if built from source"
info " rm -rf ~/.cache/codebase-memory-mcp/ # graph database"
+22 -55
View File
@@ -21,15 +21,14 @@ destination override neutralized.
Arguments:
<binary> Product binary to smoke (e.g. build/c/codebase-memory-mcp).
ui Mirror the -ui asset naming AND require the external UI pack:
ui Mirror the -ui asset naming AND require the embedded UI:
Phase 15's "no assets" outcome becomes a FAILURE
(SMOKE_REQUIRE_UI=1), so a standard binary cannot pass a ui run.
Environment:
CBM_SMOKE_ARTIFACT_DIR Release mode: an EXTRACTED release artifact
directory. Sidecars (LICENSE, install.sh, THIRD_PARTY_NOTICES.md,
and the UI pack when selected) are validated and served from
THERE instead of regenerated, so
directory. Sidecars (LICENSE, install.sh, THIRD_PARTY_NOTICES.md)
are validated and served from THERE instead of regenerated, so
the release venue smokes exactly the bytes it publishes; an
incomplete archive fails the smoke. Unset (default): sidecars
come from this checkout (local/PR mode).
@@ -57,9 +56,9 @@ if [ ! -x "$BINARY" ]; then
echo "smoke-local: binary is not executable: $BINARY" >&2
exit 2
fi
# A ui run must be handed a binary with exactly one external asset pack beside
# it; the suffix alone only renames the archive. SMOKE_REQUIRE_UI turns Phase
# 15's documented "no UI assets" SKIP into a failure, so asking for ui and
# A ui run must be handed a binary that actually carries the embedded assets;
# the suffix alone only renames the archive. SMOKE_REQUIRE_UI turns Phase 15's
# documented "no embedded assets" SKIP into a failure, so asking for ui and
# supplying a standard binary can no longer pass quietly.
case "$VARIANT" in
standard) SUFFIX="" ; REQUIRE_UI=0 ;;
@@ -73,7 +72,7 @@ esac
ARTIFACT_DIR="${CBM_SMOKE_ARTIFACT_DIR:-}"
if [ -n "$ARTIFACT_DIR" ]; then
ARTIFACT_DIR="$(cd "$ARTIFACT_DIR" && pwd)"
for required in cbm-integrations.json LICENSE install.sh THIRD_PARTY_NOTICES.md; do
for required in LICENSE install.sh THIRD_PARTY_NOTICES.md; do
[ -s "$ARTIFACT_DIR/$required" ] || {
echo "smoke-local: release artifact is missing $required" >&2
exit 2
@@ -81,37 +80,6 @@ if [ -n "$ARTIFACT_DIR" ]; then
done
fi
UI_PACK_NAME=""
UI_PACK_SOURCE=""
UI_PACK_COUNT=0
if [ -n "$ARTIFACT_DIR" ]; then
UI_PACK_SOURCE_DIR="$ARTIFACT_DIR"
else
UI_PACK_SOURCE_DIR="$(dirname "$BINARY")"
fi
shopt -s nullglob
UI_PACK_CANDIDATES=("$UI_PACK_SOURCE_DIR"/cbm-ui-*.pack)
shopt -u nullglob
for candidate in "${UI_PACK_CANDIDATES[@]+"${UI_PACK_CANDIDATES[@]}"}"; do
UI_PACK_COUNT=$((UI_PACK_COUNT + 1))
UI_PACK_SOURCE="$candidate"
candidate_name="$(basename "$candidate")"
if ! [[ "$candidate_name" =~ ^cbm-ui-[0-9a-f]{64}\.pack$ ]]; then
echo "smoke-local: invalid UI pack name beside artifact: $candidate_name" >&2
exit 2
fi
done
if [ "$VARIANT" = "ui" ]; then
if [ "$UI_PACK_COUNT" -ne 1 ] || [ ! -s "$UI_PACK_SOURCE" ]; then
echo "smoke-local: ui fixture requires exactly one content-addressed UI pack" >&2
exit 2
fi
UI_PACK_NAME="$(basename "$UI_PACK_SOURCE")"
elif [ "$UI_PACK_COUNT" -ne 0 ]; then
echo "smoke-local: standard fixture must not contain a UI asset pack" >&2
exit 2
fi
case "$(uname -s)" in
Darwin) OS=darwin ;;
Linux) OS=linux ;;
@@ -155,34 +123,33 @@ mkdir -p "$FIXTURE_DIR" "$SMOKE_TEMP_DIR" "$SMOKE_HOME" "$SMOKE_XDG_CONFIG" \
"$SMOKE_APPDATA" "$SMOKE_LOCALAPPDATA"
cp "$BINARY" "$FIXTURE_DIR/codebase-memory-mcp"
if [ -n "$ARTIFACT_DIR" ]; then
cp "$ARTIFACT_DIR/cbm-integrations.json" "$ARTIFACT_DIR/LICENSE" \
cp "$ARTIFACT_DIR/LICENSE" \
"$ARTIFACT_DIR/install.sh" "$ARTIFACT_DIR/THIRD_PARTY_NOTICES.md" "$FIXTURE_DIR/"
else
cp "$ROOT/assets/cbm-integrations.json" "$ROOT/LICENSE" "$ROOT/install.sh" "$FIXTURE_DIR/"
cp "$ROOT/LICENSE" "$ROOT/install.sh" "$FIXTURE_DIR/"
"$ROOT/scripts/gen-third-party-notices.sh" "$FIXTURE_DIR/THIRD_PARTY_NOTICES.md"
fi
if [ -n "$UI_PACK_SOURCE" ]; then
cp "$UI_PACK_SOURCE" "$FIXTURE_DIR/$UI_PACK_NAME"
fi
# The archive must carry cbm-integrations.json — install verifies it against the
# binary's embedded SHA-256 and fails closed without it. Member set and ORDER
# mirror scripts/package-release.sh (the Windows one-executable runtime-set
# contract locks that order); a fixture that omits it would smoke a release
# layout we never ship.
# Member set and ORDER mirror scripts/package-release.sh (the Windows
# single-binary contract locks that order); a fixture with a different inventory
# would smoke a release layout we never ship.
EXPECTED_ARTIFACT="codebase-memory-mcp${SUFFIX}-${OS}-${ARCH}.tar.gz"
ARCHIVE_MEMBERS=(
codebase-memory-mcp cbm-integrations.json LICENSE install.sh
THIRD_PARTY_NOTICES.md
)
[ -n "$UI_PACK_NAME" ] && ARCHIVE_MEMBERS+=("$UI_PACK_NAME")
tar -czf "$FIXTURE_DIR/$EXPECTED_ARTIFACT" -C "$FIXTURE_DIR" "${ARCHIVE_MEMBERS[@]}"
tar -czf "$FIXTURE_DIR/$EXPECTED_ARTIFACT" -C "$FIXTURE_DIR" \
codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md
if [ -n "$SUFFIX" ]; then
cp "$FIXTURE_DIR/$EXPECTED_ARTIFACT" \
"$FIXTURE_DIR/codebase-memory-mcp-${OS}-${ARCH}.tar.gz"
fi
# Linux install/update resolves the portable release asset even when this local
# smoke started from the dynamic production binary.
if [ "$OS" = "linux" ]; then
cp "$FIXTURE_DIR/$EXPECTED_ARTIFACT" \
"$FIXTURE_DIR/codebase-memory-mcp${SUFFIX}-${OS}-${ARCH}-portable.tar.gz"
if [ -n "$SUFFIX" ]; then
cp "$FIXTURE_DIR/codebase-memory-mcp${SUFFIX}-${OS}-${ARCH}-portable.tar.gz" \
"$FIXTURE_DIR/codebase-memory-mcp-${OS}-${ARCH}-portable.tar.gz"
fi
fi
(cd "$FIXTURE_DIR" && { sha256sum *.tar.gz > checksums.txt 2>/dev/null ||
shasum -a 256 *.tar.gz > checksums.txt; })
+43 -150
View File
@@ -40,32 +40,6 @@ if [ -n "$SMOKE_MODE" ] && [ "$SMOKE_MODE" != "--agent-config-only" ]; then
exit 2
fi
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)"
SMOKE_REQUIRE_UI="${SMOKE_REQUIRE_UI:-0}"
# A UI binary is a two-file runtime contract: native image plus exactly one
# content-addressed pack beside it. Remember that source pack so every fixture
# copy below stages the same sidecar rather than silently testing a binary-only
# layout that no UI release ships.
SMOKE_UI_PACK=""
SMOKE_UI_PACK_COUNT=0
shopt -s nullglob
SMOKE_UI_PACK_CANDIDATES=("$(dirname "$BINARY")"/cbm-ui-*.pack)
shopt -u nullglob
for candidate in "${SMOKE_UI_PACK_CANDIDATES[@]+"${SMOKE_UI_PACK_CANDIDATES[@]}"}"; do
SMOKE_UI_PACK_COUNT=$((SMOKE_UI_PACK_COUNT + 1))
SMOKE_UI_PACK="$candidate"
candidate_name="$(basename "$candidate")"
if ! [[ "$candidate_name" =~ ^cbm-ui-[0-9a-f]{64}\.pack$ ]]; then
echo "FAIL: invalid UI asset pack name beside smoke binary: $candidate_name"
exit 1
fi
done
if [ "$SMOKE_REQUIRE_UI" = "1" ]; then
if [ "$SMOKE_UI_PACK_COUNT" -ne 1 ] || [ ! -s "$SMOKE_UI_PACK" ]; then
echo "FAIL: SMOKE_REQUIRE_UI=1 requires exactly one UI asset pack beside $BINARY"
exit 1
fi
fi
smoke_mktemp_file() {
if [ -n "${SMOKE_TEMP_ROOT:-}" ]; then
@@ -119,14 +93,11 @@ smoke_rmtree() {
return 0
}
# Every platform ships one native binary. UI runtime copies also require their
# external, content-addressed pack beside that binary.
# 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 "$SMOKE_UI_PACK" ]; then
cp "$SMOKE_UI_PACK" "$(dirname "$destination")/$(basename "$SMOKE_UI_PACK")"
fi
}
# Retire the shared account daemon (if one is running) and wait until it
@@ -1090,7 +1061,7 @@ echo "OK: uninstall --dry-run completed"
# handoff: it prints the shipped install script's command and exits 0. An
# in-process updater is structurally a downloader -- fetch archive, extract,
# chmod, exec -- which is impossible on Windows without a second resident
# binary, and is unnecessary dropper-like surface in every platform artifact.
# binary, and is the shape Defender's ML scores as a dropper everywhere else.
echo "--- Phase 6c: update --dry-run ---"
if [[ "$BINARY" == *.exe ]]; then
UPDATE_SCRIPT="install.ps1"
@@ -1369,7 +1340,7 @@ if [[ "$BINARY" == *.exe ]]; then
echo "FAIL 8-0: installed binary missing after install"
exit 1
fi
# ONE executable, one link: a second hard link here would mean the retired
# 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
@@ -2482,11 +2453,11 @@ echo "OK 8aw: all supported Scout/Verify/Auditor profile sets"
echo ""
echo "=== Phase 9: agent config uninstall E2E ==="
# Run uninstall from the installed copy. Uninstall intentionally targets the
# exact executable that invoked it; using the build input here removes the
# harness binary on POSIX and makes every later adversarial case test a missing
# command instead of the product.
UNINSTALL_BINARY="$SELF_PATH"
# Run uninstall (same FAKE_HOME with all configs present)
UNINSTALL_BINARY="$BINARY"
if [[ "$BINARY" == *.exe ]]; then
UNINSTALL_BINARY="$SELF_PATH"
fi
HOME="$FAKE_HOME" \
XDG_CONFIG_HOME="$FAKE_HOME/.config" \
APPDATA="$FAKE_HOME/AppData/Roaming" \
@@ -2885,14 +2856,8 @@ smoke_rmtree "$IDEM_HOME"
# 9b-3: Uninstall without prior install
CLEAN_HOME=$(smoke_mktemp_dir)
mkdir -p "$CLEAN_HOME/.claude" "$CLEAN_HOME/.local/bin"
CLEAN_UNINSTALLER="$CLEAN_HOME/cbm-uninstall-driver"
if [[ "$BINARY" == *.exe ]]; then
CLEAN_UNINSTALLER="$CLEAN_UNINSTALLER.exe"
fi
copy_smoke_binary "$CLEAN_UNINSTALLER"
chmod 755 "$CLEAN_UNINSTALLER"
UNINSTALL_RC=0
UNINSTALL_OUT=$(HOME="$CLEAN_HOME" "$CLEAN_UNINSTALLER" uninstall -y -n 2>&1) || UNINSTALL_RC=$?
UNINSTALL_OUT=$(HOME="$CLEAN_HOME" "$BINARY" uninstall -y -n 2>&1) || UNINSTALL_RC=$?
if [ "$UNINSTALL_RC" -ge 128 ]; then
echo "FAIL 9b-3: uninstall crashed (rc=$UNINSTALL_RC)"
exit 1
@@ -2915,18 +2880,14 @@ smoke_rmtree "$CORRUPT_HOME"
# 9b-8: Double uninstall
DBL_HOME=$(smoke_mktemp_dir)
mkdir -p "$DBL_HOME/.claude" "$DBL_HOME/.local/bin"
DBL_UNINSTALLER="$DBL_HOME/.local/bin/codebase-memory-mcp"
DBL_RETRY_UNINSTALLER="$DBL_HOME/cbm-uninstall-retry"
if [[ "$BINARY" == *.exe ]]; then
DBL_UNINSTALLER="$DBL_UNINSTALLER.exe"
DBL_RETRY_UNINSTALLER="$DBL_RETRY_UNINSTALLER.exe"
fi
copy_smoke_binary "$DBL_UNINSTALLER"
copy_smoke_binary "$DBL_RETRY_UNINSTALLER"
chmod 755 "$DBL_UNINSTALLER" "$DBL_RETRY_UNINSTALLER"
copy_smoke_binary "$DBL_HOME/.local/bin/codebase-memory-mcp"
run_no_crash 9b-8-install env HOME="$DBL_HOME" "$BINARY" install -y
DBL_UNINSTALLER="$BINARY"
if [[ "$BINARY" == *.exe ]]; then
DBL_UNINSTALLER="$DBL_HOME/.local/bin/codebase-memory-mcp.exe"
fi
run_no_crash 9b-8-first env HOME="$DBL_HOME" "$DBL_UNINSTALLER" uninstall -y -n
run_no_crash 9b-8-second env HOME="$DBL_HOME" "$DBL_RETRY_UNINSTALLER" uninstall -y -n
run_no_crash 9b-8-second env HOME="$DBL_HOME" "$BINARY" uninstall -y -n
echo "OK 9b-8: double uninstall doesn't crash"
retire_account_daemon "9b-8-cleanup"
smoke_rmtree "$DBL_HOME"
@@ -3108,23 +3069,18 @@ if [ -n "${SMOKE_DOWNLOAD_URL:-}" ]; then
UPDATE_HOME=$(smoke_mktemp_dir)
mkdir -p "$UPDATE_HOME/.claude" "$UPDATE_HOME/.local/bin"
# This phase stages the binary by hand into a fresh HOME — it does NOT go
# through install.sh or `install`, so nothing populates ~/.cbm/assets. The
# integration asset ships next to the binary in every real layout (archive,
# build tree, installed dir), so stage it next to each staged copy here too;
# otherwise the uninstall this phase drives fails closed rendering templates.
BINARY_ASSET="$(dirname "$BINARY")/cbm-integrations.json"
# through install.sh or `install`. The binary is self-contained, so a staged
# copy is immediately able to render and remove its own integrations.
if [[ "$BINARY" == *.exe ]]; then
copy_smoke_binary "$UPDATE_HOME/.local/bin/codebase-memory-mcp.exe"
cp "$BINARY" "$UPDATE_HOME/.local/bin/codebase-memory-mcp.exe"
mkdir -p "$UPDATE_HOME/retired-install"
copy_smoke_binary "$UPDATE_HOME/retired-install/codebase-memory-mcp.exe"
[ -f "$BINARY_ASSET" ] && cp "$BINARY_ASSET" "$UPDATE_HOME/.local/bin/cbm-integrations.json"
cp "$BINARY" "$UPDATE_HOME/retired-install/codebase-memory-mcp.exe"
else
copy_smoke_binary "$UPDATE_HOME/.local/bin/codebase-memory-mcp"
cp "$BINARY" "$UPDATE_HOME/.local/bin/codebase-memory-mcp"
chmod 755 "$UPDATE_HOME/.local/bin/codebase-memory-mcp"
mkdir -p "$UPDATE_HOME/retired-install"
copy_smoke_binary "$UPDATE_HOME/retired-install/codebase-memory-mcp"
cp "$BINARY" "$UPDATE_HOME/retired-install/codebase-memory-mcp"
chmod 755 "$UPDATE_HOME/retired-install/codebase-memory-mcp"
[ -f "$BINARY_ASSET" ] && cp "$BINARY_ASSET" "$UPDATE_HOME/.local/bin/cbm-integrations.json"
if [ "$(uname -s)" = "Darwin" ]; then
codesign --sign - --force "$UPDATE_HOME/.local/bin/codebase-memory-mcp" 2>/dev/null || true
codesign --sign - --force "$UPDATE_HOME/retired-install/codebase-memory-mcp" \
@@ -3178,10 +3134,9 @@ if [ -n "${SMOKE_DOWNLOAD_URL:-}" ]; then
# Contract, every platform: update NEVER replaces the running image in
# process. It exits 0 and prints the shipped install script's command. On
# Windows regressing this means reintroducing the removed launcher stub;
# Windows regressing this means reintroducing the AV-flagged launcher stub;
# everywhere else it means putting download -> extract -> chmod -> exec back
# into the product binary. Historical scanner results motivated measuring
# this boundary but do not attribute a classifier decision to it.
# into the product binary.
if [ "$UPDATE_RC" -ne 0 ]; then
echo "FAIL 14a: update exited rc=$UPDATE_RC (expected 0)"
exit 1
@@ -3321,7 +3276,6 @@ fi
# Try standard name first, fall back to UI variant
DL_ARCHIVE="codebase-memory-mcp-${DL_OS}-${DL_ARCH}.${DL_EXT}"
DL_ARCHIVE_UI="codebase-memory-mcp-ui-${DL_OS}-${DL_ARCH}.${DL_EXT}"
DL_VARIANT_ARG="--standard"
# 12a: curl download (try standard, then UI variant)
echo "--- Phase 12a: curl download ---"
@@ -3334,7 +3288,6 @@ if ! curl -fSL --noproxy '*' -o "$DL_DIR/$DL_ARCHIVE" "$SMOKE_DOWNLOAD_URL/$DL_A
# Try UI variant
if curl -fSL --noproxy '*' -o "$DL_DIR/$DL_ARCHIVE_UI" "$SMOKE_DOWNLOAD_URL/$DL_ARCHIVE_UI" 2>>"$CURL12_ERR"; then
DL_ARCHIVE="$DL_ARCHIVE_UI"
DL_VARIANT_ARG="--ui"
else
echo "FAIL 12a: curl download failed (tried standard and ui variants)"
echo "--- curl stderr (url: $SMOKE_DOWNLOAD_URL/$DL_ARCHIVE) ---"
@@ -3381,8 +3334,8 @@ 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"
# ONE executable per runtime set: a second executable in the archive would
# mean the removed launcher/payload split came back.
# 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
@@ -3390,20 +3343,6 @@ if [ "$DL_OS" = "windows" ]; then
else
DL_BIN="$DL_DIR/codebase-memory-mcp"
fi
shopt -s nullglob
DL_UI_PACKS=("$DL_DIR"/cbm-ui-*.pack)
shopt -u nullglob
if [ "$DL_VARIANT_ARG" = "--ui" ]; then
if [ "${#DL_UI_PACKS[@]}" -ne 1 ] ||
! [[ "$(basename "${DL_UI_PACKS[0]}")" =~ ^cbm-ui-[0-9a-f]{64}\.pack$ ]]; then
echo "FAIL 12d: UI archive does not contain exactly one content-addressed pack"
exit 1
fi
elif [ "${#DL_UI_PACKS[@]}" -ne 0 ]; then
echo "FAIL 12d: standard archive contains a UI asset pack"
exit 1
fi
if [ ! -f "$DL_BIN" ]; then
echo "FAIL 12d: binary not found after extraction"
exit 1
@@ -3453,7 +3392,7 @@ if [ "$DL_OS" != "windows" ] && [ -f "$REPO_ROOT/install.sh" ]; then
# 13a: run install.sh with local URL + isolated HOME
HOME="$INSTALL_TEST_HOME" CBM_DOWNLOAD_URL="$SMOKE_DOWNLOAD_URL" \
"$REPO_ROOT/install.sh" "$DL_VARIANT_ARG" --dir="$INSTALL_TEST_DIR" 2>&1 || true
"$REPO_ROOT/install.sh" --dir="$INSTALL_TEST_DIR" 2>&1 || true
# 13b: binary placed
if [ ! -f "$INSTALL_TEST_DIR/codebase-memory-mcp" ]; then
@@ -3462,21 +3401,6 @@ if [ "$DL_OS" != "windows" ] && [ -f "$REPO_ROOT/install.sh" ]; then
fi
echo "OK 13b: binary placed"
shopt -s nullglob
INSTALLED_UI_PACKS=("$INSTALL_TEST_DIR"/cbm-ui-*.pack)
shopt -u nullglob
if [ "$DL_VARIANT_ARG" = "--ui" ]; then
if [ "${#INSTALLED_UI_PACKS[@]}" -ne 1 ] ||
! [[ "$(basename "${INSTALLED_UI_PACKS[0]}")" =~ ^cbm-ui-[0-9a-f]{64}\.pack$ ]]; then
echo "FAIL 13b: install.sh did not publish exactly one UI asset pack"
exit 1
fi
echo "OK 13b: UI asset pack placed before binary activation"
elif [ "${#INSTALLED_UI_PACKS[@]}" -ne 0 ]; then
echo "FAIL 13b: standard install retained a UI asset pack"
exit 1
fi
# 13c: binary runs
# Sign if needed on macOS
if [ "$DL_OS" = "darwin" ]; then
@@ -3551,7 +3475,7 @@ elif [ -f "$REPO_ROOT/install.ps1" ] && command -v powershell.exe &>/dev/null; t
if ! HOME="$WIN_HOME" TEMP="$WIN_HOME" TMP="$WIN_HOME" \
CBM_DOWNLOAD_URL="$WIN_URL" CBM_ARCH="$DL_ARCH" MSYS2_ARG_CONV_EXCL='*' \
powershell.exe -NoProfile -ExecutionPolicy Bypass -File \
"$WIN_SCRIPT" "$DL_VARIANT_ARG" "--dir=$WIN_DIR" 2>&1; then
"$WIN_SCRIPT" "--dir=$WIN_DIR" 2>&1; then
echo "FAIL 13f: install.ps1 execution failed"
exit 1
fi
@@ -3568,21 +3492,6 @@ elif [ -f "$REPO_ROOT/install.ps1" ] && command -v powershell.exe &>/dev/null; t
exit 1
fi
shopt -s nullglob
PS1_UI_PACKS=("$PS1_TEST_DIR"/cbm-ui-*.pack)
shopt -u nullglob
if [ "$DL_VARIANT_ARG" = "--ui" ]; then
if [ "${#PS1_UI_PACKS[@]}" -ne 1 ] ||
! [[ "$(basename "${PS1_UI_PACKS[0]}")" =~ ^cbm-ui-[0-9a-f]{64}\.pack$ ]]; then
echo "FAIL 13g: install.ps1 did not publish exactly one UI asset pack"
exit 1
fi
echo "OK 13g: UI asset pack placed before binary activation"
elif [ "${#PS1_UI_PACKS[@]}" -ne 0 ]; then
echo "FAIL 13g: standard install retained a UI asset pack"
exit 1
fi
# 13h: binary runs
if "$PS1_BIN" --version > /dev/null 2>&1; then
echo "OK 13h: binary runs"
@@ -3602,38 +3511,22 @@ else
fi
# ── Phase 15: UI HTTP server reachability ──
# Only runs if the binary has a verified external UI asset pack.
# Only runs if the binary was built with embedded UI assets.
#
# SMOKE_REQUIRE_UI=1 (set by the wrappers for a -ui variant) makes the
# no-assets outcome a FAILURE instead of a SKIP: a ui run that smoked a
# standard binary under a ui name would otherwise pass green, and a skip that
# cannot fail is not a gate.
SMOKE_REQUIRE_UI="${SMOKE_REQUIRE_UI:-0}"
smoke_ui_missing() {
if [ "$SMOKE_REQUIRE_UI" = "1" ]; then
echo "FAIL $1: SMOKE_REQUIRE_UI=1 but this binary serves no external UI assets"
stop_ui_probe
echo "FAIL $1: SMOKE_REQUIRE_UI=1 but this binary serves no embedded UI assets"
kill "$UI_PID" 2>/dev/null || true
exit 1
fi
echo "SKIP $1: $2"
}
# Keep stdin open without putting the product in a background pipeline. Bash
# waits for every member of such a pipeline, so a headless binary that exits
# immediately used to strand the `sleep 300` producer and pause the smoke for
# five minutes. A process-substitution producer has its own PID and can always
# be reaped explicitly on success, skip, or failure.
stop_ui_probe() {
if [ -n "${UI_PID:-}" ]; then
kill "$UI_PID" 2>/dev/null || true
wait "$UI_PID" 2>/dev/null || true
fi
if [ -n "${UI_STDIN_PID:-}" ]; then
kill "$UI_STDIN_PID" 2>/dev/null || true
wait "$UI_STDIN_PID" 2>/dev/null || true
fi
exec 9<&- 2>/dev/null || true
}
echo ""
echo "=== Phase 15: UI HTTP server ==="
@@ -3644,17 +3537,15 @@ UI_PORT=$(python3 -c 'import socket; s = socket.socket(); s.bind(("127.0.0.1", 0
# --ui=true is REQUIRED: the HTTP UI is a persisted, default-off setting, so
# on any fresh profile (every CI runner, every smoke HOME) a bare --port
# invocation can never serve — the probe then misread "UI disabled" as "no
# assets" on binaries with a valid pack (first exposed when the
# embedded assets" on binaries that carry them (first exposed when the
# ui-variant no-skip guard made Phase 15 mandatory). Stdin must be HELD OPEN:
# the UI does not pin the process, so stdio EOF ends it cleanly (rc=0) before
# the poll can see it serve — the drive-listing guard holds a pipe for the
# same reason. Equals-form flags match that guard's proven invocation.
exec 9< <(sleep 300)
UI_STDIN_PID=$!
"$BINARY" --ui=true --port="$UI_PORT" <&9 > /dev/null 2>&1 &
sleep 300 | "$BINARY" --ui=true --port="$UI_PORT" > /dev/null 2>&1 &
UI_PID=$!
# Readiness poll instead of a fixed sleep: SKIP is legitimate ONLY when the
# process exited (the documented no-asset-pack case); a slow start on a
# process exited (the documented no-embedded-assets case); a slow start on a
# loaded runner must not masquerade as it. The UI binds ~6s after launch even
# on a fast host (measured against the release artifact), so the window
# matches the drive-listing guard's 25s, not a 10s sprint.
@@ -3666,15 +3557,15 @@ for _ in $(seq 1 150); do
done
if [ "$UI_READY" -eq 1 ] || kill -0 "$UI_PID" 2>/dev/null; then
# 15a: GET / returns 200 with HTML content from the external pack
# 15a: GET / returns 200 with HTML content
UI_BODY=$(curl -sf "http://127.0.0.1:$UI_PORT/" 2>/dev/null || echo "")
if echo "$UI_BODY" | grep -qi "<html"; then
echo "OK 15a: UI serves HTML at /"
elif [ -z "$UI_BODY" ]; then
smoke_ui_missing "15a" "UI not reachable (binary may not have a valid asset pack)"
smoke_ui_missing "15a" "UI not reachable (binary may not have embedded assets)"
else
echo "FAIL 15a: UI root did not return HTML"
stop_ui_probe
kill "$UI_PID" 2>/dev/null || true
exit 1
fi
@@ -3690,13 +3581,15 @@ if [ "$UI_READY" -eq 1 ] || kill -0 "$UI_PID" 2>/dev/null; then
smoke_ui_missing "15b" "/api/ui-config not reachable"
else
echo "FAIL 15b: /api/ui-config did not return JSON"
stop_ui_probe
kill "$UI_PID" 2>/dev/null || true
exit 1
fi
kill "$UI_PID" 2>/dev/null || true
wait "$UI_PID" 2>/dev/null || true
else
smoke_ui_missing "Phase 15" "binary exited immediately (no UI asset pack loaded)"
smoke_ui_missing "Phase 15" "binary exited immediately (no UI assets embedded)"
fi
stop_ui_probe
echo ""
echo "=== Phase 16: stdio server leaves no orphan after shutdown ==="
+10 -39
View File
@@ -6,8 +6,7 @@
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 native executable inside the same verified runtime-set
layout as Linux and macOS.
Windows ships ONE binary, exactly like Linux and macOS.
Two categories of test:
@@ -22,11 +21,8 @@
* test_daemon_stability.py guards the daemon parameter
surface, crash recovery, busy-stop refusal, and churn
* test_windows_update_handoff.py guards that `update`
hands off to install.ps1 instead of replacing its
currently running executable
* test_daemon_open_readiness.py guards that
`daemon start --open` waits for the verified CBM UI
endpoint while ordinary daemon startup remains asynchronous
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
@@ -50,7 +46,7 @@
.PARAMETER Target
Makefile.cbm target used when building: 'cbm-with-ui' (default; needed for the
drive-picker guard's UI-enabled HTTP service) or 'cbm' (no UI - the drive guard then
drive-picker guard's embedded HTTP UI) or 'cbm' (no UI - the drive guard then
reports a precondition and is skipped).
.PARAMETER GuardsOnly
@@ -93,13 +89,9 @@ function Resolve-Binary {
param([string]$Explicit)
if ($Explicit) { return (Resolve-Path $Explicit).Path }
$built = Join-Path $repoRoot "build\c\codebase-memory-mcp.exe"
if (Test-Path $built) { return $built }
Write-Host "Building $Target via Makefile.cbm ..." -ForegroundColor Cyan
$makeArgs = @(
"-j", "-f", "Makefile.cbm", $Target, "SANITIZE=",
"TMP=$tmp", "TEMP=$tmp", "TMPDIR=$tmp"
)
if ($Target -eq "cbm-with-ui") { $makeArgs += "TEST_SEAMS=1" }
& $Make @makeArgs | Out-Host
& $Make "-j" "-f" "Makefile.cbm" $Target "SANITIZE=" "TMP=$tmp" "TEMP=$tmp" "TMPDIR=$tmp" | Out-Host
$buildExit = $LASTEXITCODE
if ($buildExit -ne 0) { throw "build failed (exit $buildExit)" }
if (-not (Test-Path $built)) { throw "binary not produced at $built" }
@@ -142,22 +134,12 @@ try {
New-Item -ItemType Directory -Path $guardBundle | Out-Null
$guardBin = Join-Path $guardBundle "codebase-memory-mcp.exe"
Copy-Item -LiteralPath $bin -Destination $guardBin
$sourceRuntimeDir = Split-Path -Parent $bin
$sourceIntegration = Join-Path $sourceRuntimeDir "cbm-integrations.json"
if (-not (Test-Path -LiteralPath $sourceIntegration -PathType Leaf)) {
throw "Windows guard source runtime is missing cbm-integrations.json beside $bin"
}
Copy-Item -LiteralPath $sourceIntegration -Destination $guardBundle
Get-ChildItem -LiteralPath $sourceRuntimeDir -File -Filter "cbm-ui-*.pack" |
ForEach-Object { Copy-Item -LiteralPath $_.FullName -Destination $guardBundle }
# Ownership is never inherited on Windows: descendants created under the
# hardened root by an admin-group token can default to the Administrators
# SID, while the exe policy demands the exact current user as owner. Stamp
# the current SID explicitly on everything staged here.
$stagedRuntime = @($guardBundle) + @(Get-ChildItem -LiteralPath $guardBundle -File |
ForEach-Object { $_.FullName })
foreach ($staged in $stagedRuntime) {
foreach ($staged in @($guardBundle, $guardBin)) {
$stagedAcl = Get-Acl -LiteralPath $staged
$stagedAcl.SetOwner($currentSid)
Set-Acl -LiteralPath $staged -AclObject $stagedAcl
@@ -173,9 +155,9 @@ try {
$env:PYTHONUTF8 = "1" # encode argv/stdio as UTF-8
# Green regression guards - must stay GREEN (exit 0). RED (exit 1) = the fix for
# the referenced issue regressed. The drive-picker guard needs the UI-enabled
# build and its external asset pack (target cbm-with-ui); against a non-UI
# binary it reports a precondition (exit 2) and is skipped rather than failed.
# the referenced issue regressed. The drive-picker guard needs the embedded HTTP
# UI (build target cbm-with-ui); against a non-UI binary it reports a precondition
# (exit 2) and is skipped rather than failed.
$guards = @(
"tests\windows\test_non_ascii_path.py",
"tests\windows\test_non_ascii_cache_dump.py",
@@ -186,10 +168,6 @@ $guards = @(
"tests\windows\test_cli_non_ascii_arg.py",
"tests\windows\test_windows_update_handoff.py"
)
$readinessGuard = "tests\test_daemon_open_readiness.py"
if (Get-ChildItem -LiteralPath $guardBundle -File -Filter "cbm-ui-*.pack") {
$guards += $readinessGuard
}
# Opt-in known-red repros - EXPECTED red (exit 1); never gate CI. Currently empty:
# test_cli_non_ascii_arg.py was promoted to a guard when #423/#20's wide-argv fix landed.
@@ -209,13 +187,6 @@ foreach ($t in $guards) {
} 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 -and $t -eq $readinessGuard) {
# This guard must run against a UI runtime set built with TEST_SEAMS=1.
# Treat a missing pack/seam as a broken gate, never a green precondition
# skip; the workflow's guard build deliberately supplies both.
Write-Host "FAILED ($t) exit=2 - UI readiness fixture is incomplete or lacks TEST_SEAMS=1" `
-ForegroundColor Red
$guardFailures += $t
} elseif ($code -eq 2) {
# Exit 2 is the guards' DOCUMENTED precondition-skip contract; every
# other unexpected code (a crashed python, an access-violation status,
+3 -38
View File
@@ -14,7 +14,7 @@ Usage: scripts/test.sh [--suites LIST] [--arch ARCH] [VAR=VAL ...]
The canonical test entry: identical in local CI, PR CI, dry run and release.
DEFAULT (no --suites) is exactly what CI runs: static contract checks
(Step 0a-0r), a CLEAN sanitizer build, every suite via the parallel harness,
(Step 0a-0j), a CLEAN sanitizer build, every suite via the parallel harness,
then the prod-binary regression guards (Steps 4-6).
Modes:
@@ -198,7 +198,7 @@ if [ -n "$SUITES" ]; then
fi
# Step 0: fast build/security harness regressions run before the compiler-heavy
# suite. The Windows package surface is static here; native runtime behavior is
# suite. The Windows package surface is static here; native launcher behavior is
# exercised by scripts/test-windows.ps1.
echo "=== Step 0a: build directory safety contract ==="
bash "$ROOT/tests/test_build_dir_safety.sh"
@@ -212,7 +212,7 @@ bash "$ROOT/tests/test_ui_dev_proxy_security.sh"
echo "=== Step 0d: daemon soak recovery contract ==="
bash "$ROOT/tests/test_soak_daemon_recovery_contract.sh"
echo "=== Step 0e: Windows runtime bundle contract ==="
echo "=== Step 0e: Windows launcher bundle contract ==="
bash "$ROOT/tests/test_windows_bundle_contract.sh"
echo "=== Step 0f: tree-sitter runtime Makefile dependencies ==="
@@ -233,27 +233,6 @@ bash "$ROOT/tests/test_venue_parity_contract.sh"
echo "=== Step 0k: spawn console-window contract (#1427) ==="
bash "$ROOT/tests/test_spawn_no_window_contract.sh"
echo "=== Step 0l: no embedded integration-script text (AV surface) ==="
bash "$ROOT/tests/test_no_embedded_scripts_contract.sh"
echo "=== Step 0m: external UI asset-pack contract ==="
bash "$ROOT/tests/test_ui_asset_pack_contract.sh"
echo "=== Step 0n: complete vendored-integrity contract ==="
bash "$ROOT/tests/test_vendored_integrity_contract.sh"
echo "=== Step 0o: package runtime-asset retention contract ==="
bash "$ROOT/tests/test_package_runtime_assets_contract.sh"
echo "=== Step 0p: exact release archive extraction contract ==="
bash "$ROOT/tests/test_release_archive_extractor_contract.sh"
echo "=== Step 0q: exact-set VirusTotal gate contract ==="
bash "$ROOT/tests/test_vt_gate_zero_tolerance_contract.sh"
echo "=== Step 0r: VirusTotal release-note evidence contract ==="
bash "$ROOT/tests/test_vt_release_notes_contract.sh"
# Verify compiler supports target arch
verify_compiler "$CC"
@@ -319,20 +298,6 @@ CBM_TEST_BINARY="$WATCHDOG_BINARY" bash "$ROOT/tests/test_worker_error_response.
# skipping it silently would hide the gap. Run it by hand:
# make -f Makefile.cbm cbm TEST_SEAMS=1 && bash tests/test_hook_conflict_notice.sh
# Step 5e: `daemon start --open` is the only UI startup operation that waits
# synchronously. Build the real product against a tiny deterministic external
# pack (no npm/network dependency) and prove it waits for the CBM HTTP root,
# rejects a foreign occupied port, and fails closed when the pack is missing.
# This runs here, after the other seam-bearing product guards, so every native
# local-CI leg exercises the exact executable behavior without lengthening the
# daemon's asynchronous startup critical path.
echo "=== Step 5e: daemon UI --open readiness ==="
make -j"$NPROC" -f Makefile.cbm cbm-with-ui TEST_SEAMS=1 \
UI_ASSET_PREBUILT=1 UI_ASSET_DIST="$ROOT/tests/fixtures/ui-readiness" \
UI_ASSET_MANIFEST="$BUILD_DIR/generated/ui_asset_readiness_manifest.c" \
${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}
python3 "$ROOT/tests/test_daemon_open_readiness.py" "$ROOT/$BUILD_DIR/codebase-memory-mcp"
# Step 6: security-strings URL allow-list regression. The MSYS2 CLANG64 toolchain
# bakes its package-tracker URL into the static Windows .exe; the binary string
# audit must allow-list it (Windows-only — Linux smoke never saw it).
+500 -804
View File
File diff suppressed because it is too large Load Diff
-5
View File
@@ -449,11 +449,6 @@ int cbm_cmd_install(int argc, char **argv);
/* uninstall: remove skills, remove editor MCP configs, remove binary. */
int cbm_cmd_uninstall(int argc, char **argv);
/* Hidden package-wrapper health probe. Verifies only immutable sidecars
* adjacent to the running executable; never consults fallbacks or mutates. */
int cbm_cmd_verify_runtime_assets(void);
bool cbm_cli_verify_runtime_assets_at(const char *binary_path, char *err, size_t err_sz);
/* update: check latest release, prompt for index deletion, prompt for ui/standard,
* download and replace binary. */
int cbm_cmd_update(int argc, char **argv);
+105 -79
View File
@@ -1,12 +1,10 @@
/*
* client_adapter.c — Generate client extension modules from the MCP tool registry.
*
* See client_adapter.h for how generation is split between the verified
* template (module scaffolding) and the live registry (tool list).
* See client_adapter.h for why these are generated rather than shipped.
*/
#include "cli/client_adapter.h"
#include "cli/integration_assets.h"
#include "foundation/constants.h"
#include "mcp/mcp.h"
@@ -24,10 +22,11 @@ typedef struct {
bool failed;
} adapter_sb_t;
static void sb_append_len(adapter_sb_t *sb, const char *s, size_t add) {
static void sb_append(adapter_sb_t *sb, const char *s) {
if (sb->failed || !s) {
return;
}
size_t add = strlen(s);
if (sb->len + add + 1 > sb->cap) {
size_t want = sb->cap ? sb->cap : CBM_SZ_2K;
while (want < sb->len + add + 1) {
@@ -50,47 +49,6 @@ static void sb_append_len(adapter_sb_t *sb, const char *s, size_t add) {
sb->buf[sb->len] = '\0';
}
static void sb_append(adapter_sb_t *sb, const char *s) {
if (!sb->failed && s) {
sb_append_len(sb, s, strlen(s));
}
}
/* Append template text, substituting each named placeholder with its value.
* An unresolved "{{" fails the build: a placeholder must never survive into
* a module a client will execute. */
static void sb_append_template(adapter_sb_t *sb, const char *text, const char *const names[],
const char *const values[], size_t pair_count) {
if (sb->failed || !text) {
sb->failed = true;
return;
}
const char *cursor = text;
while (!sb->failed && *cursor) {
const char *brace = strstr(cursor, "{{");
if (!brace) {
sb_append(sb, cursor);
return;
}
sb_append_len(sb, cursor, (size_t)(brace - cursor));
size_t matched = 0U;
for (size_t i = 0U; i < pair_count; i++) {
size_t name_length = strlen(names[i]);
if (strncmp(brace + PAIR_LEN, names[i], name_length) == 0 &&
strncmp(brace + PAIR_LEN + name_length, "}}", PAIR_LEN) == 0) {
sb_append(sb, values[i]);
matched = PAIR_LEN + name_length + PAIR_LEN;
break;
}
}
if (matched == 0U) {
sb->failed = true;
return;
}
cursor = brace + matched;
}
}
bool cbm_client_adapter_escape_js(const char *in, char *out, size_t out_sz) {
if (!in || !out || out_sz == 0) {
return false;
@@ -133,13 +91,20 @@ bool cbm_client_adapter_escape_js(const char *in, char *out, size_t out_sz) {
return true;
}
/* The module scaffolding (spawn bridge, header note, plugin surface) comes
* from the hash-verified cbm-integrations.json — those child-process-spawning
* bytes must not live inside the binary (AV surface) — while the tool list is
* still generated from the live registry, so the adapter cannot drift from
* the tools the server actually serves. No markers in the template: the
* managed-block writer adds them and REJECTS content that already carries
* them, so the body must stay marker-free. */
/* Shared preamble: the ownership markers and a note telling whoever opens the
* file that edits inside the block are overwritten. */
static void emit_header(adapter_sb_t *sb, const char *client) {
/* No markers here: cbm_text_upsert_managed_block adds them and REJECTS
* content that already carries them, so the body must stay marker-free. */
sb_append(sb, "// Generated by codebase-memory-mcp for ");
sb_append(sb, client);
sb_append(sb,
".\n"
"// Regenerated on install/update from the MCP tool registry, so it cannot\n"
"// drift from the tools the server actually serves. Edits inside this block\n"
"// are overwritten; edit outside it, or remove the markers to take ownership.\n");
}
char *cbm_client_adapter_pi(const char *binary_path) {
if (!binary_path || !binary_path[0]) {
return NULL;
@@ -152,34 +117,59 @@ char *cbm_client_adapter_pi(const char *binary_path) {
if (count <= 0) {
return NULL;
}
const cbm_integration_template_t *tpl = cbm_integration_template("adapter_pi");
if (!tpl || !tpl->tool_line || !tpl->current.bin_mode ||
strcmp(tpl->current.bin_mode, "js") != 0) {
return NULL;
}
/* Register every tool the registry advertises — never a hand-picked
* subset, which is the drift this generator exists to prevent. */
adapter_sb_t tools = {0};
adapter_sb_t sb = {0};
emit_header(&sb, "pi");
sb_append(&sb, "import { spawn } from 'node:child_process';\n\n");
sb_append(&sb, "const BIN = '");
sb_append(&sb, bin);
sb_append(&sb, "';\n\n");
/* One shared invoker: the binary prints JSON on stdout, so take the last
* line that parses rather than trusting the first — a stray log line on
* stdout must not break the bridge. */
sb_append(
&sb, "async function call(tool, args, signal) {\n"
" return new Promise((resolve) => {\n"
" const child = spawn(BIN, ['cli', tool, JSON.stringify(args ?? {})], {\n"
" stdio: ['ignore', 'pipe', 'pipe'],\n"
" env: { ...process.env, CBM_LOG_LEVEL: 'error' },\n"
" });\n"
" let out = '';\n"
" const onAbort = () => { if (!child.killed) child.kill(); };\n"
" signal?.addEventListener('abort', onAbort, { once: true });\n"
" child.stdout.on('data', (d) => (out += d.toString()));\n"
" child.on('error', (e) => {\n"
" signal?.removeEventListener('abort', onAbort);\n"
" resolve({ error: String(e && e.message ? e.message : e) });\n"
" });\n"
" child.on('close', () => {\n"
" signal?.removeEventListener('abort', onAbort);\n"
" const lines = out.split('\\n').map((l) => l.trim()).filter(Boolean);\n"
" for (let i = lines.length - 1; i >= 0; i--) {\n"
" try { return resolve(JSON.parse(lines[i])); } catch { /* keep scanning */ }\n"
" }\n"
" resolve({ error: 'no JSON response from codebase-memory-mcp' });\n"
" });\n"
" });\n"
"}\n\n");
/* Register every tool the registry advertises — never a hand-picked subset,
* which is the drift this generator exists to prevent. */
sb_append(&sb, "export function register(pi) {\n");
for (int i = 0; i < count; i++) {
const char *name = cbm_mcp_tool_name(i);
if (!name || !name[0]) {
continue;
}
const char *const tool_names[] = {"TOOL"};
const char *const tool_values[] = {name};
sb_append_template(&tools, tpl->tool_line, tool_names, tool_values, 1U);
}
if (tools.failed) {
free(tools.buf);
return NULL;
sb_append(&sb, " pi.registerTool({ name: '");
sb_append(&sb, name);
sb_append(&sb, "', run: (args, ctx) => call('");
sb_append(&sb, name);
sb_append(&sb, "', args, ctx?.signal) });\n");
}
sb_append(&sb, "}\n");
adapter_sb_t sb = {0};
const char *const names[] = {"BIN", "TOOLS"};
const char *const values[] = {bin, tools.buf ? tools.buf : ""};
sb_append_template(&sb, tpl->current.text, names, values, PAIR_LEN);
free(tools.buf);
if (sb.failed) {
free(sb.buf);
return NULL;
@@ -195,14 +185,50 @@ char *cbm_client_adapter_opencode(const char *binary_path) {
if (!cbm_client_adapter_escape_js(binary_path, bin, sizeof(bin))) {
return NULL;
}
const cbm_integration_template_t *tpl = cbm_integration_template("adapter_opencode");
if (!tpl || !tpl->current.bin_mode || strcmp(tpl->current.bin_mode, "js") != 0) {
return NULL;
}
adapter_sb_t sb = {0};
const char *const names[] = {"BIN"};
const char *const values[] = {bin};
sb_append_template(&sb, tpl->current.text, names, values, 1U);
emit_header(&sb, "OpenCode");
sb_append(&sb, "// OpenCode already reaches every tool over MCP; this adds only the\n"
"// automatic graph lookup before a grep/glob, which other clients get\n"
"// through their own hook configuration.\n");
sb_append(&sb, "import { spawn } from 'node:child_process';\n\n");
sb_append(&sb, "const BIN = '");
sb_append(&sb, bin);
sb_append(&sb, "';\n\n");
/* hook-augment requires hook_event_name and accepts Grep/Glob only under
* PreToolUse; omitting it makes the whole hook a silent no-op. */
sb_append(&sb, "function augment(tool, args) {\n"
" return new Promise((resolve) => {\n"
" const child = spawn(BIN, ['hook-augment'], {\n"
" stdio: ['pipe', 'pipe', 'ignore'],\n"
" env: { ...process.env, CBM_LOG_LEVEL: 'error' },\n"
" });\n"
" let out = '';\n"
" child.stdout.on('data', (d) => (out += d.toString()));\n"
" child.on('error', () => resolve(''));\n"
" child.on('close', () => resolve(out));\n"
" child.stdin.end(JSON.stringify({\n"
" hook_event_name: 'PreToolUse',\n"
" tool_name: tool,\n"
" tool_input: args ?? {},\n"
" }));\n"
" });\n"
"}\n\n");
sb_append(&sb,
"export const CodebaseMemory = async () => ({\n"
" 'tool.execute.after': async (input, output) => {\n"
" const tool = input?.tool === 'grep' ? 'Grep' : input?.tool === 'glob' ? 'Glob' "
": null;\n"
" if (!tool) return;\n"
" const extra = await augment(tool, output?.args);\n"
" if (extra && typeof output?.output === 'string') {\n"
" output.output += '\\n' + extra;\n"
" }\n"
" },\n"
"});\n");
if (sb.failed) {
free(sb.buf);
return NULL;
+6 -15
View File
@@ -3,22 +3,13 @@
*
* Some clients have no MCP support (pi), and some have no declarative hook
* configuration (OpenCode); their only extension point is a JavaScript/TypeScript
* module. Generation splits along what each source is authoritative for:
* module. We do not ship such a module as a repository asset — it would be a
* second, hand-maintained copy of the tool surface, and hand-maintained copies of
* the tool list have already produced defects here.
*
* - The module SCAFFOLDING (the node:child_process spawn bridge, header note,
* plugin surface) comes from the hash-verified cbm-integrations.json via
* integration_assets.h. It used to be compiled in as C string literals, but
* child-process-spawning script text inside a native binary is unnecessary
* mixed-content and attack surface with plausible static-classifier overlap.
* It therefore ships as independently inspectable data and is verified
* against the binary's embedded SHA-256 before use; this is not a claim of
* feature attribution for any vendor verdict.
*
* - The TOOL LIST is still generated at install time from the live registry
* (cbm_mcp_tool_count / cbm_mcp_tool_name), never hand-maintained in the
* asset file: hand-maintained copies of the tool surface have already
* produced defects here. Adding a tool to TOOLS[] adds it to every
* generated adapter with no second edit — that property is preserved.
* Instead the module is GENERATED at install time from the live registry
* (cbm_mcp_tool_count / cbm_mcp_tool_name), so it cannot drift: adding a tool to
* TOOLS[] adds it to every generated adapter with no second edit.
*
* Generated text is returned heap-allocated; the caller frees it. Nothing here
* touches the filesystem — writing is the caller's job, so dry-run and
-652
View File
@@ -1,652 +0,0 @@
/*
* integration_assets.c — Verified access to the shipped integration templates.
*
* See integration_assets.h for the architecture (why the bodies live in a
* data file, the integrity model, and the ownership-reference role of the
* content-addressed stored copy under <home>/.cbm/assets/<sha256>/).
*/
#include "cli/integration_assets.h"
#include "foundation/compat.h"
#include "foundation/compat_fs.h"
#include "foundation/constants.h"
#include "foundation/platform.h"
#include "foundation/sha256.h"
#ifdef _WIN32
#include "foundation/win_utf8.h"
#endif
#include "yyjson/yyjson.h"
#include "cbm_integrations_hash.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#ifndef _WIN32
#include <unistd.h>
#endif
#ifdef __APPLE__
#include <mach-o/dyld.h>
#endif
enum {
ASSETS_DIR_PERM = 0755,
ASSETS_FILE_PERM = 0644,
/* The shipped file is ~8 KB; the cap only bounds a corrupted candidate. */
ASSETS_MAX_BYTES = CBM_SZ_64K * CBM_SZ_16,
};
/* The one actionable failure message: every "cannot use the templates" path
* reports the same recovery, because the recovery is always the same. */
static const char assets_error_message[] =
"integration assets missing or modified - reinstall from the release archive "
"(expected " CBM_INTEGRATIONS_ASSET_NAME " next to the binary, in its package share "
"directory, in $CBM_ASSETS_DIR, or under ~/.cbm/assets/" CBM_INTEGRATIONS_SHA256 ")";
typedef struct {
const char *id;
cbm_integration_template_t tpl;
cbm_integration_body_t *released_storage;
} asset_entry_t;
/* Success-only cache: a failed resolve is re-attempted on every call so one
* bad candidate never poisons a later call that could succeed. */
static struct {
bool ok;
char *bytes;
size_t length;
yyjson_doc *doc;
asset_entry_t *entries;
size_t entry_count;
} g_assets;
static void assets_fill_error(char *err, size_t err_sz) {
if (err && err_sz > 0U) {
(void)snprintf(err, err_sz, "%s", assets_error_message);
}
}
static bool assets_exe_dir(char *out, size_t out_sz) {
if (out_sz < CBM_SZ_2) {
return false;
}
#ifdef _WIN32
char *module_path = cbm_module_path_utf8();
size_t length = module_path ? strlen(module_path) : 0U;
if (!module_path || length == 0U || length >= out_sz) {
free(module_path);
return false;
}
memcpy(out, module_path, length + 1U);
free(module_path);
cbm_normalize_path_sep(out);
#elif defined(__APPLE__)
uint32_t self_sz = (uint32_t)out_sz;
if (_NSGetExecutablePath(out, &self_sz) != 0) {
return false;
}
#else
ssize_t length = readlink("/proc/self/exe", out, out_sz - SKIP_ONE);
if (length <= 0 || (size_t)length >= out_sz - SKIP_ONE) {
return false;
}
out[length] = '\0';
#endif
char *slash = strrchr(out, '/');
if (!slash || slash == out) {
return false;
}
*slash = '\0';
return true;
}
bool cbm_integration_assets_ownership_path(const char *home, char *out, size_t out_sz) {
if (!home || !home[0]) {
return false;
}
int written = snprintf(out, out_sz, "%s/.cbm/assets/%s/%s", home, CBM_INTEGRATIONS_SHA256,
CBM_INTEGRATIONS_ASSET_NAME);
return written > 0 && (size_t)written < out_sz;
}
/* Read a candidate fully. Returns false for missing/unreadable/oversized. */
static bool assets_read_file(const char *path, char **out_bytes, size_t *out_length) {
*out_bytes = NULL;
*out_length = 0U;
FILE *file = cbm_fopen(path, "rb");
if (!file) {
return false;
}
if (fseek(file, 0L, SEEK_END) != 0) {
(void)fclose(file);
return false;
}
long size = ftell(file);
if (size <= 0 || size > (long)ASSETS_MAX_BYTES || fseek(file, 0L, SEEK_SET) != 0) {
(void)fclose(file);
return false;
}
char *bytes = malloc((size_t)size + SKIP_ONE);
if (!bytes) {
(void)fclose(file);
return false;
}
size_t got = fread(bytes, 1U, (size_t)size, file);
(void)fclose(file);
if (got != (size_t)size) {
free(bytes);
return false;
}
bytes[got] = '\0';
*out_bytes = bytes;
*out_length = got;
return true;
}
static bool assets_bytes_verified(const char *bytes, size_t length) {
char hex[CBM_SHA256_HEX_LEN + SKIP_ONE];
cbm_sha256_hex(bytes, length, hex);
return strcmp(hex, CBM_INTEGRATIONS_SHA256) == 0;
}
bool cbm_integration_assets_verify_file(const char *path) {
char *bytes = NULL;
size_t length = 0U;
bool verified =
path && assets_read_file(path, &bytes, &length) && assets_bytes_verified(bytes, length);
free(bytes);
return verified;
}
static bool assets_parse_body(yyjson_val *value, cbm_integration_body_t *body) {
yyjson_val *text = yyjson_is_obj(value) ? yyjson_obj_get(value, "text") : NULL;
yyjson_val *bin = yyjson_is_obj(value) ? yyjson_obj_get(value, "bin") : NULL;
if (!text || !yyjson_is_str(text) || (bin && !yyjson_is_str(bin))) {
return false;
}
body->text = yyjson_get_str(text);
body->bin_mode = bin ? yyjson_get_str(bin) : NULL;
return true;
}
/* Parse verified bytes into the template table. The variant picked here is
* the platform's ("windows" on _WIN32, else "posix", with "all" accepted for
* platform-independent templates such as the JS adapters). */
static bool assets_adopt_bytes(char *bytes, size_t length) {
yyjson_doc *doc = yyjson_read(bytes, length, 0);
yyjson_val *root = doc ? yyjson_doc_get_root(doc) : NULL;
yyjson_val *format = root && yyjson_is_obj(root) ? yyjson_obj_get(root, "format") : NULL;
yyjson_val *templates = root && yyjson_is_obj(root) ? yyjson_obj_get(root, "templates") : NULL;
if (!format || !yyjson_is_int(format) || yyjson_get_int(format) != 1 || !templates ||
!yyjson_is_obj(templates)) {
if (doc) {
yyjson_doc_free(doc);
}
return false;
}
#ifdef _WIN32
static const char platform_variant[] = "windows";
#else
static const char platform_variant[] = "posix";
#endif
size_t template_count = yyjson_obj_size(templates);
asset_entry_t *entries = calloc(template_count ? template_count : 1U, sizeof(*entries));
if (!entries) {
yyjson_doc_free(doc);
return false;
}
size_t entry_count = 0U;
size_t index;
size_t max;
yyjson_val *key;
yyjson_val *value;
bool parse_ok = true;
yyjson_obj_foreach(templates, index, max, key, value) {
if (!yyjson_is_str(key) || !yyjson_is_obj(value)) {
parse_ok = false;
break;
}
yyjson_val *variant = yyjson_obj_get(value, platform_variant);
if (!variant) {
variant = yyjson_obj_get(value, "all");
}
if (!variant) {
/* Not offered for this platform: skip rather than reject, so a
* future platform-scoped template does not brick the others. */
continue;
}
asset_entry_t *entry = &entries[entry_count];
entry->id = yyjson_get_str(key);
if (!assets_parse_body(variant, &entry->tpl.current)) {
parse_ok = false;
break;
}
yyjson_val *tool_line = yyjson_obj_get(value, "tool_line");
if (tool_line && !yyjson_is_str(tool_line)) {
parse_ok = false;
break;
}
entry->tpl.tool_line = tool_line ? yyjson_get_str(tool_line) : NULL;
yyjson_val *released = yyjson_obj_get(value, "released");
if (released) {
if (!yyjson_is_arr(released)) {
parse_ok = false;
break;
}
size_t released_count = yyjson_arr_size(released);
if (released_count > 0U) {
entry->released_storage = calloc(released_count, sizeof(*entry->released_storage));
if (!entry->released_storage) {
parse_ok = false;
break;
}
size_t released_index;
size_t released_max;
yyjson_val *released_value;
size_t stored = 0U;
yyjson_arr_foreach(released, released_index, released_max, released_value) {
if (!assets_parse_body(released_value, &entry->released_storage[stored])) {
parse_ok = false;
break;
}
stored++;
}
if (!parse_ok) {
entry_count++;
break;
}
entry->tpl.released = entry->released_storage;
entry->tpl.released_count = stored;
}
}
entry_count++;
}
if (!parse_ok) {
for (size_t i = 0U; i < entry_count; i++) {
free(entries[i].released_storage);
}
free(entries);
yyjson_doc_free(doc);
return false;
}
g_assets.bytes = bytes;
g_assets.length = length;
g_assets.doc = doc;
g_assets.entries = entries;
g_assets.entry_count = entry_count;
g_assets.ok = true;
return true;
}
/* Try one candidate path: read + hash-verify + parse. On success the bytes
* are adopted into the cache (ownership transferred). */
static bool assets_try_candidate(const char *path) {
char *bytes = NULL;
size_t length = 0U;
if (!assets_read_file(path, &bytes, &length)) {
return false;
}
if (!assets_bytes_verified(bytes, length) || !assets_adopt_bytes(bytes, length)) {
free(bytes);
return false;
}
return true;
}
static bool assets_resolve(const char *home) {
if (g_assets.ok) {
return true;
}
/* CBM_ASSETS_DIR set = authoritative: an explicit override that is
* missing or modified FAILS; silently falling back would hide exactly
* the tampering the hash check exists to surface. */
static const char env_missing[] = "\x1f"
"CBM_ASSETS_DIR_MISSING"
"\x1f";
char env_buf[CBM_SZ_4K];
char candidate[CBM_SZ_4K];
const char *assets_dir =
cbm_safe_getenv("CBM_ASSETS_DIR", env_buf, sizeof(env_buf), env_missing);
if (!assets_dir) {
return false; /* present but unrepresentable: fail, do not fall back */
}
if (strcmp(assets_dir, env_missing) != 0 && assets_dir[0]) {
int written = snprintf(candidate, sizeof(candidate), "%s/%s", assets_dir,
CBM_INTEGRATIONS_ASSET_NAME);
return written > 0 && (size_t)written < sizeof(candidate) &&
assets_try_candidate(candidate);
}
char exe_dir[CBM_SZ_4K];
if (assets_exe_dir(exe_dir, sizeof(exe_dir))) {
int written =
snprintf(candidate, sizeof(candidate), "%s/%s", exe_dir, CBM_INTEGRATIONS_ASSET_NAME);
if (written > 0 && (size_t)written < sizeof(candidate) && assets_try_candidate(candidate)) {
return true;
}
/* System package layout: <prefix>/bin/<binary> with the immutable
* runtime asset under <prefix>/share/codebase-memory-mcp/. */
written = snprintf(candidate, sizeof(candidate), "%s/../share/codebase-memory-mcp/%s",
exe_dir, CBM_INTEGRATIONS_ASSET_NAME);
if (written > 0 && (size_t)written < sizeof(candidate) && assets_try_candidate(candidate)) {
return true;
}
/* Source-tree layout: build/c/<binary> with assets/ at the root. */
written = snprintf(candidate, sizeof(candidate), "%s/../../assets/%s", exe_dir,
CBM_INTEGRATIONS_ASSET_NAME);
if (written > 0 && (size_t)written < sizeof(candidate) && assets_try_candidate(candidate)) {
return true;
}
}
return cbm_integration_assets_ownership_path(home, candidate, sizeof(candidate)) &&
assets_try_candidate(candidate);
}
bool cbm_integration_assets_require(const char *home, char *err, size_t err_sz) {
if (assets_resolve(home)) {
return true;
}
assets_fill_error(err, err_sz);
return false;
}
static bool assets_transaction_close(cbm_activation_transaction_t **transaction_io) {
return !transaction_io || !*transaction_io ||
cbm_activation_transaction_close(transaction_io) == CBM_ACTIVATION_TRANSACTION_OK;
}
static bool assets_stage_target(const char *target, cbm_activation_transaction_t **transaction_out,
char *err, size_t err_sz) {
cbm_activation_transaction_status_t status = cbm_activation_transaction_stage_bytes(
target, g_assets.bytes, g_assets.length, transaction_out);
if (status == CBM_ACTIVATION_TRANSACTION_OK && *transaction_out) {
return true;
}
if (err && err_sz > 0U) {
(void)snprintf(err, err_sz, "cannot stage the verified %s copy for %s: %s",
CBM_INTEGRATIONS_ASSET_NAME, target,
cbm_activation_transaction_status_message(status));
}
(void)assets_transaction_close(transaction_out);
return false;
}
static bool assets_install_validator(const char *target_path, void *context) {
(void)context;
return cbm_integration_assets_verify_file(target_path);
}
cbm_activation_transaction_status_t cbm_integration_assets_commit_install(
cbm_activation_transaction_t *transaction) {
if (!transaction) {
return CBM_ACTIVATION_TRANSACTION_INVALID_ARGUMENT;
}
cbm_activation_transaction_status_t status =
cbm_activation_transaction_commit(transaction, assets_install_validator, NULL);
#ifndef _WIN32
if (status == CBM_ACTIVATION_TRANSACTION_OK) {
const char *target = cbm_activation_transaction_target_path(transaction);
if (!target || chmod(target, ASSETS_FILE_PERM) != 0) {
cbm_activation_transaction_status_t rollback =
cbm_activation_transaction_rollback(transaction);
return rollback == CBM_ACTIVATION_TRANSACTION_OK
? CBM_ACTIVATION_TRANSACTION_IO
: CBM_ACTIVATION_TRANSACTION_ROLLBACK_FAILED;
}
}
#endif
return status;
}
cbm_activation_transaction_status_t cbm_integration_assets_commit_removal(
cbm_activation_transaction_t *transaction) {
if (!transaction) {
return CBM_ACTIVATION_TRANSACTION_INVALID_ARGUMENT;
}
cbm_activation_transaction_status_t status =
cbm_activation_transaction_commit(transaction, NULL, NULL);
const char *retained = cbm_activation_transaction_backup_path(transaction);
if (status != CBM_ACTIVATION_TRANSACTION_OK) {
return status;
}
if (retained && cbm_integration_assets_verify_file(retained)) {
return CBM_ACTIVATION_TRANSACTION_OK;
}
cbm_activation_transaction_status_t rollback = cbm_activation_transaction_rollback(transaction);
return rollback == CBM_ACTIVATION_TRANSACTION_OK ? CBM_ACTIVATION_TRANSACTION_VALIDATION_FAILED
: CBM_ACTIVATION_TRANSACTION_ROLLBACK_FAILED;
}
bool cbm_integration_assets_stage_install(const char *home, const char *install_dir,
cbm_activation_transaction_t **adjacent_transaction_out,
cbm_activation_transaction_t **ownership_transaction_out,
char *err, size_t err_sz) {
if (adjacent_transaction_out) {
*adjacent_transaction_out = NULL;
}
if (ownership_transaction_out) {
*ownership_transaction_out = NULL;
}
if (!home || !home[0] || !install_dir || !install_dir[0] || !adjacent_transaction_out ||
!ownership_transaction_out || !cbm_integration_assets_require(home, err, err_sz)) {
if ((!home || !home[0] || !install_dir || !install_dir[0]) && err && err_sz > 0U) {
(void)snprintf(err, err_sz, "integration asset install paths are unavailable");
}
return false;
}
char adjacent[CBM_SZ_4K];
char ownership[CBM_SZ_4K];
char ownership_parent[CBM_SZ_4K];
int adjacent_length =
snprintf(adjacent, sizeof(adjacent), "%s/%s", install_dir, CBM_INTEGRATIONS_ASSET_NAME);
if (adjacent_length <= 0 || (size_t)adjacent_length >= sizeof(adjacent) ||
!cbm_integration_assets_ownership_path(home, ownership, sizeof(ownership))) {
assets_fill_error(err, err_sz);
return false;
}
(void)snprintf(ownership_parent, sizeof(ownership_parent), "%s", ownership);
char *slash = strrchr(ownership_parent, '/');
if (!slash) {
assets_fill_error(err, err_sz);
return false;
}
*slash = '\0';
if (!cbm_mkdir_p(install_dir, ASSETS_DIR_PERM) ||
!cbm_mkdir_p(ownership_parent, ASSETS_DIR_PERM)) {
if (err && err_sz > 0U) {
(void)snprintf(err, err_sz, "cannot create directories for the verified %s copies",
CBM_INTEGRATIONS_ASSET_NAME);
}
return false;
}
if (!assets_stage_target(adjacent, adjacent_transaction_out, err, err_sz) ||
!assets_stage_target(ownership, ownership_transaction_out, err, err_sz)) {
bool owner_closed = assets_transaction_close(ownership_transaction_out);
bool adjacent_closed = assets_transaction_close(adjacent_transaction_out);
if ((!owner_closed || !adjacent_closed) && err && err_sz > 0U) {
(void)snprintf(err, err_sz,
"cannot clean up a partial integration asset install transaction");
}
return false;
}
return true;
}
static bool assets_stage_owned_removal(const char *path,
cbm_activation_transaction_t **transaction_out,
bool *foreign_preserved_out, char *err, size_t err_sz) {
cbm_path_info_t info;
if (cbm_path_info_utf8(path, &info) != 0) {
return true;
}
if (!info.is_regular || info.is_symlink || !cbm_integration_assets_verify_file(path)) {
if (foreign_preserved_out) {
*foreign_preserved_out = true;
}
return true;
}
cbm_activation_transaction_status_t status =
cbm_activation_transaction_stage_removal(path, transaction_out);
if (status == CBM_ACTIVATION_TRANSACTION_OK && *transaction_out) {
/* stage_removal snapshots the exact file identity; commit revalidates
* it, binding this digest check to the file that will be retained. */
if (cbm_integration_assets_verify_file(path)) {
return true;
}
if (!assets_transaction_close(transaction_out)) {
if (err && err_sz > 0U) {
(void)snprintf(err, err_sz,
"cannot clean up a changed integration asset removal transaction");
}
return false;
}
if (foreign_preserved_out) {
*foreign_preserved_out = true;
}
return true;
}
if (err && err_sz > 0U) {
(void)snprintf(err, err_sz, "cannot stage owned integration asset removal for %s: %s", path,
cbm_activation_transaction_status_message(status));
}
(void)assets_transaction_close(transaction_out);
return false;
}
bool cbm_integration_assets_stage_remove(const char *home, const char *install_dir,
cbm_activation_transaction_t **adjacent_transaction_out,
cbm_activation_transaction_t **ownership_transaction_out,
bool *foreign_preserved_out, char *err, size_t err_sz) {
if (adjacent_transaction_out) {
*adjacent_transaction_out = NULL;
}
if (ownership_transaction_out) {
*ownership_transaction_out = NULL;
}
if (foreign_preserved_out) {
*foreign_preserved_out = false;
}
if (!home || !home[0] || !install_dir || !install_dir[0] || !adjacent_transaction_out ||
!ownership_transaction_out) {
if (err && err_sz > 0U) {
(void)snprintf(err, err_sz, "integration asset uninstall paths are unavailable");
}
return false;
}
char adjacent[CBM_SZ_4K];
char ownership[CBM_SZ_4K];
int adjacent_length =
snprintf(adjacent, sizeof(adjacent), "%s/%s", install_dir, CBM_INTEGRATIONS_ASSET_NAME);
if (adjacent_length <= 0 || (size_t)adjacent_length >= sizeof(adjacent) ||
!cbm_integration_assets_ownership_path(home, ownership, sizeof(ownership))) {
if (err && err_sz > 0U) {
(void)snprintf(err, err_sz, "integration asset uninstall paths are too long");
}
return false;
}
if (!assets_stage_owned_removal(adjacent, adjacent_transaction_out, foreign_preserved_out, err,
err_sz) ||
!assets_stage_owned_removal(ownership, ownership_transaction_out, foreign_preserved_out,
err, err_sz)) {
bool owner_closed = assets_transaction_close(ownership_transaction_out);
bool adjacent_closed = assets_transaction_close(adjacent_transaction_out);
if ((!owner_closed || !adjacent_closed) && err && err_sz > 0U) {
(void)snprintf(err, err_sz,
"cannot clean up a partial integration asset removal transaction");
}
return false;
}
return true;
}
#ifdef CBM_CLI_ENABLE_TEST_API
bool cbm_integration_assets_install(const char *home, bool dry_run, char *err, size_t err_sz) {
if (!home || !home[0] || !cbm_integration_assets_require(home, err, err_sz)) {
if (!home || !home[0]) {
assets_fill_error(err, err_sz);
}
return false;
}
if (dry_run) {
return true;
}
char stored[CBM_SZ_4K];
char parent[CBM_SZ_4K];
if (!cbm_integration_assets_ownership_path(home, stored, sizeof(stored))) {
assets_fill_error(err, err_sz);
return false;
}
(void)snprintf(parent, sizeof(parent), "%s", stored);
char *slash = strrchr(parent, '/');
if (!slash) {
assets_fill_error(err, err_sz);
return false;
}
*slash = '\0';
if (!cbm_mkdir_p(parent, ASSETS_DIR_PERM)) {
if (err && err_sz > 0U) {
(void)snprintf(err, err_sz, "cannot create %s to store the verified %s copy", parent,
CBM_INTEGRATIONS_ASSET_NAME);
}
return false;
}
cbm_activation_transaction_t *transaction = NULL;
if (!assets_stage_target(stored, &transaction, err, err_sz)) {
return false;
}
cbm_activation_transaction_status_t status = cbm_integration_assets_commit_install(transaction);
if (status != CBM_ACTIVATION_TRANSACTION_OK) {
(void)assets_transaction_close(&transaction);
if (err && err_sz > 0U) {
(void)snprintf(err, err_sz, "cannot publish the verified %s copy to %s",
CBM_INTEGRATIONS_ASSET_NAME, stored);
}
return false;
}
status = cbm_activation_transaction_finalize(transaction);
bool finalized =
status == CBM_ACTIVATION_TRANSACTION_OK || status == CBM_ACTIVATION_TRANSACTION_DEFERRED;
bool closed = cbm_activation_transaction_close(&transaction) == CBM_ACTIVATION_TRANSACTION_OK;
if (!finalized || !closed) {
if (err && err_sz > 0U) {
(void)snprintf(err, err_sz, "cannot finalize the verified %s copy at %s",
CBM_INTEGRATIONS_ASSET_NAME, stored);
}
return false;
}
return true;
}
#endif
const cbm_integration_template_t *cbm_integration_template(const char *id) {
if (!id || !assets_resolve(NULL)) {
return NULL;
}
for (size_t i = 0U; i < g_assets.entry_count; i++) {
if (strcmp(g_assets.entries[i].id, id) == 0) {
return &g_assets.entries[i].tpl;
}
}
return NULL;
}
#ifdef CBM_CLI_ENABLE_TEST_API
void cbm_integration_assets_reset_for_testing(void) {
for (size_t i = 0U; i < g_assets.entry_count; i++) {
free(g_assets.entries[i].released_storage);
}
free(g_assets.entries);
g_assets.entries = NULL;
g_assets.entry_count = 0U;
if (g_assets.doc) {
yyjson_doc_free(g_assets.doc);
g_assets.doc = NULL;
}
free(g_assets.bytes);
g_assets.bytes = NULL;
g_assets.length = 0U;
g_assets.ok = false;
}
#endif
-134
View File
@@ -1,134 +0,0 @@
/*
* integration_assets.h — Verified access to the shipped integration templates.
*
* The binary used to embed every integration artifact it installs — nine
* complete shebang'd shell scripts, their PowerShell/.cmd twins, and the
* generated JS/TS client modules — as C string literals, written to client
* config dirs at 0755. That block of script-shaped bytes inside an unsigned
* executable overlaps script-bearing installer/launcher feature families used
* by static malware classifiers. That does not identify the cause of any
* opaque vendor verdict. The unnecessary bodies now live in ONE independently
* inspectable data file, assets/cbm-integrations.json,
* and the binary embeds exactly ONE constant about them: the file's SHA-256
* (generated at build time into cbm_integrations_hash.h).
*
* Integrity model: every use verifies the file against that constant first.
* This preserves the tamper-evidence the templates had while compiled in;
* a verified copy is bit-identical to the shipped one by construction, so
* WHICH copy satisfied the check never matters — only availability does.
*
* Resolution order: $CBM_ASSETS_DIR (authoritative when set — a bad explicit
* override fails, it never falls back), else next to the running binary (the
* flat release-archive layout install.sh/install.ps1 execute from), else the
* source-tree layout <bindir>/../../assets (build/c/ builds and test-runner),
* else the stored copy under <home>/.cbm/assets/<sha256>/.
*
* The stored copy is the OWNERSHIP REFERENCE: `install` persists the verified
* bytes to <home>/.cbm/assets/<sha256>/ — a SIBLING of the disposable cache
* (~/.cache/codebase-memory-mcp), never inside it — so uninstall can decide
* "is this deployed file ours?" by materializing templates from it long after
* the release archive is gone. Deciding ownership by materialize-and-compare
* against these templates REPLACES the old exact-match against embedded
* literals; released[] carries the historical bodies older versions wrote, so
* upgrades and uninstalls keep recognising them. Content addressing prevents
* same-version rebuilds from overwriting another build's ownership reference.
* Foreign or user-modified
* files are preserved exactly as before.
*
* Single-threaded by design: only the CLI install/uninstall paths touch this,
* never the MCP server or hook-augment hot paths.
*/
#ifndef CBM_CLI_INTEGRATION_ASSETS_H
#define CBM_CLI_INTEGRATION_ASSETS_H
#include <stdbool.h>
#include <stddef.h>
#include "cli/activation_transaction.h"
#define CBM_INTEGRATIONS_ASSET_NAME "cbm-integrations.json"
/* One template body. `text` may contain {{BIN}} / {{EVENT}} / {{TOOLS}} /
* {{TOOL}} placeholders; `bin_mode` names how {{BIN}} must be encoded when
* substituted ("sh", "ps", "cmd", "raw", "js"), or NULL for a body that
* takes no binary path at all. */
typedef struct {
const char *text;
const char *bin_mode;
} cbm_integration_body_t;
/* A template resolved for THIS platform (posix/windows/all variant). The
* released list is the exact set of historical bodies previous versions
* materialized; it exists so ownership checks still recognise files written
* before this version — the ONE place old bodies may live. */
typedef struct {
cbm_integration_body_t current;
const cbm_integration_body_t *released;
size_t released_count;
const char *tool_line; /* adapters only: per-tool registration line */
} cbm_integration_template_t;
/* Locate, read and hash-verify the asset file (cached after first success;
* failures are re-resolved on every call so a transient state never sticks).
* `home` may be NULL when no stored-copy candidate should be considered.
* Returns true on success; on failure fills `err` (when given) with the
* actionable message. */
bool cbm_integration_assets_require(const char *home, char *err, size_t err_sz);
/* Verify one explicit file against this binary's compiled digest. This does
* not consult fallbacks or populate the template cache. */
bool cbm_integration_assets_verify_file(const char *path);
/* Content-addressed ownership reference used by install/uninstall. */
bool cbm_integration_assets_ownership_path(const char *home, char *out, size_t out_sz);
/* Stage both runtime copies from the verified immutable snapshot: the fixed
* sidecar beside the target executable and the content-addressed ownership reference.
* No target is changed until the caller commits both transactions. The caller
* must hold the native activation guard from this call through rollback or
* finalize. A failure closes every partial stage and leaves both outputs NULL. */
bool cbm_integration_assets_stage_install(const char *home, const char *install_dir,
cbm_activation_transaction_t **adjacent_transaction_out,
cbm_activation_transaction_t **ownership_transaction_out,
char *err, size_t err_sz);
/* Publish one staged integration copy and validate the exact post-rename
* target against this binary's compiled digest before it can be finalized.
* If the staged inode was rewritten after staging, commit rolls the previous
* target back and returns VALIDATION_FAILED. */
cbm_activation_transaction_status_t cbm_integration_assets_commit_install(
cbm_activation_transaction_t *transaction);
/* Stage removal of exact, hash-owned adjacent and ownership copies. Missing
* files are successful no-ops. Foreign or modified files are preserved and
* reported through foreign_preserved_out. The caller owns any returned
* transactions and must commit/rollback/finalize them under the activation
* guard. */
bool cbm_integration_assets_stage_remove(const char *home, const char *install_dir,
cbm_activation_transaction_t **adjacent_transaction_out,
cbm_activation_transaction_t **ownership_transaction_out,
bool *foreign_preserved_out, char *err, size_t err_sz);
/* Move the snapshotted target to the transaction's private retained path,
* then hash that exact retained object before permitting final deletion. An
* in-place rewrite after stage_remove is restored and reported as
* VALIDATION_FAILED instead of being deleted. */
cbm_activation_transaction_status_t cbm_integration_assets_commit_removal(
cbm_activation_transaction_t *transaction);
/* Template lookup for this platform. Returns NULL when the assets are
* unavailable or the id/variant is unknown. The returned pointers stay valid
* for the process lifetime (or until the test-only reset). */
const cbm_integration_template_t *cbm_integration_template(const char *id);
#ifdef CBM_CLI_ENABLE_TEST_API
/* Focused lifecycle probe for the ownership-copy publisher. Production uses
* cbm_integration_assets_stage_install as part of the guarded runtime set. */
bool cbm_integration_assets_install(const char *home, bool dry_run, char *err, size_t err_sz);
/* Drop the cached document so a test can steer resolution via CBM_ASSETS_DIR
* (set = authoritative) and observe verification failures deterministically. */
void cbm_integration_assets_reset_for_testing(void);
#endif
#endif /* CBM_CLI_INTEGRATION_ASSETS_H */
-1
View File
@@ -159,7 +159,6 @@ cbm_daemon_process_role_t cbm_daemon_process_role(int argc, char *const argv[])
}
static const char *const stateless_commands[] = {
"--verify-runtime-assets",
"install",
"uninstall",
"update",
+13 -73
View File
@@ -21,7 +21,7 @@
#include "mcp/index_supervisor.h"
#include "store/store.h"
#include "ui/config.h"
#include "ui/asset_pack.h"
#include "ui/embedded_assets.h"
#include "ui/http_server.h"
#include "watcher/watcher.h"
@@ -50,7 +50,6 @@ enum {
HOST_HTTP_CONFIG_POLL_MS = 1000,
HOST_HTTP_RETRY_INITIAL_MS = 1000,
HOST_HTTP_RETRY_MAX_MS = 30000,
HOST_UI_ASSET_SHUTDOWN_MS = 10000,
HOST_WATCH_INTERVAL_MS = 5000,
HOST_CONFLICT_LOG_CAP = 1024 * 1024,
HOST_OPERATION_LOG_CAP = 5 * 1024 * 1024,
@@ -89,13 +88,8 @@ struct host_state {
cbm_http_server_t *http;
cbm_thread_t watcher_thread;
cbm_thread_t http_thread;
cbm_thread_t ui_assets_thread;
bool watcher_started;
bool http_started;
bool ui_assets_started;
bool ui_assets_start_failed;
bool ui_assets_failure_logged;
atomic_bool ui_assets_completed;
bool http_retiring;
bool http_config_loaded;
bool http_config_enabled;
@@ -298,18 +292,6 @@ static void *host_http_thread(void *opaque) {
return NULL;
}
static void *host_ui_assets_thread(void *opaque) {
host_state_t *host = opaque;
char error[HOST_PATH_CAP];
if (cbm_ui_assets_warm(cbm_get_home_dir(), error, sizeof(error))) {
cbm_log_info("ui.assets_ready", "pack", CBM_UI_ASSET_PACK_NAME);
} else if (cbm_ui_assets_state() != CBM_UI_ASSETS_CANCELLED) {
cbm_log_warn("ui.assets_unavailable", "reason", error);
}
atomic_store_explicit(&host->ui_assets_completed, true, memory_order_release);
return NULL;
}
static int host_watcher_index(const char *project_name, const char *root_path, void *opaque) {
host_state_t *host = opaque;
return host && host->application
@@ -452,31 +434,16 @@ static void host_http_reconcile_at(host_state_t *host, uint64_t now_ms, bool for
if (!host || !host->http_ops) {
return;
}
if (host->http_ops == &g_host_http_default_ops) {
cbm_ui_assets_state_t state = cbm_ui_assets_state();
host->http_assets_available = state == CBM_UI_ASSETS_READY;
if ((state == CBM_UI_ASSETS_FAILED || state == CBM_UI_ASSETS_CANCELLED ||
host->ui_assets_start_failed) &&
!host->ui_assets_failure_logged) {
cbm_log_warn("ui.assets_unavailable", "pack", CBM_UI_ASSET_PACK_NAME, "hint",
"reinstall the matching UI release");
host->ui_assets_failure_logged = true;
}
}
bool load_config =
force_config_load || !host->http_config_loaded || now_ms >= host->http_next_config_load_ms;
cbm_ui_config_t desired = {
.ui_enabled = host->http_config_enabled,
.ui_port = host->http_config_port,
};
bool config_changed = false;
if (load_config) {
host->http_next_config_load_ms = host_deadline_from(now_ms, HOST_HTTP_CONFIG_POLL_MS);
host->http_ops->config_load(host->http_ops->context, &desired);
config_changed = !host->http_config_loaded ||
desired.ui_enabled != host->http_config_enabled ||
desired.ui_port != host->http_config_port;
if (!force_config_load && host->http_config_loaded && now_ms < host->http_next_config_load_ms) {
return;
}
host->http_next_config_load_ms = host_deadline_from(now_ms, HOST_HTTP_CONFIG_POLL_MS);
cbm_ui_config_t desired;
host->http_ops->config_load(host->http_ops->context, &desired);
bool config_changed = !host->http_config_loaded ||
desired.ui_enabled != host->http_config_enabled ||
desired.ui_port != host->http_config_port;
if (config_changed && !host->http_retiring) {
host->http_retiring = true;
host->http_retry_at_ms = now_ms;
@@ -506,9 +473,10 @@ static void host_http_reconcile_at(host_state_t *host, uint64_t now_ms, bool for
return;
}
if (!host->http_assets_available) {
if (config_changed && cbm_ui_assets_state() == CBM_UI_ASSETS_UNAVAILABLE) {
if (config_changed) {
cbm_log_warn("ui.no_assets", "hint", "rebuild with: make -f Makefile.cbm cbm-with-ui");
}
host->http_retry_at_ms = UINT64_MAX;
return;
}
if (now_ms < host->http_retry_at_ms) {
@@ -542,9 +510,6 @@ static void host_background_stop(host_state_t *host) {
if (host->watcher) {
cbm_watcher_stop(host->watcher);
}
if (host->ui_assets_started) {
cbm_ui_assets_request_cancel();
}
}
static bool host_background_join(host_state_t *host) {
@@ -560,18 +525,6 @@ static bool host_background_join(host_state_t *host) {
}
host->watcher_started = false;
}
if (host->ui_assets_started) {
uint64_t deadline = host_deadline_from(cbm_now_ms(), HOST_UI_ASSET_SHUTDOWN_MS);
while (!atomic_load_explicit(&host->ui_assets_completed, memory_order_acquire) &&
cbm_now_ms() < deadline) {
cbm_usleep(1000);
}
if (!atomic_load_explicit(&host->ui_assets_completed, memory_order_acquire) ||
cbm_thread_join(&host->ui_assets_thread) != 0) {
return false;
}
host->ui_assets_started = false;
}
return true;
}
@@ -614,8 +567,7 @@ static void host_state_free(host_state_t *host) {
static bool host_state_prepare(host_state_t *host, const cbm_daemon_ipc_endpoint_t *endpoint) {
host->http_ops = &g_host_http_default_ops;
host->http_assets_available = false;
atomic_init(&host->ui_assets_completed, false);
host->http_assets_available = CBM_EMBEDDED_FILE_COUNT > 0;
if (!cbm_secure_random(host->ui_readiness_secret, sizeof(host->ui_readiness_secret))) {
cbm_log_error("daemon.readiness_secret_failed", "reason", "system_rng_unavailable");
return false;
@@ -877,18 +829,6 @@ static bool host_background_start(host_state_t *host) {
}
host->watcher_started = true;
/* Runtime publication already succeeded before this function is called.
* Pack I/O and hashing therefore run concurrently with normal daemon
* service rather than extending its startup critical path. */
if (cbm_ui_assets_supported()) {
if (cbm_thread_create(&host->ui_assets_thread, 0, host_ui_assets_thread, host) == 0) {
host->ui_assets_started = true;
} else {
host->ui_assets_start_failed = true;
cbm_log_warn("ui.assets_unavailable", "reason", "warmup_thread_create_failed");
}
}
host_http_reconcile_at(host, cbm_now_ms(), true);
return true;
}
+7 -16
View File
@@ -72,7 +72,7 @@ enum {
#include <io.h>
#endif
#include "ui/http_server.h"
#include "ui/asset_pack.h"
#include "ui/embedded_assets.h"
#include "ui/config.h"
#include <yyjson/yyjson.h>
@@ -1040,14 +1040,6 @@ static int handle_subcommand(int argc, char **argv, cbm_project_lock_manager_t *
}
}
for (int i = SKIP_ONE; i < argc; i++) {
if (strcmp(argv[i], "--verify-runtime-assets") == 0) {
if (i != SKIP_ONE || argc != MAIN_CLI_ARGC) {
(void)fprintf(
stderr, "codebase-memory-mcp: --verify-runtime-assets accepts no arguments\n");
return 2;
}
return cbm_cmd_verify_runtime_assets();
}
if (strcmp(argv[i], "--version") == 0) {
printf("codebase-memory-mcp %s\n", CBM_VERSION);
return 0;
@@ -2038,7 +2030,7 @@ static int main_daemon_ctl_finish_ui_open(cbm_daemon_runtime_client_t **client_i
bool open_browser);
static void main_daemon_ctl_print_ui_configuration(void) {
if (!cbm_ui_assets_supported()) {
if (!(CBM_EMBEDDED_FILE_COUNT > 0)) {
return;
}
cbm_ui_config_t ui_config;
@@ -2121,8 +2113,7 @@ static int main_daemon_ctl_finish_ui_open(cbm_daemon_runtime_client_t **client_i
"opened\n",
timeout_ms);
(void)fprintf(stderr,
"hint: verify matching runtime assets with `codebase-memory-mcp "
"--verify-runtime-assets`, check the daemon log, and if port %d is in use "
"hint: check the daemon log, and if port %d is in use "
"retry with --port=N\n",
port);
return EXIT_FAILURE;
@@ -2229,7 +2220,7 @@ static int main_run_daemon_ctl(int argc, char **argv, const cbm_daemon_ipc_endpo
"last session; run `daemon stop` first if you want a permanent one\n",
(unsigned long)status.daemon_pid);
}
if (!cbm_ui_assets_supported()) {
if (!(CBM_EMBEDDED_FILE_COUNT > 0)) {
if (requested_port > 0 || open_browser) {
(void)fprintf(stderr, "warning: this binary was built without UI support; "
"--port/--open have no effect\n");
@@ -2294,7 +2285,7 @@ static int main_run_daemon_ctl(int argc, char **argv, const cbm_daemon_ipc_endpo
/* The committed control connection satisfied the daemon's no-client
* startup window; configure the UI before departing. */
int ui_port = 0;
if (cbm_ui_assets_supported()) {
if ((CBM_EMBEDDED_FILE_COUNT > 0)) {
cbm_ui_config_t ui_config;
cbm_ui_config_load(&ui_config);
ui_port = requested_port > 0 ? requested_port : ui_config.ui_port;
@@ -2327,7 +2318,7 @@ static int main_run_daemon_ctl(int argc, char **argv, const cbm_daemon_ipc_endpo
printf("It survives idle periods and session ends; `codebase-memory-mcp daemon stop` "
"retires it.\n");
int ui_result =
cbm_ui_assets_supported()
(CBM_EMBEDDED_FILE_COUNT > 0)
? main_daemon_ctl_finish_ui_open(&start_result.client, ui_port, open_browser)
: EXIT_SUCCESS;
if (start_result.client) {
@@ -2842,7 +2833,7 @@ int main(int argc, char **argv) {
(void)main_version_cohort_close(&client_cohort_lease, &client_cohort_manager);
return EXIT_FAILURE;
}
if (explicitly_enabled && !cbm_ui_assets_supported()) {
if (explicitly_enabled && !(CBM_EMBEDDED_FILE_COUNT > 0)) {
(void)fprintf(stderr, "codebase-memory-mcp: --ui requested, but this binary was built "
"without UI support; rebuild with `make -f Makefile.cbm "
"cbm-with-ui`.\n");
-6
View File
@@ -1,6 +0,0 @@
/* Test runners exercise the real loader with no compiled-in pack by default. */
#include <stdint.h>
const char CBM_UI_ASSET_PACK_NAME[] = "";
const char CBM_UI_ASSET_SHA256[] = "";
const uint64_t CBM_UI_ASSET_SIZE = UINT64_C(0);
-892
View File
@@ -1,892 +0,0 @@
/* asset_pack.c — Hash-bound loader for the external UI frontend pack. */
#include "ui/asset_pack.h"
#include "foundation/compat.h"
#include "foundation/compat_fs.h"
#include "foundation/constants.h"
#include "foundation/platform.h"
#include "foundation/sha256.h"
#ifdef _WIN32
#include "foundation/win_utf8.h"
#endif
#include <errno.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#endif
enum {
UI_PACK_HEADER_BYTES = 80,
UI_PACK_ENTRY_BYTES = 24,
UI_PACK_VERSION = 1,
UI_PACK_MAX_FILES = 1024,
UI_PACK_MAX_PATH_BYTES = 255,
UI_ASSETS_DIR_PERM = 0755,
UI_ASSET_FILE_PERM = 0644,
UI_PATH_CAP = 4096,
};
#define UI_PACK_MAX_BYTES ((size_t)CBM_SZ_64K * CBM_SZ_1K)
typedef struct {
unsigned char *bytes;
size_t length;
cbm_ui_asset_t *assets;
size_t asset_count;
char *paths;
} ui_asset_snapshot_t;
static ui_asset_snapshot_t g_snapshot;
static atomic_int g_state = CBM_UI_ASSETS_COLD;
static atomic_bool g_cancel_requested = false;
static char g_binary_path[UI_PATH_CAP];
#ifdef CBM_CLI_ENABLE_TEST_API
static bool g_test_manifest_set;
static char g_test_pack_name[UI_PATH_CAP];
static char g_test_sha256[CBM_SHA256_HEX_LEN + 1U];
static uint64_t g_test_size;
#endif
static const char *ui_manifest_name(void) {
#ifdef CBM_CLI_ENABLE_TEST_API
if (g_test_manifest_set) {
return g_test_pack_name;
}
#endif
return CBM_UI_ASSET_PACK_NAME;
}
static const char *ui_manifest_sha256(void) {
#ifdef CBM_CLI_ENABLE_TEST_API
if (g_test_manifest_set) {
return g_test_sha256;
}
#endif
return CBM_UI_ASSET_SHA256;
}
static uint64_t ui_manifest_size(void) {
#ifdef CBM_CLI_ENABLE_TEST_API
if (g_test_manifest_set) {
return g_test_size;
}
#endif
return CBM_UI_ASSET_SIZE;
}
static bool ui_hex_sha256(const char *value) {
if (!value || strlen(value) != CBM_SHA256_HEX_LEN) {
return false;
}
for (size_t index = 0; index < CBM_SHA256_HEX_LEN; index++) {
char byte = value[index];
if (!((byte >= '0' && byte <= '9') || (byte >= 'a' && byte <= 'f'))) {
return false;
}
}
return true;
}
static bool ui_content_addressed_name(const char *name, const char *sha256) {
static const char prefix[] = "cbm-ui-";
static const char suffix[] = ".pack";
if (!name || !sha256 || strncmp(name, prefix, sizeof(prefix) - 1U) != 0) {
return false;
}
size_t expected_length = (sizeof(prefix) - 1U) + CBM_SHA256_HEX_LEN + (sizeof(suffix) - 1U);
return strlen(name) == expected_length &&
strncmp(name + sizeof(prefix) - 1U, sha256, CBM_SHA256_HEX_LEN) == 0 &&
strcmp(name + expected_length - (sizeof(suffix) - 1U), suffix) == 0;
}
bool cbm_ui_assets_supported(void) {
const char *name = ui_manifest_name();
const char *sha256 = ui_manifest_sha256();
uint64_t size = ui_manifest_size();
return size >= UI_PACK_HEADER_BYTES && size <= UI_PACK_MAX_BYTES && ui_hex_sha256(sha256) &&
ui_content_addressed_name(name, sha256);
}
const char *cbm_ui_assets_current_pack_name(void) {
return cbm_ui_assets_supported() ? ui_manifest_name() : NULL;
}
void cbm_ui_assets_set_binary_path(const char *path) {
if (!path) {
g_binary_path[0] = '\0';
return;
}
int written = snprintf(g_binary_path, sizeof(g_binary_path), "%s", path);
if (written <= 0 || (size_t)written >= sizeof(g_binary_path)) {
g_binary_path[0] = '\0';
}
}
static void ui_fill_error(char *err, size_t err_sz, const char *detail) {
if (!err || err_sz == 0U) {
return;
}
(void)snprintf(err, err_sz,
"UI assets missing or modified - reinstall the matching release "
"(expected %s%s%s)",
ui_manifest_name(), detail && detail[0] ? ": " : "", detail ? detail : "");
}
static uint16_t ui_read_u16(const unsigned char *bytes) {
return (uint16_t)bytes[0] | ((uint16_t)bytes[1] << 8U);
}
static uint32_t ui_read_u32(const unsigned char *bytes) {
return (uint32_t)bytes[0] | ((uint32_t)bytes[1] << 8U) | ((uint32_t)bytes[2] << 16U) |
((uint32_t)bytes[3] << 24U);
}
static uint64_t ui_read_u64(const unsigned char *bytes) {
uint64_t value = 0U;
for (size_t index = 0U; index < sizeof(uint64_t); index++) {
value |= (uint64_t)bytes[index] << (index * 8U);
}
return value;
}
static const char *ui_mime_type(unsigned int mime_id) {
static const char *const types[] = {
NULL,
"text/html; charset=utf-8",
"application/javascript; charset=utf-8",
"text/css; charset=utf-8",
"application/json; charset=utf-8",
"image/svg+xml",
"image/png",
"image/jpeg",
"image/webp",
"image/avif",
"image/x-icon",
"font/woff2",
"font/woff",
"application/wasm",
};
return mime_id < sizeof(types) / sizeof(types[0]) ? types[mime_id] : NULL;
}
static unsigned int ui_expected_mime(const char *path) {
const char *dot = strrchr(path, '.');
if (!dot) {
return 0U;
}
if (strcmp(dot, ".html") == 0)
return 1U;
if (strcmp(dot, ".js") == 0 || strcmp(dot, ".mjs") == 0)
return 2U;
if (strcmp(dot, ".css") == 0)
return 3U;
if (strcmp(dot, ".json") == 0)
return 4U;
if (strcmp(dot, ".svg") == 0)
return 5U;
if (strcmp(dot, ".png") == 0)
return 6U;
if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
return 7U;
if (strcmp(dot, ".webp") == 0)
return 8U;
if (strcmp(dot, ".avif") == 0)
return 9U;
if (strcmp(dot, ".ico") == 0)
return 10U;
if (strcmp(dot, ".woff2") == 0)
return 11U;
if (strcmp(dot, ".woff") == 0)
return 12U;
if (strcmp(dot, ".wasm") == 0)
return 13U;
return 0U;
}
static bool ui_path_valid(const unsigned char *path, size_t length) {
if (length < 2U || length > UI_PACK_MAX_PATH_BYTES || path[0] != '/' ||
path[length - 1U] == '/') {
return false;
}
for (size_t index = 0U; index < length; index++) {
unsigned char byte = path[index];
bool allowed = (byte >= 'A' && byte <= 'Z') || (byte >= 'a' && byte <= 'z') ||
(byte >= '0' && byte <= '9') || byte == '.' || byte == '_' || byte == '-' ||
byte == '/';
if (!allowed || (byte == '/' && index + 1U < length && path[index + 1U] == '/')) {
return false;
}
}
for (size_t start = 1U; start < length;) {
size_t end = start;
while (end < length && path[end] != '/') {
end++;
}
size_t segment = end - start;
if ((segment == 1U && path[start] == '.') ||
(segment == 2U && path[start] == '.' && path[start + 1U] == '.')) {
return false;
}
start = end + 1U;
}
static const unsigned char index_path[] = "/index.html";
static const unsigned char asset_prefix[] = "/assets/";
return (length == sizeof(index_path) - 1U &&
memcmp(path, index_path, sizeof(index_path) - 1U) == 0) ||
(length > sizeof(asset_prefix) - 1U &&
memcmp(path, asset_prefix, sizeof(asset_prefix) - 1U) == 0);
}
static int ui_path_compare(const unsigned char *left, size_t left_length,
const unsigned char *right, size_t right_length) {
size_t common = left_length < right_length ? left_length : right_length;
int compared = memcmp(left, right, common);
if (compared != 0) {
return compared;
}
return left_length < right_length ? -1 : left_length > right_length ? 1 : 0;
}
static void ui_snapshot_free(ui_asset_snapshot_t *snapshot) {
if (!snapshot) {
return;
}
free(snapshot->paths);
free(snapshot->assets);
free(snapshot->bytes);
memset(snapshot, 0, sizeof(*snapshot));
}
static bool ui_parse_pack(unsigned char *bytes, size_t length, ui_asset_snapshot_t *out) {
/* Keep the seven format bytes as separate comparisons. A contiguous
* native-binary copy would make the composition gate unable to
* distinguish this parser constant from an accidentally embedded pack. */
bool valid_magic = bytes && length >= 8U && bytes[0] == 'C' && bytes[1] == 'B' &&
bytes[2] == 'M' && bytes[3] == 'U' && bytes[4] == 'I' && bytes[5] == 'P' &&
bytes[6] == 'K' && bytes[7] == 0U;
if (!bytes || !out || length < UI_PACK_HEADER_BYTES || !valid_magic ||
ui_read_u16(bytes + 8U) != UI_PACK_VERSION ||
ui_read_u16(bytes + 10U) != UI_PACK_HEADER_BYTES || ui_read_u32(bytes + 12U) != 0U) {
return false;
}
uint32_t file_count = ui_read_u32(bytes + 16U);
uint32_t entry_bytes = ui_read_u32(bytes + 20U);
uint64_t index_offset = ui_read_u64(bytes + 24U);
uint64_t index_bytes = ui_read_u64(bytes + 32U);
uint64_t paths_offset = ui_read_u64(bytes + 40U);
uint64_t paths_bytes = ui_read_u64(bytes + 48U);
uint64_t payload_offset = ui_read_u64(bytes + 56U);
uint64_t payload_bytes = ui_read_u64(bytes + 64U);
uint64_t total_bytes = ui_read_u64(bytes + 72U);
uint64_t length_u64 = (uint64_t)length;
if (file_count == 0U || file_count > UI_PACK_MAX_FILES || entry_bytes != UI_PACK_ENTRY_BYTES ||
index_offset != UI_PACK_HEADER_BYTES ||
index_bytes != (uint64_t)file_count * UI_PACK_ENTRY_BYTES || index_offset > length_u64 ||
index_bytes > length_u64 - index_offset || paths_offset != index_offset + index_bytes ||
paths_offset > length_u64 || paths_bytes > length_u64 - paths_offset ||
payload_offset != paths_offset + paths_bytes || payload_offset > length_u64 ||
payload_bytes > length_u64 - payload_offset ||
total_bytes != payload_offset + payload_bytes || total_bytes != length_u64 ||
paths_bytes > SIZE_MAX - file_count || payload_bytes > SIZE_MAX) {
return false;
}
cbm_ui_asset_t *assets = calloc(file_count, sizeof(*assets));
char *paths = malloc((size_t)paths_bytes + file_count);
if (!assets || !paths) {
free(assets);
free(paths);
return false;
}
uint64_t expected_path_offset = 0U;
uint64_t expected_data_offset = 0U;
size_t path_storage_offset = 0U;
bool has_index = false;
const unsigned char *previous_path = NULL;
size_t previous_length = 0U;
for (uint32_t index = 0U; index < file_count; index++) {
const unsigned char *entry =
bytes + (size_t)index_offset + (size_t)index * UI_PACK_ENTRY_BYTES;
uint32_t path_offset = ui_read_u32(entry);
uint16_t path_length = ui_read_u16(entry + 4U);
unsigned int mime_id = entry[6U];
unsigned int cache_id = entry[7U];
uint64_t data_offset = ui_read_u64(entry + 8U);
uint64_t data_length = ui_read_u64(entry + 16U);
if (path_offset != expected_path_offset || path_length == 0U ||
(uint64_t)path_length > paths_bytes - expected_path_offset ||
data_offset != expected_data_offset || data_length == 0U ||
data_length > payload_bytes - expected_data_offset) {
free(assets);
free(paths);
return false;
}
const unsigned char *path_bytes = bytes + (size_t)paths_offset + path_offset;
if (!ui_path_valid(path_bytes, path_length) ||
(previous_path &&
ui_path_compare(previous_path, previous_length, path_bytes, path_length) >= 0)) {
free(assets);
free(paths);
return false;
}
char *stored_path = paths + path_storage_offset;
memcpy(stored_path, path_bytes, path_length);
stored_path[path_length] = '\0';
const char *mime = ui_mime_type(mime_id);
bool index_asset = strcmp(stored_path, "/index.html") == 0;
if (!mime || ui_expected_mime(stored_path) != mime_id ||
(index_asset ? cache_id != CBM_UI_ASSET_REVALIDATE
: cache_id != CBM_UI_ASSET_IMMUTABLE) ||
(index_asset && has_index)) {
free(assets);
free(paths);
return false;
}
has_index = has_index || index_asset;
assets[index] = (cbm_ui_asset_t){
.path = stored_path,
.data = bytes + (size_t)payload_offset + (size_t)data_offset,
.size = (size_t)data_length,
.content_type = mime,
.cache = (cbm_ui_asset_cache_t)cache_id,
};
previous_path = path_bytes;
previous_length = path_length;
expected_path_offset += path_length;
expected_data_offset += data_length;
path_storage_offset += path_length + 1U;
}
if (!has_index || expected_path_offset != paths_bytes ||
expected_data_offset != payload_bytes) {
free(assets);
free(paths);
return false;
}
*out = (ui_asset_snapshot_t){
.bytes = bytes,
.length = length,
.assets = assets,
.asset_count = file_count,
.paths = paths,
};
return true;
}
static bool ui_read_regular_file(const char *path, unsigned char **bytes_out, size_t *length_out) {
*bytes_out = NULL;
*length_out = 0U;
cbm_path_info_t info;
uint64_t expected = ui_manifest_size();
if (!path || cbm_path_info_utf8(path, &info) != 0 || !info.is_regular || info.is_symlink ||
info.size <= 0 || (uint64_t)info.size != expected || expected > UI_PACK_MAX_BYTES) {
return false;
}
unsigned char *bytes = malloc((size_t)expected);
if (!bytes) {
return false;
}
#ifdef _WIN32
wchar_t *wide = cbm_path_to_wide(path);
HANDLE file = wide ? CreateFileW(wide, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, NULL)
: INVALID_HANDLE_VALUE;
free(wide);
if (file == INVALID_HANDLE_VALUE) {
free(bytes);
return false;
}
BY_HANDLE_FILE_INFORMATION handle_info;
bool handle_info_ok = GetFileInformationByHandle(file, &handle_info) != 0;
uint64_t handle_size = handle_info_ok ? ((uint64_t)handle_info.nFileSizeHigh << 32U) |
(uint64_t)handle_info.nFileSizeLow
: UINT64_MAX;
bool ok = handle_info_ok && handle_size == expected &&
!(handle_info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) &&
!(handle_info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY);
size_t offset = 0U;
while (ok && offset < (size_t)expected &&
!atomic_load_explicit(&g_cancel_requested, memory_order_acquire)) {
DWORD chunk =
(DWORD)(((size_t)expected - offset) > CBM_SZ_64K ? CBM_SZ_64K
: ((size_t)expected - offset));
DWORD got = 0U;
ok = ReadFile(file, bytes + offset, chunk, &got, NULL) && got == chunk;
offset += got;
}
(void)CloseHandle(file);
ok = ok && offset == (size_t)expected;
#else
int flags = O_RDONLY;
#ifdef O_CLOEXEC
flags |= O_CLOEXEC;
#endif
#ifdef O_NOFOLLOW
flags |= O_NOFOLLOW;
#endif
int file = open(path, flags);
struct stat opened_info;
bool ok = file >= 0 && fstat(file, &opened_info) == 0 && S_ISREG(opened_info.st_mode) &&
(uint64_t)opened_info.st_size == expected;
size_t offset = 0U;
while (ok && offset < (size_t)expected &&
!atomic_load_explicit(&g_cancel_requested, memory_order_acquire)) {
size_t chunk = (size_t)expected - offset;
if (chunk > CBM_SZ_64K) {
chunk = CBM_SZ_64K;
}
ssize_t got = read(file, bytes + offset, chunk);
if (got < 0 && errno == EINTR) {
continue;
}
if (got <= 0) {
ok = false;
break;
}
offset += (size_t)got;
}
if (file >= 0) {
(void)close(file);
}
ok = ok && offset == (size_t)expected;
#endif
if (!ok || atomic_load_explicit(&g_cancel_requested, memory_order_acquire)) {
free(bytes);
return false;
}
*bytes_out = bytes;
*length_out = (size_t)expected;
return true;
}
static bool ui_verify_bytes(const unsigned char *bytes, size_t length) {
cbm_sha256_ctx hash;
cbm_sha256_init(&hash);
for (size_t offset = 0U; offset < length; offset += CBM_SZ_64K) {
if (atomic_load_explicit(&g_cancel_requested, memory_order_acquire)) {
return false;
}
size_t chunk = length - offset;
if (chunk > CBM_SZ_64K) {
chunk = CBM_SZ_64K;
}
cbm_sha256_update(&hash, bytes + offset, chunk);
}
uint8_t digest[CBM_SHA256_DIGEST_LEN];
cbm_sha256_final(&hash, digest);
static const char hex_chars[] = "0123456789abcdef";
char hex[CBM_SHA256_HEX_LEN + 1U];
for (size_t index = 0U; index < sizeof(digest); index++) {
hex[index * 2U] = hex_chars[digest[index] >> 4U];
hex[index * 2U + 1U] = hex_chars[digest[index] & 0x0fU];
}
hex[CBM_SHA256_HEX_LEN] = '\0';
return strcmp(hex, ui_manifest_sha256()) == 0;
}
static bool ui_candidate_path(const char *directory, char out[UI_PATH_CAP]) {
if (!directory || !directory[0]) {
return false;
}
int written = snprintf(out, UI_PATH_CAP, "%s/%s", directory, ui_manifest_name());
return written > 0 && written < UI_PATH_CAP;
}
static bool ui_binary_directory(char out[UI_PATH_CAP]) {
int written = snprintf(out, UI_PATH_CAP, "%s", g_binary_path);
if (written <= 0 || written >= UI_PATH_CAP) {
return false;
}
char *slash = strrchr(out, '/');
#ifdef _WIN32
char *backslash = strrchr(out, '\\');
if (backslash && (!slash || backslash > slash)) {
slash = backslash;
}
#endif
if (!slash || slash == out) {
return false;
}
*slash = '\0';
return true;
}
static bool ui_try_path(const char *path, ui_asset_snapshot_t *snapshot) {
unsigned char *bytes = NULL;
size_t length = 0U;
if (!ui_read_regular_file(path, &bytes, &length)) {
return false;
}
if (!ui_verify_bytes(bytes, length) || !ui_parse_pack(bytes, length, snapshot)) {
free(bytes);
return false;
}
return true;
}
bool cbm_ui_assets_verify_file(const char *path) {
if (!cbm_ui_assets_supported() || !path || !path[0]) {
return false;
}
ui_asset_snapshot_t verified = {0};
bool ok = ui_try_path(path, &verified);
ui_snapshot_free(&verified);
return ok;
}
static bool ui_resolve_snapshot(const char *home, ui_asset_snapshot_t *snapshot) {
static const char missing[] = "\x1f"
"CBM_UI_ASSETS_DIR_MISSING"
"\x1f";
char env_buffer[UI_PATH_CAP];
char candidate[UI_PATH_CAP];
const char *override =
cbm_safe_getenv("CBM_UI_ASSETS_DIR", env_buffer, sizeof(env_buffer), missing);
if (!override) {
return false;
}
if (strcmp(override, missing) != 0 && override[0]) {
return ui_candidate_path(override, candidate) && ui_try_path(candidate, snapshot);
}
char binary_directory[UI_PATH_CAP];
if (ui_binary_directory(binary_directory)) {
if (ui_candidate_path(binary_directory, candidate)) {
cbm_path_info_t info;
if (cbm_path_info_utf8(candidate, &info) == 0) {
return ui_try_path(candidate, snapshot);
}
}
int written = snprintf(candidate, sizeof(candidate), "%s/../share/codebase-memory-mcp/%s",
binary_directory, ui_manifest_name());
if (written > 0 && (size_t)written < sizeof(candidate)) {
cbm_path_info_t info;
if (cbm_path_info_utf8(candidate, &info) == 0) {
return ui_try_path(candidate, snapshot);
}
}
}
(void)home;
return false;
}
bool cbm_ui_assets_warm(const char *home, char *err, size_t err_sz) {
if (!cbm_ui_assets_supported()) {
return true;
}
int expected = CBM_UI_ASSETS_COLD;
if (!atomic_compare_exchange_strong_explicit(&g_state, &expected, CBM_UI_ASSETS_LOADING,
memory_order_acq_rel, memory_order_acquire)) {
if (expected == CBM_UI_ASSETS_READY) {
return true;
}
const char *detail = expected == CBM_UI_ASSETS_LOADING ? "load already in progress"
: expected == CBM_UI_ASSETS_CANCELLED ? "load cancelled"
: "load previously failed";
ui_fill_error(err, err_sz, detail);
return false;
}
ui_asset_snapshot_t snapshot = {0};
bool loaded = ui_resolve_snapshot(home, &snapshot);
if (atomic_load_explicit(&g_cancel_requested, memory_order_acquire)) {
ui_snapshot_free(&snapshot);
atomic_store_explicit(&g_state, CBM_UI_ASSETS_CANCELLED, memory_order_release);
ui_fill_error(err, err_sz, "load cancelled");
return false;
}
if (!loaded) {
ui_snapshot_free(&snapshot);
atomic_store_explicit(&g_state, CBM_UI_ASSETS_FAILED, memory_order_release);
ui_fill_error(err, err_sz, "verified pack not found");
return false;
}
g_snapshot = snapshot;
atomic_store_explicit(&g_state, CBM_UI_ASSETS_READY, memory_order_release);
return true;
}
void cbm_ui_assets_request_cancel(void) {
atomic_store_explicit(&g_cancel_requested, true, memory_order_release);
int expected = CBM_UI_ASSETS_COLD;
(void)atomic_compare_exchange_strong_explicit(&g_state, &expected, CBM_UI_ASSETS_CANCELLED,
memory_order_acq_rel, memory_order_acquire);
}
cbm_ui_assets_state_t cbm_ui_assets_state(void) {
if (!cbm_ui_assets_supported()) {
return CBM_UI_ASSETS_UNAVAILABLE;
}
return (cbm_ui_assets_state_t)atomic_load_explicit(&g_state, memory_order_acquire);
}
const cbm_ui_asset_t *cbm_ui_asset_lookup(const char *path) {
if (!path || cbm_ui_assets_state() != CBM_UI_ASSETS_READY) {
return NULL;
}
size_t low = 0U;
size_t high = g_snapshot.asset_count;
while (low < high) {
size_t middle = low + (high - low) / 2U;
int compared = strcmp(path, g_snapshot.assets[middle].path);
if (compared == 0) {
return &g_snapshot.assets[middle];
}
if (compared < 0) {
high = middle;
} else {
low = middle + 1U;
}
}
return NULL;
}
bool cbm_ui_assets_stage_install(const char *install_dir,
cbm_activation_transaction_t **transaction_out, char *err,
size_t err_sz) {
if (transaction_out) {
*transaction_out = NULL;
}
if (!cbm_ui_assets_supported()) {
return true;
}
if (!transaction_out || !install_dir || !install_dir[0] ||
!cbm_ui_assets_warm(NULL, err, err_sz)) {
if (!install_dir || !install_dir[0]) {
ui_fill_error(err, err_sz, "install directory unavailable");
}
return false;
}
char stored[UI_PATH_CAP];
if (!ui_candidate_path(install_dir, stored)) {
ui_fill_error(err, err_sz, "install path is too long");
return false;
}
if (!cbm_mkdir_p(install_dir, UI_ASSETS_DIR_PERM)) {
ui_fill_error(err, err_sz, "cannot create the install directory");
return false;
}
cbm_activation_transaction_status_t status = cbm_activation_transaction_stage_bytes(
stored, g_snapshot.bytes, g_snapshot.length, transaction_out);
if (status != CBM_ACTIVATION_TRANSACTION_OK || !*transaction_out) {
if (*transaction_out) {
(void)cbm_activation_transaction_close(transaction_out);
}
ui_fill_error(err, err_sz, "cannot stage the verified pack");
return false;
}
return true;
}
static bool ui_install_validator(const char *target_path, void *context) {
(void)context;
return cbm_ui_assets_verify_file(target_path);
}
cbm_activation_transaction_status_t cbm_ui_assets_commit_install(
cbm_activation_transaction_t *transaction) {
if (!transaction) {
return CBM_ACTIVATION_TRANSACTION_INVALID_ARGUMENT;
}
cbm_activation_transaction_status_t status =
cbm_activation_transaction_commit(transaction, ui_install_validator, NULL);
#ifndef _WIN32
if (status == CBM_ACTIVATION_TRANSACTION_OK) {
const char *target = cbm_activation_transaction_target_path(transaction);
if (!target || chmod(target, UI_ASSET_FILE_PERM) != 0) {
cbm_activation_transaction_status_t rollback =
cbm_activation_transaction_rollback(transaction);
return rollback == CBM_ACTIVATION_TRANSACTION_OK
? CBM_ACTIVATION_TRANSACTION_IO
: CBM_ACTIVATION_TRANSACTION_ROLLBACK_FAILED;
}
}
#endif
return status;
}
cbm_activation_transaction_status_t cbm_ui_assets_commit_removal(
cbm_activation_transaction_t *transaction) {
if (!transaction) {
return CBM_ACTIVATION_TRANSACTION_INVALID_ARGUMENT;
}
cbm_activation_transaction_status_t status =
cbm_activation_transaction_commit(transaction, NULL, NULL);
const char *retained = cbm_activation_transaction_backup_path(transaction);
if (status != CBM_ACTIVATION_TRANSACTION_OK) {
return status;
}
if (retained && cbm_ui_assets_verify_file(retained)) {
return CBM_ACTIVATION_TRANSACTION_OK;
}
cbm_activation_transaction_status_t rollback =
cbm_activation_transaction_rollback(transaction);
return rollback == CBM_ACTIVATION_TRANSACTION_OK
? CBM_ACTIVATION_TRANSACTION_VALIDATION_FAILED
: CBM_ACTIVATION_TRANSACTION_ROLLBACK_FAILED;
}
#ifdef CBM_CLI_ENABLE_TEST_API
bool cbm_ui_assets_install(const char *install_dir, bool dry_run, char *err, size_t err_sz) {
if (!cbm_ui_assets_supported()) {
return true;
}
if (!install_dir || !install_dir[0] || !cbm_ui_assets_warm(NULL, err, err_sz)) {
if (!install_dir || !install_dir[0]) {
ui_fill_error(err, err_sz, "install directory unavailable");
}
return false;
}
if (dry_run) {
return true;
}
cbm_activation_transaction_t *transaction = NULL;
if (!cbm_ui_assets_stage_install(install_dir, &transaction, err, err_sz)) {
return false;
}
cbm_activation_transaction_status_t status = cbm_ui_assets_commit_install(transaction);
if (status != CBM_ACTIVATION_TRANSACTION_OK) {
(void)cbm_activation_transaction_close(&transaction);
ui_fill_error(err, err_sz, "cannot publish the verified pack");
return false;
}
status = cbm_activation_transaction_finalize(transaction);
bool finalized =
status == CBM_ACTIVATION_TRANSACTION_OK || status == CBM_ACTIVATION_TRANSACTION_DEFERRED;
bool closed = cbm_activation_transaction_close(&transaction) == CBM_ACTIVATION_TRANSACTION_OK;
if (!finalized || !closed) {
ui_fill_error(err, err_sz, "cannot finalize the verified pack");
return false;
}
return true;
}
#endif
bool cbm_ui_assets_remove(const char *install_dir, bool dry_run, char *err, size_t err_sz) {
if (!cbm_ui_assets_supported()) {
return true;
}
char path[UI_PATH_CAP];
if (!ui_candidate_path(install_dir, path)) {
ui_fill_error(err, err_sz, "uninstall path is too long");
return false;
}
cbm_path_info_t info;
if (cbm_path_info_utf8(path, &info) != 0) {
return true;
}
ui_asset_snapshot_t verified = {0};
if (!ui_try_path(path, &verified)) {
ui_fill_error(err, err_sz, "adjacent pack is not owned; preserved");
return false;
}
ui_snapshot_free(&verified);
if (dry_run) {
return true;
}
cbm_activation_transaction_t *transaction = NULL;
cbm_activation_transaction_status_t status =
cbm_activation_transaction_stage_removal(path, &transaction);
if (status == CBM_ACTIVATION_TRANSACTION_OK && transaction) {
status = cbm_ui_assets_commit_removal(transaction);
}
if (status != CBM_ACTIVATION_TRANSACTION_OK || !transaction) {
(void)cbm_activation_transaction_close(&transaction);
ui_fill_error(err, err_sz,
status == CBM_ACTIVATION_TRANSACTION_VALIDATION_FAILED
? "adjacent pack changed before removal; preserved"
: "cannot remove the owned adjacent pack");
return false;
}
status = cbm_activation_transaction_finalize(transaction);
bool finalized =
status == CBM_ACTIVATION_TRANSACTION_OK || status == CBM_ACTIVATION_TRANSACTION_DEFERRED;
bool closed = cbm_activation_transaction_close(&transaction) == CBM_ACTIVATION_TRANSACTION_OK;
if (!finalized || !closed) {
ui_fill_error(err, err_sz, "cannot finalize the owned adjacent pack removal");
return false;
}
return true;
}
bool cbm_ui_assets_stage_remove(const char *install_dir,
cbm_activation_transaction_t **transaction_out,
bool *foreign_preserved_out, char *err, size_t err_sz) {
if (transaction_out) {
*transaction_out = NULL;
}
if (foreign_preserved_out) {
*foreign_preserved_out = false;
}
if (!transaction_out) {
ui_fill_error(err, err_sz, "uninstall transaction output unavailable");
return false;
}
if (!cbm_ui_assets_supported()) {
return true;
}
char path[UI_PATH_CAP];
if (!install_dir || !install_dir[0] || !ui_candidate_path(install_dir, path)) {
ui_fill_error(err, err_sz, "uninstall path is too long");
return false;
}
cbm_path_info_t info;
if (cbm_path_info_utf8(path, &info) != 0) {
return true;
}
if (!info.is_regular || info.is_symlink || !cbm_ui_assets_verify_file(path)) {
if (foreign_preserved_out) {
*foreign_preserved_out = true;
}
return true;
}
cbm_activation_transaction_status_t status =
cbm_activation_transaction_stage_removal(path, transaction_out);
if (status != CBM_ACTIVATION_TRANSACTION_OK || !*transaction_out) {
ui_fill_error(err, err_sz, cbm_activation_transaction_status_message(status));
if (*transaction_out) {
(void)cbm_activation_transaction_close(transaction_out);
}
return false;
}
/* Revalidate after the transaction captured target identity. Commit will
* refuse if that identity changes before removal. */
if (!cbm_ui_assets_verify_file(path)) {
if (cbm_activation_transaction_close(transaction_out) != CBM_ACTIVATION_TRANSACTION_OK) {
ui_fill_error(err, err_sz, "cannot clean up a changed pack removal transaction");
return false;
}
if (foreign_preserved_out) {
*foreign_preserved_out = true;
}
}
return true;
}
#ifdef CBM_CLI_ENABLE_TEST_API
void cbm_ui_assets_set_manifest_for_testing(const char *name, const char *sha256, uint64_t size) {
int name_written = snprintf(g_test_pack_name, sizeof(g_test_pack_name), "%s", name ? name : "");
int hash_written = snprintf(g_test_sha256, sizeof(g_test_sha256), "%s", sha256 ? sha256 : "");
g_test_manifest_set = name_written > 0 && (size_t)name_written < sizeof(g_test_pack_name) &&
hash_written == CBM_SHA256_HEX_LEN;
g_test_size = size;
}
void cbm_ui_assets_reset_for_testing(void) {
ui_snapshot_free(&g_snapshot);
atomic_store_explicit(&g_state, CBM_UI_ASSETS_COLD, memory_order_release);
atomic_store_explicit(&g_cancel_requested, false, memory_order_release);
g_binary_path[0] = '\0';
g_test_manifest_set = false;
g_test_pack_name[0] = '\0';
g_test_sha256[0] = '\0';
g_test_size = 0U;
}
#endif
-111
View File
@@ -1,111 +0,0 @@
/*
* asset_pack.h — Verified external frontend assets.
*
* UI builds embed only a content-addressed pack name, exact size and SHA-256.
* The uncompressed pack stays outside the native executable and is read once
* into an immutable process-lifetime snapshot before the HTTP listener starts.
*/
#ifndef CBM_UI_ASSET_PACK_H
#define CBM_UI_ASSET_PACK_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "cli/activation_transaction.h"
typedef enum {
CBM_UI_ASSETS_UNAVAILABLE = 0,
CBM_UI_ASSETS_COLD,
CBM_UI_ASSETS_LOADING,
CBM_UI_ASSETS_READY,
CBM_UI_ASSETS_FAILED,
CBM_UI_ASSETS_CANCELLED,
} cbm_ui_assets_state_t;
typedef enum {
CBM_UI_ASSET_REVALIDATE = 1,
CBM_UI_ASSET_IMMUTABLE = 2,
} cbm_ui_asset_cache_t;
typedef struct {
const char *path;
const unsigned char *data;
size_t size;
const char *content_type;
cbm_ui_asset_cache_t cache;
} cbm_ui_asset_t;
/* Defined by asset_pack_stub.c for standard builds, asset_manifest_stub.c for
* test runners, or the generated manifest for UI builds. No frontend payload
* bytes live in any of them. */
extern const char CBM_UI_ASSET_PACK_NAME[];
extern const char CBM_UI_ASSET_SHA256[];
extern const uint64_t CBM_UI_ASSET_SIZE;
/* Compile-time capability; performs no filesystem I/O and is true before the
* asynchronous warmup starts. */
bool cbm_ui_assets_supported(void);
const char *cbm_ui_assets_current_pack_name(void);
/* Exact binary path used to resolve the adjacent and package-share candidates.
* Call before starting warmup. */
void cbm_ui_assets_set_binary_path(const char *path);
/* Resolve, hash and structurally validate the complete pack, then publish one
* immutable lookup snapshot. A standard build is a successful no-op. */
bool cbm_ui_assets_warm(const char *home, char *err, size_t err_sz);
void cbm_ui_assets_request_cancel(void);
cbm_ui_assets_state_t cbm_ui_assets_state(void);
/* Returns NULL until the complete snapshot is READY. The returned view remains
* valid for the process lifetime. */
const cbm_ui_asset_t *cbm_ui_asset_lookup(const char *path);
/* Direct CLI install publishes the verified content-addressed pack beside the
* target executable before that executable becomes active. Uninstall removes
* only an exact hash-verified adjacent pack. */
bool cbm_ui_assets_remove(const char *install_dir, bool dry_run, char *err, size_t err_sz);
/* Verify one explicit pack against this binary's compiled name/size/hash and
* parser contract, without publishing a global snapshot. Standard builds
* accept no pack and return true only through the supported() no-op paths. */
bool cbm_ui_assets_verify_file(const char *path);
/* Stage removal of the exact owned adjacent pack. Missing/standard packs are
* successful no-ops; a foreign file is preserved and reported separately. */
bool cbm_ui_assets_stage_remove(const char *install_dir,
cbm_activation_transaction_t **transaction_out,
bool *foreign_preserved_out, char *err, size_t err_sz);
/* Commit an owned-pack removal only after the exact retained object (moved out
* of the public name by the transaction) still verifies against this binary.
* A same-inode rewrite after staging is rolled back and preserved. */
cbm_activation_transaction_status_t cbm_ui_assets_commit_removal(
cbm_activation_transaction_t *transaction);
/* Stage the current verified content-addressed pack without publishing it.
* Standard builds return true with a NULL transaction. UI callers retain the
* transaction through the coordinated binary commit, then finalize/rollback
* it while still holding the native activation guard. */
bool cbm_ui_assets_stage_install(const char *install_dir,
cbm_activation_transaction_t **transaction_out, char *err,
size_t err_sz);
/* Publish a staged pack with a post-rename parser/hash validator, binding the
* verification to the bytes at the final public name before finalization. */
cbm_activation_transaction_status_t cbm_ui_assets_commit_install(
cbm_activation_transaction_t *transaction);
#ifdef CBM_CLI_ENABLE_TEST_API
/* Focused lifecycle probe. Production publishes through stage_install as part
* of the native guarded runtime-set transaction. */
bool cbm_ui_assets_install(const char *install_dir, bool dry_run, char *err, size_t err_sz);
/* Single-threaded test seams for exercising the parser with caller-created
* fixtures whose digest is recomputed after each structural mutation. */
void cbm_ui_assets_set_manifest_for_testing(const char *name, const char *sha256, uint64_t size);
void cbm_ui_assets_reset_for_testing(void);
#endif
#endif /* CBM_UI_ASSET_PACK_H */
-98
View File
@@ -1,98 +0,0 @@
/* Standard builds have no frontend pack and perform no UI asset I/O. */
#include "ui/asset_pack.h"
const char CBM_UI_ASSET_PACK_NAME[] = "";
const char CBM_UI_ASSET_SHA256[] = "";
const uint64_t CBM_UI_ASSET_SIZE = UINT64_C(0);
bool cbm_ui_assets_supported(void) {
return false;
}
const char *cbm_ui_assets_current_pack_name(void) {
return NULL;
}
void cbm_ui_assets_set_binary_path(const char *path) {
(void)path;
}
bool cbm_ui_assets_warm(const char *home, char *err, size_t err_sz) {
(void)home;
(void)err;
(void)err_sz;
return true;
}
void cbm_ui_assets_request_cancel(void) {}
cbm_ui_assets_state_t cbm_ui_assets_state(void) {
return CBM_UI_ASSETS_UNAVAILABLE;
}
const cbm_ui_asset_t *cbm_ui_asset_lookup(const char *path) {
(void)path;
return NULL;
}
#ifdef CBM_CLI_ENABLE_TEST_API
bool cbm_ui_assets_install(const char *install_dir, bool dry_run, char *err, size_t err_sz) {
(void)install_dir;
(void)dry_run;
(void)err;
(void)err_sz;
return true;
}
#endif
bool cbm_ui_assets_remove(const char *install_dir, bool dry_run, char *err, size_t err_sz) {
(void)install_dir;
(void)dry_run;
(void)err;
(void)err_sz;
return true;
}
bool cbm_ui_assets_verify_file(const char *path) {
(void)path;
return false;
}
bool cbm_ui_assets_stage_remove(const char *install_dir,
cbm_activation_transaction_t **transaction_out,
bool *foreign_preserved_out, char *err, size_t err_sz) {
(void)install_dir;
(void)err;
(void)err_sz;
if (transaction_out) {
*transaction_out = NULL;
}
if (foreign_preserved_out) {
*foreign_preserved_out = false;
}
return transaction_out != NULL;
}
bool cbm_ui_assets_stage_install(const char *install_dir,
cbm_activation_transaction_t **transaction_out, char *err,
size_t err_sz) {
(void)install_dir;
(void)err;
(void)err_sz;
if (transaction_out) {
*transaction_out = NULL;
}
return transaction_out != NULL;
}
cbm_activation_transaction_status_t cbm_ui_assets_commit_install(
cbm_activation_transaction_t *transaction) {
return transaction ? CBM_ACTIVATION_TRANSACTION_INVALID_ARGUMENT
: CBM_ACTIVATION_TRANSACTION_OK;
}
cbm_activation_transaction_status_t cbm_ui_assets_commit_removal(
cbm_activation_transaction_t *transaction) {
return transaction ? CBM_ACTIVATION_TRANSACTION_INVALID_ARGUMENT
: CBM_ACTIVATION_TRANSACTION_OK;
}
+3 -4
View File
@@ -6,7 +6,7 @@
*/
#include "foundation/constants.h"
#include "ui/config.h"
#include "ui/asset_pack.h"
#include "ui/embedded_assets.h"
#include "foundation/log.h"
#include "foundation/platform.h"
#include "foundation/compat_fs.h"
@@ -122,9 +122,8 @@ void cbm_ui_config_load(cbm_ui_config_t *cfg) {
bool opened = false;
char *buffer = config_read_file(path, &length, &opened);
if (!opened) {
/* Capability does not transiently disappear while the daemon warms
* the verified external pack asynchronously. */
if (cbm_ui_assets_supported()) {
/* No config file — auto-enable UI if binary has embedded assets */
if (CBM_EMBEDDED_FILE_COUNT > 0) {
cfg->ui_enabled = true;
}
return;
+25
View File
@@ -0,0 +1,25 @@
/*
* embedded_assets.h — Interface for embedded frontend assets.
*
* When built with `cbm-with-ui`, the embed script generates embedded_assets.c
* with actual file data. Otherwise, embedded_stub.c provides an empty array.
*/
#ifndef CBM_UI_EMBEDDED_ASSETS_H
#define CBM_UI_EMBEDDED_ASSETS_H
typedef struct {
const char *path; /* URL path, e.g. "/index.html" */
const unsigned char *data; /* raw file bytes */
unsigned int size; /* byte count */
const char *content_type; /* MIME type, e.g. "text/html" */
} cbm_embedded_file_t;
/* Array of embedded files + count. Defined in embedded_assets.c or embedded_stub.c.
* Not const — sizes are initialized at startup by a constructor function. */
extern cbm_embedded_file_t CBM_EMBEDDED_FILES[];
extern const int CBM_EMBEDDED_FILE_COUNT;
/* Look up an embedded file by URL path. Returns NULL if not found. */
const cbm_embedded_file_t *cbm_embedded_lookup(const char *path);
#endif /* CBM_UI_EMBEDDED_ASSETS_H */
+22
View File
@@ -0,0 +1,22 @@
/*
* embedded_stub.c — Empty asset table when built without frontend.
*
* Used by the standard `cbm` target (no Node.js required).
* The `cbm-with-ui` target replaces this with generated embedded_assets.c.
*/
#include "ui/embedded_assets.h"
#include <stddef.h>
#include <string.h>
cbm_embedded_file_t CBM_EMBEDDED_FILES[] = {{NULL, NULL, 0, NULL}};
const int CBM_EMBEDDED_FILE_COUNT = 0;
const cbm_embedded_file_t *cbm_embedded_lookup(const char *path) {
for (int i = 0; i < CBM_EMBEDDED_FILE_COUNT; i++) {
if (strcmp(CBM_EMBEDDED_FILES[i].path, path) == 0) {
return &CBM_EMBEDDED_FILES[i];
}
}
return NULL;
}
+15 -19
View File
@@ -3,8 +3,8 @@
*
* Transport (sockets, parsing, limits) lives in httpd.c; this file owns
* the routes and their handlers:
* GET / → verified external index.html
* GET /assets/... → verified external JS/CSS
* GET / → embedded index.html
* GET /assets/... → embedded JS/CSS
* POST /rpc → JSON-RPC dispatch via own cbm_mcp_server_t
* OPTIONS /rpc → CORS preflight (for vite dev on :5173)
* GET/POST /api/... → UI support endpoints (layout, index, browse, …)
@@ -15,7 +15,7 @@
*/
#include "ui/http_server.h"
#include "ui/httpd.h"
#include "ui/asset_pack.h"
#include "ui/embedded_assets.h"
#include "ui/layout3d.h"
#include "mcp/mcp.h"
#include "store/store.h"
@@ -186,7 +186,7 @@ struct cbm_http_server {
bool readiness_secret_set;
};
/* ── Serve verified frontend asset ───────────────────────────── */
/* ── Serve embedded asset ─────────────────────────────────────── */
/* Content-Security-Policy for the served UI. No external host appears in any
* directive, so the browser cannot load or connect to anything off-origin —
@@ -201,22 +201,20 @@ struct cbm_http_server {
"style-src 'self' 'unsafe-inline'; font-src 'self' data:; " \
"worker-src 'self' blob:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'\r\n"
static bool serve_frontend_asset(cbm_http_conn_t *c, const char *path) {
const cbm_ui_asset_t *f = cbm_ui_asset_lookup(path);
static bool serve_embedded(cbm_http_conn_t *c, const char *path) {
const cbm_embedded_file_t *f = cbm_embedded_lookup(path);
if (!f)
return false;
/* Build headers with correct Content-Type for this asset */
char hdrs[1024];
const char *cache = f->cache == CBM_UI_ASSET_REVALIDATE
? "Cache-Control: no-cache\r\n"
: "Cache-Control: public, max-age=31536000, immutable\r\n";
snprintf(hdrs, sizeof(hdrs),
"%sContent-Type: %s\r\n"
"%sX-Content-Type-Options: nosniff\r\n" CBM_UI_CSP,
g_cors, f->content_type, cache);
"Cache-Control: public, max-age=31536000, immutable\r\n"
"X-Content-Type-Options: nosniff\r\n" CBM_UI_CSP,
g_cors, f->content_type);
cbm_http_reply_buf(c, 200, hdrs, f->data, f->size);
cbm_http_reply_buf(c, 200, hdrs, f->data, (size_t)f->size);
return true;
}
@@ -992,7 +990,6 @@ void cbm_http_server_set_binary_path(const char *path) {
g_binary_path[0] = '\0';
}
}
cbm_ui_assets_set_binary_path(g_binary_path);
}
/* Execute through the daemon's shared job registry. The thread is retained in
@@ -1915,23 +1912,22 @@ static void dispatch_request(cbm_http_server_t *srv, cbm_http_conn_t *c,
/* GET / → index.html (no-cache so browser always gets latest) */
if (cbm_http_path_match(req->path, "/")) {
const cbm_ui_asset_t *f = cbm_ui_asset_lookup("/index.html");
const cbm_embedded_file_t *f = cbm_embedded_lookup("/index.html");
if (f) {
char html_hdrs[1024];
snprintf(html_hdrs, sizeof(html_hdrs),
"%sContent-Type: text/html; charset=utf-8\r\nCache-Control: no-cache\r\n"
"X-Content-Type-Options: nosniff\r\n" CBM_UI_CSP,
g_cors);
cbm_http_reply_buf(c, 200, html_hdrs, f->data, f->size);
cbm_http_reply_buf(c, 200, html_hdrs, f->data, (size_t)f->size);
return;
}
cbm_http_replyf(c, 503, "Cache-Control: no-store\r\nRetry-After: 1\r\n",
"frontend assets are not ready");
cbm_http_replyf(c, 404, g_cors, "no frontend embedded");
return;
}
/* GET /assets/... → exact lookup in the immutable verified pack. */
if (serve_frontend_asset(c, req->path))
/* GET /assets/... → exact lookup in the embedded asset table. */
if (serve_embedded(c, req->path))
return;
cbm_http_replyf(c, 404, g_cors, "not found");
+1 -1
View File
@@ -2,7 +2,7 @@
* http_server.h — Embedded HTTP server for the graph visualization UI.
*
* Binds to 127.0.0.1:<port> only (localhost).
* Serves the verified external frontend pack and proxies /rpc to a dedicated
* Serves embedded frontend assets and proxies /rpc to a dedicated
* read-only cbm_mcp_server_t instance.
*
* Runs in a background pthread, same pattern as the watcher thread.
+1 -1
View File
@@ -116,7 +116,7 @@ void cbm_http_replyf(cbm_http_conn_t *c, int status, const char *extra_headers,
#endif
;
/* Binary-safe variant for verified asset-pack entries. */
/* Binary-safe variant for embedded assets. */
void cbm_http_reply_buf(cbm_http_conn_t *c, int status, const char *extra_headers, const void *data,
size_t len);
+17 -68
View File
@@ -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 one-executable runtime-set 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).
@@ -28,15 +28,14 @@ Run inside the VM's CLANGARM64 shell (or a CI msys2 shell) from the repo root.
Environment:
SMOKE_ARCH arm64 (default) | amd64 — selects the served artifact name.
SMOKE_VARIANT standard (default) | ui. ui requires the external UI pack:
SMOKE_VARIANT standard (default) | ui. ui requires the embedded UI:
Phase 15's "no assets" SKIP becomes a FAILURE, so a standard
binary cannot pass a ui run.
CBM_SMOKE_ARTIFACT_DIR
Release mode: an EXTRACTED windows release artifact
(codebase-memory-mcp.exe + cbm-integrations.json + LICENSE +
install.ps1 + THIRD_PARTY_NOTICES.md + the UI pack when selected). The
exact variant set is served verbatim; an incomplete archive
fails the smoke.
(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).
@@ -60,7 +59,7 @@ ARTIFACT_DIR="${CBM_SMOKE_ARTIFACT_DIR:-}"
if [ -n "$ARTIFACT_DIR" ]; then
ARTIFACT_DIR="$(cd "$ARTIFACT_DIR" && pwd)"
for required in codebase-memory-mcp.exe \
cbm-integrations.json LICENSE install.ps1 THIRD_PARTY_NOTICES.md; do
LICENSE install.ps1 THIRD_PARTY_NOTICES.md; do
[ -s "$ARTIFACT_DIR/$required" ] ||
{ echo "vm-smoke: release artifact is missing $required" >&2; exit 2; }
done
@@ -76,9 +75,9 @@ case "$SMOKE_ARCH" in
arm64 | amd64) ;;
*) echo "vm-smoke: SMOKE_ARCH must be arm64 or amd64. Please consult --help." >&2; exit 2 ;;
esac
# A ui run must be handed a binary with exactly one external asset pack beside
# it; the suffix alone only renames the archive. SMOKE_REQUIRE_UI turns Phase
# 15's documented "no UI assets" SKIP into a failure, so asking for ui and
# A ui run must be handed a binary that actually carries the embedded assets;
# the suffix alone only renames the archive. SMOKE_REQUIRE_UI turns Phase 15's
# documented "no embedded assets" SKIP into a failure, so asking for ui and
# supplying a standard binary can no longer pass quietly.
case "$SMOKE_VARIANT" in
standard) SUFFIX="" ; REQUIRE_UI=0 ;;
@@ -86,37 +85,6 @@ ui) SUFFIX="-ui" ; REQUIRE_UI=1 ;;
*) echo "vm-smoke: SMOKE_VARIANT must be standard or ui. Please consult --help." >&2; exit 2 ;;
esac
if [ -n "$ARTIFACT_DIR" ]; then
UI_PACK_SOURCE_DIR="$ARTIFACT_DIR"
else
UI_PACK_SOURCE_DIR="$(dirname "$BINARY_SRC")"
fi
shopt -s nullglob
UI_PACK_CANDIDATES=("$UI_PACK_SOURCE_DIR"/cbm-ui-*.pack)
shopt -u nullglob
UI_PACK_COUNT=0
UI_PACK_NAME=""
UI_PACK_SOURCE=""
for candidate in "${UI_PACK_CANDIDATES[@]+"${UI_PACK_CANDIDATES[@]}"}"; do
UI_PACK_COUNT=$((UI_PACK_COUNT + 1))
UI_PACK_SOURCE="$candidate"
candidate_name="$(basename "$candidate")"
if ! [[ "$candidate_name" =~ ^cbm-ui-[0-9a-f]{64}\.pack$ ]]; then
echo "vm-smoke: invalid UI pack name beside artifact: $candidate_name" >&2
exit 2
fi
done
if [ "$SMOKE_VARIANT" = "ui" ]; then
if [ "$UI_PACK_COUNT" -ne 1 ] || [ ! -s "$UI_PACK_SOURCE" ]; then
echo "vm-smoke: ui fixture requires exactly one content-addressed UI pack" >&2
exit 2
fi
UI_PACK_NAME="$(basename "$UI_PACK_SOURCE")"
elif [ "$UI_PACK_COUNT" -ne 0 ]; then
echo "vm-smoke: standard fixture must not contain a UI asset pack" >&2
exit 2
fi
PROFILE_ROOT="$(cygpath -u "$USERPROFILE")"
SMOKE_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-vm-smoke.XXXXXX")"
FIXTURE_DIR="$SMOKE_DIR/artifacts"
@@ -178,45 +146,26 @@ MSYS2_ARG_CONV_EXCL='*' powershell.exe -NoProfile -ExecutionPolicy Bypass \
-SmokeRoot "$(cygpath -w "$SMOKE_DIR")"
cp "$BINARY_SRC" "$SMOKE_DIR/codebase-memory-mcp.exe"
cp "$SMOKE_DIR/codebase-memory-mcp.exe" "$FIXTURE_DIR/"
if [ -n "$UI_PACK_SOURCE" ]; then
cp "$UI_PACK_SOURCE" "$SMOKE_DIR/$UI_PACK_NAME"
cp "$UI_PACK_SOURCE" "$FIXTURE_DIR/$UI_PACK_NAME"
fi
# The integration asset must sit next to the smoke binary in $SMOKE_DIR: phases
# that invoke it directly (Phase 14 stages it by hand and drives an uninstall)
# resolve the asset next to the binary, and without it those render-template
# operations fail closed. Its own copy lands in the served archive below.
if [ -n "$ARTIFACT_DIR" ]; then
ASSET_SRC="$ARTIFACT_DIR/cbm-integrations.json"
else
ASSET_SRC="$(dirname "$BINARY_SRC")/cbm-integrations.json"
fi
cp "$ASSET_SRC" "$SMOKE_DIR/cbm-integrations.json"
# 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.
if [ -n "$ARTIFACT_DIR" ]; then
cp "$ARTIFACT_DIR/cbm-integrations.json" "$ARTIFACT_DIR/LICENSE" \
cp "$ARTIFACT_DIR/LICENSE" \
"$ARTIFACT_DIR/install.ps1" "$ARTIFACT_DIR/THIRD_PARTY_NOTICES.md" "$FIXTURE_DIR/"
else
# build.sh stages cbm-integrations.json next to the binary; ship it in the
# archive so the installed binary can verify and render its templates.
cp "$(dirname "$BINARY_SRC")/cbm-integrations.json" LICENSE install.ps1 "$FIXTURE_DIR/"
cp LICENSE install.ps1 "$FIXTURE_DIR/"
scripts/gen-third-party-notices.sh "$FIXTURE_DIR/THIRD_PARTY_NOTICES.md"
fi
# Member set + ORDER mirror scripts/package-release.sh (Windows): the install
# verifies cbm-integrations.json against the binary's embedded SHA-256 and fails
# closed without it.
# Member set + ORDER mirror scripts/package-release.sh (Windows).
EXPECTED_ARTIFACT="codebase-memory-mcp${SUFFIX}-windows-${SMOKE_ARCH}.zip"
(
cd "$FIXTURE_DIR"
ARCHIVE_MEMBERS=(
codebase-memory-mcp.exe cbm-integrations.json LICENSE install.ps1
THIRD_PARTY_NOTICES.md
)
[ -n "$UI_PACK_NAME" ] && ARCHIVE_MEMBERS+=("$UI_PACK_NAME")
zip -q "$EXPECTED_ARTIFACT" "${ARCHIVE_MEMBERS[@]}"
zip -q "$EXPECTED_ARTIFACT" \
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
sha256sum *.zip > checksums.txt
)
+23 -1075
View File
File diff suppressed because it is too large Load Diff
-2
View File
@@ -284,13 +284,11 @@ TEST(daemon_bootstrap_classifies_stateless_commands_without_client) {
char *install[] = {"codebase-memory-mcp", "install", "--dry-run", NULL};
char *uninstall[] = {"codebase-memory-mcp", "uninstall", NULL};
char *update[] = {"codebase-memory-mcp", "update", "-n", NULL};
char *verify_runtime_assets[] = {"codebase-memory-mcp", "--verify-runtime-assets", NULL};
ASSERT_EQ(classify(2, version), CBM_DAEMON_PROCESS_STATELESS);
ASSERT_EQ(classify(3, help), CBM_DAEMON_PROCESS_STATELESS);
ASSERT_EQ(classify(3, install), CBM_DAEMON_PROCESS_STATELESS);
ASSERT_EQ(classify(2, uninstall), CBM_DAEMON_PROCESS_STATELESS);
ASSERT_EQ(classify(3, update), CBM_DAEMON_PROCESS_STATELESS);
ASSERT_EQ(classify(2, verify_runtime_assets), CBM_DAEMON_PROCESS_STATELESS);
ASSERT_FALSE(cbm_daemon_process_role_requires_client(CBM_DAEMON_PROCESS_STATELESS));
PASS();
}
+3 -45
View File
@@ -1,9 +1,8 @@
r"""Product guard for ``daemon start --open`` UI readiness.
The daemon intentionally warms and verifies its external UI pack after the
control socket is already available. ``--open`` is the one synchronous user
request in that flow: it must wait for the *CBM HTTP endpoint*, not merely for
the daemon process, before it reports or opens the URL.
The control socket becomes available before the HTTP listener does. ``--open``
is the one synchronous user request in that flow: it must wait for the *CBM HTTP
endpoint*, not merely for the daemon process, before it reports or opens the URL.
This guard uses a real UI build and isolated daemon generations:
@@ -11,8 +10,6 @@ This guard uses a real UI build and isolated daemon generations:
the verified UI listener becomes available;
* a foreign service returning the exact formerly accepted HTML markers proves
public page text is not treated as daemon identity;
* a copied binary without its content-addressed pack proves missing/invalid
runtime assets never result in an opened or reported UI URL.
* a second ``daemon start --open`` proves the already-active daemon path obtains
the same generation-bound proof over a fresh authenticated IPC client.
@@ -330,38 +327,6 @@ def assert_active_daemon_open(binary, work):
stop_daemon(binary, env, daemon_pid)
def assert_missing_pack_failure(binary, work):
isolated = os.path.join(work, "without-pack")
os.makedirs(isolated, exist_ok=True)
copied_binary = os.path.join(isolated, os.path.basename(binary))
shutil.copy2(binary, copied_binary)
cache = os.path.join(work, "cache-missing-pack")
marker = os.path.join(work, "browser-missing-pack.txt")
probe, port = occupied_loopback_port()
probe.close() # reserve an unused port number without keeping it occupied
process, env = launch_start(copied_binary, work, cache, marker, port, 900)
daemon_pid = 0
try:
result, text = collect(process, timeout=10)
daemon_pid = pid_from(text)
url = "http://127.0.0.1:%d" % port
if (result.returncode == 0 or os.path.exists(marker) or url in text or
"UI endpoint did not become ready" not in text or
"matching runtime assets" not in text):
print("RED: a UI binary without its verified pack must fail --open without "
"reporting/opening a URL:\n%s" % text[:700])
return False
print("PASS: missing verified UI assets fail --open without opening/reporting a URL")
return True
finally:
if process.poll() is None:
process.kill()
_, text = collect(process)
daemon_pid = daemon_pid or pid_from(text)
stop_daemon(copied_binary, env, daemon_pid)
def main():
if len(sys.argv) != 2:
print("usage: python3 test_daemon_open_readiness.py <ui-binary>")
@@ -370,11 +335,6 @@ def main():
if not os.path.isfile(binary):
print("SETUP FAIL: binary not found: %s" % binary)
return 2
binary_dir = os.path.dirname(binary)
if not any(name.startswith("cbm-ui-") and name.endswith(".pack")
for name in os.listdir(binary_dir)):
print("SETUP FAIL: no cbm-ui-*.pack adjacent to UI binary: %s" % binary_dir)
return 2
if os.name == "nt":
with open(binary, "rb") as handle:
if OPEN_MARKER_ENV.encode("ascii") not in handle.read():
@@ -392,8 +352,6 @@ def main():
return 1
if not assert_active_daemon_open(binary, work):
return 1
if not assert_missing_pack_failure(binary, work):
return 1
print("\nGREEN: daemon --open is bound to verified UI endpoint readiness.")
return 0
-212
View File
@@ -1,212 +0,0 @@
#!/usr/bin/env bash
# Contract: the native product must not embed integration or frontend bytes.
#
# The binary used to carry nine complete shebang'd shell scripts, their
# PowerShell/.cmd twins and the node:child_process client modules as C string
# literals — written to client config dirs at 0755. A block of script-shaped
# bytes inside a native executable is unnecessary mixed-content with plausible
# static-classifier overlap. That is a risk-reduction rationale, not feature
# attribution for an opaque vendor verdict. Those bodies now live in
# assets/cbm-integrations.json (hash-verified; the binary embeds only the
# SHA-256), and this contract is the proof the removal STAYS removed: one
# revived string literal would silently hand the next release its
# malware-shaped surface back, and nothing else in CI would notice.
#
# Two independent legs:
# 1. SOURCE: no production C source under src/ may contain a shebang or a
# node:child_process literal. Absence at source level makes absence a
# BUILD property of every artifact (standard, ui, all platforms) — not a
# reachability assumption about any one binary.
# 2. ARTIFACT: when build/c/codebase-memory-mcp exists, its bytes are scanned
# directly. scripts/ci/check-binary-composition.sh runs the script needles
# on every release variant (A6 and A7), so a
# missing local build here skips only a redundant leg, never the property.
#
# Comment stripping is mandatory, not tidiness: a contract that a COMMENT can
# satisfy (or trip) is a false guard — see test_spawn_no_window_contract.sh,
# whose first version passed with its fix reverted because prose matched.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
python3 - "$ROOT" <<'PY'
import pathlib
import sys
root = pathlib.Path(sys.argv[1])
def strip_comments(text):
"""Blank out C comments, preserving line count and column positions."""
out = []
i, n = 0, len(text)
while i < n:
two = text[i:i + 2]
if two == "/*":
j = text.find("*/", i + 2)
j = n if j < 0 else j + 2
out.append("".join(c if c == "\n" else " " for c in text[i:j]))
i = j
elif two == "//":
j = text.find("\n", i)
j = n if j < 0 else j
out.append(" " * (j - i))
i = j
else:
out.append(text[i])
i += 1
return "".join(out)
# A file may opt out only with a justified, at-the-case entry (O10): the
# exact repo-relative path plus WHY. Empty on purpose — the templates live in
# assets/cbm-integrations.json, and no production source needs script text.
ALLOWLIST = {
# "src/foo/bar.c": "why this file legitimately embeds script text",
}
# "#!" alone covers every shebang, including one assembled from adjacent
# string literals ("#!" "/bin/sh"); node:child_process covers the JS bridge.
SOURCE_NEEDLES = ["#!", "node:child_process"]
# Every FIRST-PARTY tree linked into the product binary. Vendored trees are
# deliberately out of scope (pinned upstream code we do not author; the
# artifact leg and the release gate still cover whatever they contribute).
SOURCE_GLOBS = [
"src/**/*.c",
"internal/cbm/*.c",
"internal/cbm/lsp/*.c",
"internal/cbm/lsp/generated/*.c",
]
failures = []
scanned = 0
sources = sorted(p for g in SOURCE_GLOBS for p in root.glob(g))
for path in sources:
rel = path.relative_to(root).as_posix()
scanned += 1
if rel in ALLOWLIST:
continue
text = strip_comments(path.read_text(encoding="utf-8", errors="replace"))
for needle in SOURCE_NEEDLES:
for idx, line in enumerate(text.splitlines(), start=1):
if needle in line:
failures.append(f"{rel}:{idx} contains '{needle}': {line.strip()[:80]}")
# Anti-vacuity: the files that carried the embedded bodies must still be in
# scope — if they move, this contract must move with them, loudly.
for must_scan in ("src/cli/cli.c", "src/cli/client_adapter.c"):
if not (root / must_scan).is_file():
failures.append(f"{must_scan} is gone — re-point this contract at the "
"file that now owns integration installs")
# The release gate must enforce UI script absence unconditionally and preserve
# the native-vs-pack byte boundary. A debug environment escape hatch here would
# make the source assertion true while shipped UI binaries quietly bypass it.
composition = (root / "scripts/ci/check-binary-composition.sh").read_text(
encoding="utf-8", errors="replace"
)
if "CBM_CHECK_UI_SCRIPTS_ABSENT" in composition:
failures.append("binary composition gate still permits UI script-scan bypass")
for required in ("A6-no-embedded-scripts", "A7-no-ui-asset-bytes", "CBMUIPK",
"<!doctype html>", '<script type="module" crossorigin'):
if required not in composition:
failures.append(f"binary composition gate is missing {required!r}")
a7 = composition.split("# A7 —", 1)
if len(a7) != 2:
failures.append("binary composition gate has no A7 implementation")
else:
a7_body = a7[1].split("\n}", 1)[0]
if 'if [ "$is_ui"' in a7_body:
failures.append("A7 raw frontend-byte check is conditional on UI path classification")
if 'for needle in "${UI_ASSET_BYTE_NEEDLES[@]}"' not in a7_body:
failures.append("A7 does not scan every native artifact for every frontend-byte needle")
# Filename-selected abort/busy-loop injectors are useful only in deterministic
# supervisor tests. They must be compiled out of ordinary production builds,
# while the broad source-matrix smoke explicitly opts into them so coverage is
# preserved rather than silently deleted.
cbm_source = (root / "internal/cbm/cbm.c").read_text(
encoding="utf-8", errors="replace"
)
if "#ifdef CBM_ENABLE_TEST_SEAMS\n/* Deterministic supervisor fault injection" not in cbm_source:
failures.append("internal crash/hang fault injector is not behind CBM_ENABLE_TEST_SEAMS")
if "#ifdef CBM_ENABLE_TEST_SEAMS\n cbm_test_fault_inject(rel_path);\n#endif" not in cbm_source:
failures.append("fault-injector call site is not behind CBM_ENABLE_TEST_SEAMS")
for needle in ("CBM_TEST_CRASH_ON", "CBM_TEST_HANG_ON"):
if needle not in composition.split("SEAM_NEEDLES=(", 1)[-1].split(")", 1)[0]:
failures.append(f"binary composition gate does not forbid {needle}")
smoke_workflow = (root / ".github/workflows/smoke.yml").read_text(
encoding="utf-8", errors="replace"
)
if smoke_workflow.count("TEST_SEAMS=1") != 3:
failures.append("wide smoke matrix must opt into test seams on all three build jobs")
# Standard production must not acquire the parser's pack/file-I/O surface just
# because tests need it. One stub substitution keeps each link topology exact:
# standard = no-I/O stub; UI = real parser + generated manifest; test/repro/TSan
# = real parser + empty manifest. Linking either manifest twice is an error.
makefile = (root / "Makefile.cbm").read_text(encoding="utf-8", errors="replace")
for required in (
"src/ui/asset_pack_stub.c",
"TEST_PROD_SRCS = $(subst src/ui/asset_pack_stub.c,src/ui/asset_pack.c "
"src/ui/asset_manifest_stub.c,$(PROD_SRCS))",
"PROD_SRCS_WITH_ASSETS = $(subst src/ui/asset_pack_stub.c,src/ui/asset_pack.c "
"$(UI_ASSET_MANIFEST),$(PROD_SRCS))",
):
if required not in makefile:
failures.append(f"Makefile UI link topology is missing {required!r}")
ui_sources = makefile.split("UI_SRCS =", 1)[-1].split("# mimalloc", 1)[0]
for forbidden in ("src/ui/asset_pack.c", "src/ui/asset_manifest_stub.c"):
if forbidden in ui_sources:
failures.append(f"standard UI_SRCS still links {forbidden}")
for target in ("test-runner", "test-repro-runner", "test-runner-tsan"):
rule = makefile.split(f"$(BUILD_DIR)/{target}:", 1)
if len(rule) != 2 or "$(TEST_PROD_SRCS)" not in rule[1].split("\n\n", 1)[0]:
failures.append(f"{target} does not link the full parser test source set")
if scanned == 0:
print("FAIL: scanned no production sources — this contract checks nothing")
sys.exit(1)
if failures:
print(f"FAIL: {len(failures)} native-image byte-boundary violation(s):\n")
for f in failures:
print(f" - {f}")
print("\nIntegration bodies belong in assets/cbm-integrations.json (verified by "
"the embedded SHA-256), and frontend bytes belong in the verified UI pack, "
"never in the binary. A justified exception needs "
"an ALLOWLIST entry in this file.")
sys.exit(1)
print(f"OK: {scanned} production source file(s) free of script literals")
PY
# ── Artifact leg ─────────────────────────────────────────────────────
BINARY="build/c/codebase-memory-mcp"
if [ ! -f "$BINARY" ]; then
# Not a silent green: name exactly why this leg may skip (O10). The source
# leg above already proved the build property, and the release gate
# (check-binary-composition.sh A6) scans every shipped artifact.
echo "skip: $BINARY not built here; artifact leg runs in the release gate (A6)"
exit 0
fi
fail=0
for needle in '#!/usr/bin/env bash' '#!/bin/bash' '#!/bin/sh' 'node:child_process' \
'CBMUIPK' '<!doctype html>' '<html lang="en" class="dark">' \
'<script type="module" crossorigin'; do
if LC_ALL=C grep -a -q -F -e "$needle" "$BINARY"; then
echo "FAIL: built binary contains '$needle' — sidecar bytes are embedded again"
fail=1
fi
done
# Anti-vacuity canary, same as the release gate: proves the scan reads a real
# artifact rather than a stub/truncated file where absence is meaningless.
if ! LC_ALL=C grep -a -q -F -e 'codebase-memory-mcp' "$BINARY"; then
echo "FAIL: canary string missing — $BINARY is not a readable product binary"
fail=1
fi
[ "$fail" -eq 0 ] && echo "OK: built binary carries no integration scripts or raw UI-pack bytes"
exit "$fail"
@@ -1,517 +0,0 @@
#!/usr/bin/env bash
# Runtime-sidecar contract for release archives, package managers and source
# installers. Release packaging executes the final staged binary against the
# exact adjacent files it will archive; downstream packages must retain those
# files either beside the executable or in ../share/codebase-memory-mcp.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
python3 - "$ROOT" "$BASH" <<'PY'
from __future__ import annotations
import json
import hashlib
import os
import pathlib
import platform
import re
import shlex
import struct
import subprocess
import sys
import tarfile
import tempfile
import zipfile
root = pathlib.Path(sys.argv[1])
bash_executable = pathlib.Path(sys.argv[2])
failures: list[str] = []
def read(relative: str) -> str:
return (root / relative).read_text(encoding="utf-8")
def require(condition: bool, message: str) -> None:
if not condition:
failures.append(message)
package_release = read("scripts/package-release.sh")
test_workflow = read(".github/workflows/_test.yml")
strip_position = package_release.find('strip_release_binary "$STAGED_BINARY"')
probe_position = package_release.find('"$STAGED_BINARY" --verify-runtime-assets')
tar_position = package_release.find('tar -czf "$OUT_DIR/$NAME.tar.gz"')
zip_position = package_release.find('zip -q "$OUT_DIR/$NAME.zip"')
require(
'if ! "$STAGED_BINARY" --verify-runtime-assets; then' in package_release,
"release packaging must execute the final staged binary's runtime-asset probe",
)
require(
min(strip_position, probe_position, tar_position, zip_position) >= 0
and strip_position < probe_position < tar_position
and probe_position < zip_position,
"Unix release packaging must probe after final binary transformation and before archive",
)
require(
re.search(
r'tar\s+-czf\s+"\$OUT_DIR/\$NAME\.tar\.gz"\s+-C\s+"\$PACK_DIR"',
package_release,
)
is not None,
"Unix release archive must be created from the same private directory that was probed",
)
require(
'cd "$PACK_DIR"' in package_release
and 'zip -q "$OUT_DIR/$NAME.zip" "${ARCHIVE_MEMBERS[@]}"' in package_release,
"Windows release archive must be created from the same private directory that was probed",
)
windows_test_job = re.search(
r"(?ms)^ test-windows:\n(?P<body>.*?)(?=^ [a-zA-Z0-9_-]+:\n)",
test_workflow,
)
require(
windows_test_job is not None
and re.search(r"(?m)^\s+zip\s*$", windows_test_job.group("body")) is not None,
"Windows test jobs that exercise package-release.sh must install zip",
)
homebrew = read("pkg/homebrew/Formula/codebase-memory-mcp.rb")
require(
re.search(
r'\(share\s*/\s*["\']codebase-memory-mcp["\']\)\.install\s+'
r'["\']cbm-integrations\.json["\']',
homebrew,
)
is not None,
"Homebrew must install cbm-integrations.json in ../share/codebase-memory-mcp",
)
require(
re.search(
r'assert_path_exists\s+share\s*/\s*["\']codebase-memory-mcp/'
r'cbm-integrations\.json["\']',
homebrew,
)
is not None,
"Homebrew formula test must assert the installed integration asset",
)
require(
"codebase-memory-mcp install" in homebrew,
"Homebrew must retain the opt-in agent-configuration caveat",
)
aur = read("pkg/aur/PKGBUILD")
require(
re.search(
r'install\s+-Dm644\s+["\']\$\{srcdir\}/cbm-integrations\.json["\']'
r'\s*\\?\s*["\']\$\{pkgdir\}/usr/share/codebase-memory-mcp/'
r'cbm-integrations\.json["\']',
aur,
)
is not None,
"AUR must install cbm-integrations.json in /usr/share/codebase-memory-mcp",
)
scoop = json.loads(read("pkg/scoop/codebase-memory-mcp.json"))
scoop_post_install = scoop.get("post_install", [])
if isinstance(scoop_post_install, str):
scoop_post_install = [scoop_post_install]
require(
not any(re.search(r'\binstall(?:\s|$)', command, re.IGNORECASE)
for command in scoop_post_install),
"Scoop must not invoke the full native install command after extracting the package",
)
require(
scoop["architecture"]["64bit"].get("bin") == "codebase-memory-mcp.exe",
"Scoop shim must target the executable inside the extracted package directory",
)
choco_install = read("pkg/chocolatey/tools/chocolateyInstall.ps1")
choco_uninstall = read("pkg/chocolatey/tools/chocolateyUninstall.ps1")
require(
re.search(r'&\s*\$binPath\s+install\b', choco_install, re.IGNORECASE) is None,
"Chocolatey must not invoke the full native install command after extraction",
)
require(
re.search(r'-UnzipLocation\s+\$installDir\b', choco_install) is not None
and re.search(
r'\$binPath\s*=\s*Join-Path\s+\$installDir\s+'
r'["\']codebase-memory-mcp\.exe["\']',
choco_install,
re.IGNORECASE,
)
is not None,
"Chocolatey must shim the executable in the whole extracted package directory",
)
require(
re.search(r'Remove-Item\s+\$installDir\s+-Recurse\s+-Force', choco_uninstall) is not None,
"Chocolatey uninstall must remove the package-owned extraction directory",
)
setup_sh = read("scripts/setup.sh")
asset_helper = setup_sh.split("install_integration_asset() {", 1)[-1].split(
"download_binary() {", 1
)[0]
download_body = setup_sh.split("download_binary() {", 1)[-1].split(
"# --- Build from source ---", 1
)[0]
source_body = setup_sh.split("build_from_source() {", 1)[-1].split(
"# --- MCP auto-configuration ---", 1
)[0]
require(
re.search(
r'cp\s+["\']\$source_path["\']\s+'
r'["\']\$\{INSTALL_DIR\}/cbm-integrations\.json["\']',
asset_helper,
)
is not None,
"Unix integration-asset helper must copy the JSON beside the binary",
)
require(
'install_integration_asset "$tmpdir"' in download_body,
"Unix release download must install cbm-integrations.json beside the binary",
)
require(
'install_integration_asset "${SOURCE_DIR}/build/c"' in source_body,
"Unix source build must install cbm-integrations.json beside the binary",
)
setup_windows = read("scripts/setup-windows.ps1")
windows_source = setup_windows.split("if ($FromSource) {", 1)[-1].split(
"} else {", 1
)[0]
windows_download = setup_windows.split("} else {", 1)[-1]
require(
re.search(
r'cp\s+build/c/codebase-memory-mcp\s+build/c/cbm-integrations\.json\s+'
r'/home/\$wslUser/\.local/bin/',
windows_source,
)
is not None,
"Windows source build must copy cbm-integrations.json beside the WSL binary",
)
require(
re.search(
r'Expand-Archive\s+-Path\s+\$tmpZip\s+-DestinationPath\s+\$InstallDir\s+-Force',
windows_download,
re.IGNORECASE,
)
is not None,
"Windows release download must retain the complete archive in the install directory",
)
glama = read("pkg/glama/Dockerfile")
require(
re.search(r'tar\s+-xz\s+-C\s+/tmp[^\n]*cbm-integrations\.json', glama) is not None,
"Glama package extraction must retain cbm-integrations.json",
)
require(
re.search(
r'COPY\s+--from=fetch\s+/tmp/cbm-integrations\.json\s+'
r'/usr/local/share/codebase-memory-mcp/cbm-integrations\.json',
glama,
)
is not None,
"Glama must install cbm-integrations.json at the executable-relative share path",
)
# Exercise the packager itself with a tiny native executable that implements
# the same hidden command-line contract as the product. This is deliberately a
# native fixture (rather than a shell script): package-release still has to run
# its real strip and binary-composition gates before it reaches the association
# probe. The two negative cases distinguish self-integrity from association:
# the alternate UI pack is structurally valid and named by its own SHA-256, but
# it is not the pack the staged binary declares; likewise, the repository JSON
# is a valid manifest but is not the manifest expected by the mismatch fixture.
fixture_source_template = r'''
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define EXPECT_INTEGRATION __EXPECT_INTEGRATION__
#define EXPECT_UI_NAME __EXPECT_UI_NAME__
#define EXPECT_UI_MARKER __EXPECT_UI_MARKER__
static int file_contains(const char *path, const char *needle) {
FILE *stream = fopen(path, "rb");
if (!stream) return 0;
const size_t needle_length = strlen(needle);
size_t matched = 0;
int byte = 0;
while (needle_length > 0 && (byte = fgetc(stream)) != EOF) {
if ((unsigned char)byte == (unsigned char)needle[matched]) {
matched++;
if (matched == needle_length) {
fclose(stream);
return 1;
}
} else {
matched = ((unsigned char)byte == (unsigned char)needle[0]) ? 1U : 0U;
}
}
fclose(stream);
return 0;
}
static int adjacent_path(const char *argv0, const char *leaf, char *out, size_t out_size) {
char directory[4096];
const size_t length = strlen(argv0);
if (length == 0 || length >= sizeof(directory)) return 0;
memcpy(directory, argv0, length + 1U);
char *slash = strrchr(directory, '/');
char *backslash = strrchr(directory, '\\');
if (backslash && (!slash || backslash > slash)) slash = backslash;
if (!slash) return 0;
*slash = '\0';
const int written = snprintf(out, out_size, "%s/%s", directory, leaf);
return written > 0 && (size_t)written < out_size;
}
int main(int argc, char **argv) {
static const char product[] = "codebase-memory-mcp";
char path[4096];
if (argc != 2 || strcmp(argv[1], "--verify-runtime-assets") != 0) {
fprintf(stderr, "%s fixture accepts only the runtime-asset probe\n", product);
return 64;
}
if (!adjacent_path(argv[0], "cbm-integrations.json", path, sizeof(path)) ||
!file_contains(path, EXPECT_INTEGRATION)) {
fprintf(stderr, "%s fixture rejected integration assets\n", product);
return 65;
}
if (EXPECT_UI_NAME[0] != '\0' &&
(!adjacent_path(argv[0], EXPECT_UI_NAME, path, sizeof(path)) ||
!file_contains(path, EXPECT_UI_MARKER))) {
fprintf(stderr, "%s fixture rejected UI assets\n", product);
return 66;
}
return 0;
}
'''
def valid_ui_pack(marker: str) -> bytes:
path = b"/index.html"
payload = ("<title>" + marker + "</title>").encode("ascii")
header = bytearray(80)
header[:8] = b"CBMUIPK\0"
struct.pack_into("<HHIII", header, 8, 1, 80, 0, 1, 24)
paths_offset = 80 + 24
payload_offset = paths_offset + len(path)
total = payload_offset + len(payload)
struct.pack_into(
"<QQQQQQQ",
header,
24,
80,
24,
paths_offset,
len(path),
payload_offset,
len(payload),
total,
)
entry = struct.pack("<IHBBQQ", 0, len(path), 1, 1, 0, len(payload))
return bytes(header) + entry + path + payload
host_system = platform.system().lower()
host_is_windows = (
os.name == "nt"
or bool(os.environ.get("MSYSTEM"))
or host_system.startswith(("windows", "msys", "mingw", "cygwin"))
)
host_goos = "windows" if host_is_windows else (
"darwin" if host_system == "darwin" else "linux"
)
binary_name = "codebase-memory-mcp.exe" if host_goos == "windows" else "codebase-memory-mcp"
archive_suffix = ".zip" if host_goos == "windows" else ".tar.gz"
compiler = shlex.split(os.environ.get("CC", "cc"))
arch_flags = shlex.split(os.environ.get("ARCHFLAGS", ""))
integration_marker = '\"format\":1,\"product\":\"codebase-memory-mcp\"'
pack_a = valid_ui_pack("fixture-pack-a")
pack_b = valid_ui_pack("fixture-pack-b")
pack_a_digest = hashlib.sha256(pack_a).hexdigest()
pack_b_digest = hashlib.sha256(pack_b).hexdigest()
pack_a_name = f"cbm-ui-{pack_a_digest}.pack"
pack_b_name = f"cbm-ui-{pack_b_digest}.pack"
def build_fixture(
directory: pathlib.Path,
expected_integration: str,
expected_ui_name: str = "",
expected_ui_marker: str = "",
) -> None:
source = fixture_source_template
source = source.replace("__EXPECT_INTEGRATION__", json.dumps(expected_integration))
source = source.replace("__EXPECT_UI_NAME__", json.dumps(expected_ui_name))
source = source.replace("__EXPECT_UI_MARKER__", json.dumps(expected_ui_marker))
source_path = directory / "runtime_asset_fixture.c"
source_path.write_text(source, encoding="utf-8")
command = compiler + arch_flags + ["-std=c11", "-O2", str(source_path)]
if host_goos == "linux":
command.append("-Wl,-z,noexecstack")
command += ["-o", str(directory / binary_name)]
compiled = subprocess.run(
command,
cwd=root,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
check=False,
)
require(
compiled.returncode == 0,
"could not compile native release-packaging fixture: " + compiled.stdout.strip(),
)
def package_fixture(
work: pathlib.Path,
case: str,
variant: str,
expected_integration: str,
pack_name: str = "",
pack_bytes: bytes = b"",
expected_ui_name: str = "",
expected_ui_marker: str = "",
) -> tuple[subprocess.CompletedProcess[str], pathlib.Path]:
build_dir = work / f"{case}-build"
output_dir = work / f"{case}-output"
build_dir.mkdir()
output_dir.mkdir()
build_fixture(build_dir, expected_integration, expected_ui_name, expected_ui_marker)
if pack_name:
(build_dir / pack_name).write_bytes(pack_bytes)
environment = os.environ.copy()
environment["BUILD_DIR"] = str(build_dir)
archive_name = f"codebase-memory-mcp{'-ui' if variant == 'ui' else ''}-{host_goos}-fixture{archive_suffix}"
packaged = subprocess.run(
[
str(bash_executable),
str(root / "scripts/package-release.sh"),
host_goos,
"fixture",
"--variant",
variant,
"--out-dir",
str(output_dir),
],
cwd=root,
env=environment,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
check=False,
)
return packaged, output_dir / archive_name
if compiler:
with tempfile.TemporaryDirectory(prefix="cbm-package-association-") as temp:
work = pathlib.Path(temp)
standard, standard_archive = package_fixture(
work, "standard-ok", "standard", integration_marker
)
require(
standard.returncode == 0 and standard_archive.is_file(),
"release packager rejected a matching standard fixture: " + standard.stdout.strip(),
)
if standard_archive.is_file():
if host_goos == "windows":
with zipfile.ZipFile(standard_archive) as archive:
standard_members = set(archive.namelist())
else:
with tarfile.open(standard_archive, "r:gz") as archive:
standard_members = set(archive.getnames())
require(
standard_members
== {
binary_name,
"cbm-integrations.json",
"LICENSE",
"install.ps1" if host_goos == "windows" else "install.sh",
"THIRD_PARTY_NOTICES.md",
},
"standard archive must contain exactly the five verified/staged members "
f"(got {sorted(standard_members)!r})",
)
ui, ui_archive = package_fixture(
work,
"ui-ok",
"ui",
integration_marker,
pack_a_name,
pack_a,
pack_a_name,
"fixture-pack-a",
)
require(
ui.returncode == 0 and ui_archive.is_file(),
"release packager rejected a matching UI fixture: " + ui.stdout.strip(),
)
if ui_archive.is_file():
if host_goos == "windows":
with zipfile.ZipFile(ui_archive) as archive:
ui_members = set(archive.namelist())
else:
with tarfile.open(ui_archive, "r:gz") as archive:
ui_members = set(archive.getnames())
require(
ui_members
== {
binary_name,
"cbm-integrations.json",
"LICENSE",
"install.ps1" if host_goos == "windows" else "install.sh",
"THIRD_PARTY_NOTICES.md",
pack_a_name,
},
"UI archive must contain exactly the five base members plus its verified pack "
f"(got {sorted(ui_members)!r})",
)
wrong_pack, wrong_pack_archive = package_fixture(
work,
"ui-wrong-pack",
"ui",
integration_marker,
pack_b_name,
pack_b,
pack_a_name,
"fixture-pack-a",
)
require(
wrong_pack.returncode != 0
and "staged binary rejected its adjacent runtime assets" in wrong_pack.stdout
and not wrong_pack_archive.exists(),
"release packager accepted a structurally valid, self-hashed UI pack for a different binary",
)
wrong_manifest, wrong_manifest_archive = package_fixture(
work,
"standard-wrong-manifest",
"standard",
"manifest-association-that-is-not-in-the-repository-asset",
)
require(
wrong_manifest.returncode != 0
and "staged binary rejected its adjacent runtime assets" in wrong_manifest.stdout
and not wrong_manifest_archive.exists(),
"release packager accepted a valid integration manifest that did not match the binary",
)
if failures:
print("FAIL: package runtime-asset contract", file=sys.stderr)
for failure in failures:
print(f" - {failure}", file=sys.stderr)
raise SystemExit(1)
print("PASS: release and package-manager runtime assets stay bound to their executable")
PY
+16 -551
View File
@@ -1,15 +1,14 @@
/*
* test_ui.c — Tests for the graph visualization UI module.
*
* Covers: config persistence, external asset-pack validation, layout engine.
* Covers: config persistence, embedded asset lookup, layout engine.
*/
#include "../src/foundation/compat.h"
#include "../src/foundation/compat_fs.h"
#include "test_framework.h"
#include "test_helpers.h"
#include "foundation/sha256.h"
#include "ui/asset_pack.h"
#include "ui/config.h"
#include "ui/embedded_assets.h"
#include "ui/layout3d.h"
#include "store/store.h"
#ifdef _WIN32
@@ -125,9 +124,9 @@ TEST(config_save_atomically_replaces_a_complete_generation) {
char old_bytes[512] = {0};
#ifdef _WIN32
DWORD old_length = 0;
bool old_read =
ReadFile(old_handle, old_bytes, (DWORD)sizeof(old_bytes) - 1U, &old_length, NULL) != 0 &&
old_length > 0;
bool old_read = ReadFile(old_handle, old_bytes, (DWORD)sizeof(old_bytes) - 1U, &old_length,
NULL) != 0 &&
old_length > 0;
bool old_closed = CloseHandle(old_handle) != 0;
#else
size_t old_length = fread(old_bytes, 1, sizeof(old_bytes) - 1, old_handle);
@@ -260,545 +259,18 @@ TEST(config_missing_fields) {
PASS();
}
/* ── External UI asset-pack tests ────────────────────────────── */
/* ── Embedded asset tests ─────────────────────────────────────── */
enum {
TEST_UI_PACK_HEADER_BYTES = 80,
TEST_UI_PACK_ENTRY_BYTES = 24,
};
static void ui_test_write_u16(unsigned char *bytes, uint16_t value) {
bytes[0] = (unsigned char)value;
bytes[1] = (unsigned char)(value >> 8U);
}
static void ui_test_write_u32(unsigned char *bytes, uint32_t value) {
for (size_t index = 0U; index < sizeof(value); index++) {
bytes[index] = (unsigned char)(value >> (index * 8U));
}
}
static void ui_test_write_u64(unsigned char *bytes, uint64_t value) {
for (size_t index = 0U; index < sizeof(value); index++) {
bytes[index] = (unsigned char)(value >> (index * 8U));
}
}
static unsigned char *ui_test_pack(size_t *length_out, bool malformed_index) {
static const char asset_path[] = "/assets/app.js";
static const char index_path[] = "/index.html";
static const unsigned char asset_data[] = "console.log(1);";
static const unsigned char index_data[] = "<h1>CBM</h1>";
size_t paths_length = sizeof(asset_path) - 1U + sizeof(index_path) - 1U;
size_t payload_length = sizeof(asset_data) - 1U + sizeof(index_data) - 1U;
size_t index_length = TEST_UI_PACK_ENTRY_BYTES * 2U;
size_t paths_offset = TEST_UI_PACK_HEADER_BYTES + index_length;
size_t payload_offset = paths_offset + paths_length;
size_t total = payload_offset + payload_length;
unsigned char *pack = calloc(total, 1U);
if (!pack) {
return NULL;
}
pack[0] = 'C';
pack[1] = 'B';
pack[2] = 'M';
pack[3] = 'U';
pack[4] = 'I';
pack[5] = 'P';
pack[6] = 'K';
ui_test_write_u16(pack + 8U, 1U);
ui_test_write_u16(pack + 10U, TEST_UI_PACK_HEADER_BYTES);
ui_test_write_u32(pack + 16U, 2U);
ui_test_write_u32(pack + 20U, TEST_UI_PACK_ENTRY_BYTES);
ui_test_write_u64(pack + 24U, TEST_UI_PACK_HEADER_BYTES);
ui_test_write_u64(pack + 32U, index_length);
ui_test_write_u64(pack + 40U, paths_offset);
ui_test_write_u64(pack + 48U, paths_length);
ui_test_write_u64(pack + 56U, payload_offset);
ui_test_write_u64(pack + 64U, payload_length);
ui_test_write_u64(pack + 72U, total);
unsigned char *asset_entry = pack + TEST_UI_PACK_HEADER_BYTES;
ui_test_write_u32(asset_entry, malformed_index ? 1U : 0U);
ui_test_write_u16(asset_entry + 4U, sizeof(asset_path) - 1U);
asset_entry[6U] = 2U;
asset_entry[7U] = CBM_UI_ASSET_IMMUTABLE;
ui_test_write_u64(asset_entry + 8U, 0U);
ui_test_write_u64(asset_entry + 16U, sizeof(asset_data) - 1U);
unsigned char *index_entry = asset_entry + TEST_UI_PACK_ENTRY_BYTES;
ui_test_write_u32(index_entry, sizeof(asset_path) - 1U);
ui_test_write_u16(index_entry + 4U, sizeof(index_path) - 1U);
index_entry[6U] = 1U;
index_entry[7U] = CBM_UI_ASSET_REVALIDATE;
ui_test_write_u64(index_entry + 8U, sizeof(asset_data) - 1U);
ui_test_write_u64(index_entry + 16U, sizeof(index_data) - 1U);
memcpy(pack + paths_offset, asset_path, sizeof(asset_path) - 1U);
memcpy(pack + paths_offset + sizeof(asset_path) - 1U, index_path, sizeof(index_path) - 1U);
memcpy(pack + payload_offset, asset_data, sizeof(asset_data) - 1U);
memcpy(pack + payload_offset + sizeof(asset_data) - 1U, index_data, sizeof(index_data) - 1U);
*length_out = total;
return pack;
}
static bool ui_test_publish_pack(const char *directory, unsigned char *pack, size_t length,
char path_out[1024]) {
char hash[CBM_SHA256_HEX_LEN + 1U];
cbm_sha256_hex(pack, length, hash);
char name[128];
int name_length = snprintf(name, sizeof(name), "cbm-ui-%s.pack", hash);
int path_length = snprintf(path_out, 1024U, "%s/%s", directory, name);
if (name_length <= 0 || (size_t)name_length >= sizeof(name) || path_length <= 0 ||
path_length >= 1024) {
return false;
}
FILE *file = fopen(path_out, "wb");
bool written = file && fwrite(pack, 1U, length, file) == length;
if (file && fclose(file) != 0) {
written = false;
}
if (written) {
cbm_ui_assets_set_manifest_for_testing(name, hash, length);
}
return written;
}
static void ui_test_restore_assets_env(char *old_value) {
if (old_value) {
(void)cbm_setenv("CBM_UI_ASSETS_DIR", old_value, 1);
free(old_value);
} else {
(void)cbm_unsetenv("CBM_UI_ASSETS_DIR");
}
}
TEST(ui_asset_pack_validates_before_publication) {
char directory[] = "/tmp/cbm_ui_pack_XXXXXX";
ASSERT_NOT_NULL(cbm_mkdtemp(directory));
size_t length = 0U;
unsigned char *pack = ui_test_pack(&length, false);
ASSERT_NOT_NULL(pack);
char pack_path[1024];
cbm_ui_assets_reset_for_testing();
ASSERT_TRUE(ui_test_publish_pack(directory, pack, length, pack_path));
free(pack);
char *old_assets = getenv("CBM_UI_ASSETS_DIR") ? strdup(getenv("CBM_UI_ASSETS_DIR")) : NULL;
ASSERT_EQ(cbm_setenv("CBM_UI_ASSETS_DIR", directory, 1), 0);
char error[512];
ASSERT_TRUE(cbm_ui_assets_supported());
ASSERT_EQ(cbm_ui_assets_state(), CBM_UI_ASSETS_COLD);
ASSERT_TRUE(cbm_ui_assets_warm(NULL, error, sizeof(error)));
ASSERT_EQ(cbm_ui_assets_state(), CBM_UI_ASSETS_READY);
const cbm_ui_asset_t *asset = cbm_ui_asset_lookup("/assets/app.js");
ASSERT_NOT_NULL(asset);
ASSERT_EQ(asset->cache, CBM_UI_ASSET_IMMUTABLE);
ASSERT_STR_EQ(asset->content_type, "application/javascript; charset=utf-8");
ASSERT_EQ(asset->size, sizeof("console.log(1);") - 1U);
ASSERT_NULL(cbm_ui_asset_lookup("/api/processes"));
cbm_ui_assets_reset_for_testing();
ui_test_restore_assets_env(old_assets);
(void)cbm_unlink(pack_path);
(void)cbm_rmdir(directory);
TEST(embedded_lookup_not_found) {
/* With stub, everything should return NULL */
const cbm_embedded_file_t *f = cbm_embedded_lookup("/nonexistent");
ASSERT_NULL(f);
PASS();
}
TEST(ui_asset_pack_wrong_hash_fails_closed) {
char directory[] = "/tmp/cbm_ui_hash_XXXXXX";
ASSERT_NOT_NULL(cbm_mkdtemp(directory));
size_t length = 0U;
unsigned char *pack = ui_test_pack(&length, false);
ASSERT_NOT_NULL(pack);
char pack_path[1024];
cbm_ui_assets_reset_for_testing();
ASSERT_TRUE(ui_test_publish_pack(directory, pack, length, pack_path));
pack[length - 1U] ^= 1U;
FILE *file = fopen(pack_path, "wb");
ASSERT_NOT_NULL(file);
ASSERT_EQ(fwrite(pack, 1U, length, file), length);
ASSERT_EQ(fclose(file), 0);
free(pack);
char *old_assets = getenv("CBM_UI_ASSETS_DIR") ? strdup(getenv("CBM_UI_ASSETS_DIR")) : NULL;
ASSERT_EQ(cbm_setenv("CBM_UI_ASSETS_DIR", directory, 1), 0);
char error[512];
ASSERT_FALSE(cbm_ui_assets_warm(NULL, error, sizeof(error)));
ASSERT_EQ(cbm_ui_assets_state(), CBM_UI_ASSETS_FAILED);
ASSERT_NULL(cbm_ui_asset_lookup("/index.html"));
cbm_ui_assets_reset_for_testing();
ui_test_restore_assets_env(old_assets);
(void)cbm_unlink(pack_path);
(void)cbm_rmdir(directory);
PASS();
}
TEST(ui_asset_pack_structural_corruption_fails_after_valid_hash) {
char directory[] = "/tmp/cbm_ui_format_XXXXXX";
ASSERT_NOT_NULL(cbm_mkdtemp(directory));
size_t length = 0U;
unsigned char *pack = ui_test_pack(&length, true);
ASSERT_NOT_NULL(pack);
char pack_path[1024];
cbm_ui_assets_reset_for_testing();
ASSERT_TRUE(ui_test_publish_pack(directory, pack, length, pack_path));
free(pack);
char *old_assets = getenv("CBM_UI_ASSETS_DIR") ? strdup(getenv("CBM_UI_ASSETS_DIR")) : NULL;
ASSERT_EQ(cbm_setenv("CBM_UI_ASSETS_DIR", directory, 1), 0);
char error[512];
ASSERT_FALSE(cbm_ui_assets_warm(NULL, error, sizeof(error)));
ASSERT_EQ(cbm_ui_assets_state(), CBM_UI_ASSETS_FAILED);
ASSERT_NULL(cbm_ui_asset_lookup("/index.html"));
cbm_ui_assets_reset_for_testing();
ui_test_restore_assets_env(old_assets);
(void)cbm_unlink(pack_path);
(void)cbm_rmdir(directory);
PASS();
}
/* A daemon can be asked to stop immediately after creating the warmup thread,
* before that worker enters cbm_ui_assets_warm(). The cancellation must latch
* across that scheduling window instead of being cleared by worker startup. */
TEST(ui_asset_pack_prestart_cancellation_is_not_lost) {
char directory[] = "/tmp/cbm_ui_cancel_XXXXXX";
ASSERT_NOT_NULL(cbm_mkdtemp(directory));
size_t length = 0U;
unsigned char *pack = ui_test_pack(&length, false);
ASSERT_NOT_NULL(pack);
char pack_path[1024];
cbm_ui_assets_reset_for_testing();
ASSERT_TRUE(ui_test_publish_pack(directory, pack, length, pack_path));
free(pack);
char *old_assets = getenv("CBM_UI_ASSETS_DIR") ? strdup(getenv("CBM_UI_ASSETS_DIR")) : NULL;
ASSERT_EQ(cbm_setenv("CBM_UI_ASSETS_DIR", directory, 1), 0);
cbm_ui_assets_request_cancel();
char error[512];
bool warmed = cbm_ui_assets_warm(NULL, error, sizeof(error));
cbm_ui_assets_state_t state = cbm_ui_assets_state();
cbm_ui_assets_reset_for_testing();
ui_test_restore_assets_env(old_assets);
(void)cbm_unlink(pack_path);
(void)cbm_rmdir(directory);
ASSERT_FALSE(warmed);
ASSERT_EQ(state, CBM_UI_ASSETS_CANCELLED);
PASS();
}
TEST(ui_asset_pack_install_never_follows_a_predictable_temp_symlink) {
#ifdef _WIN32
SKIP_PLATFORM(
"Windows reparse-point creation requires privileges; installer contracts cover it");
#else
static const char sentinel_content[] = "foreign sentinel must survive\n";
char source_directory[] = "/tmp/cbm_ui_install_source_XXXXXX";
char install_directory[] = "/tmp/cbm_ui_install_target_XXXXXX";
ASSERT_NOT_NULL(cbm_mkdtemp(source_directory));
ASSERT_NOT_NULL(cbm_mkdtemp(install_directory));
size_t pack_length = 0U;
unsigned char *pack = ui_test_pack(&pack_length, false);
ASSERT_NOT_NULL(pack);
char source_pack[1024];
cbm_ui_assets_reset_for_testing();
ASSERT_TRUE(ui_test_publish_pack(source_directory, pack, pack_length, source_pack));
free(pack);
const char *pack_name = strrchr(source_pack, '/');
ASSERT_NOT_NULL(pack_name);
pack_name++;
char installed_pack[1024];
char old_temporary[1024];
char sentinel_path[1024];
ASSERT_LT(
snprintf(installed_pack, sizeof(installed_pack), "%s/%s", install_directory, pack_name),
(int)sizeof(installed_pack));
ASSERT_LT(snprintf(old_temporary, sizeof(old_temporary), "%s.tmp", installed_pack),
(int)sizeof(old_temporary));
ASSERT_LT(snprintf(sentinel_path, sizeof(sentinel_path), "%s/foreign.txt", install_directory),
(int)sizeof(sentinel_path));
ASSERT_EQ(th_write_file(sentinel_path, sentinel_content), 0);
ASSERT_EQ(symlink(sentinel_path, old_temporary), 0);
char *old_assets = getenv("CBM_UI_ASSETS_DIR") ? strdup(getenv("CBM_UI_ASSETS_DIR")) : NULL;
ASSERT_EQ(cbm_setenv("CBM_UI_ASSETS_DIR", source_directory, 1), 0);
char error[512] = {0};
bool installed = cbm_ui_assets_install(install_directory, false, error, sizeof(error));
char sentinel_after[sizeof(sentinel_content)] = {0};
FILE *sentinel_file = cbm_fopen(sentinel_path, "rb");
size_t sentinel_length =
sentinel_file ? fread(sentinel_after, 1U, sizeof(sentinel_after), sentinel_file) : 0U;
bool sentinel_closed = !sentinel_file || fclose(sentinel_file) == 0;
struct stat old_temp_info;
bool old_temp_is_still_symlink =
lstat(old_temporary, &old_temp_info) == 0 && S_ISLNK(old_temp_info.st_mode);
cbm_path_info_t installed_info;
bool installed_is_regular = cbm_path_info_utf8(installed_pack, &installed_info) == 0 &&
installed_info.is_regular && !installed_info.is_symlink;
bool removed = cbm_ui_assets_remove(install_directory, false, error, sizeof(error));
cbm_ui_assets_reset_for_testing();
ui_test_restore_assets_env(old_assets);
(void)cbm_unlink(old_temporary);
(void)cbm_unlink(installed_pack);
(void)cbm_unlink(sentinel_path);
(void)cbm_unlink(source_pack);
(void)cbm_rmdir(install_directory);
(void)cbm_rmdir(source_directory);
ASSERT_TRUE(installed);
ASSERT_TRUE(sentinel_closed);
ASSERT_EQ(sentinel_length, sizeof(sentinel_content) - 1U);
ASSERT_MEM_EQ(sentinel_after, sentinel_content, sizeof(sentinel_content) - 1U);
ASSERT_TRUE(old_temp_is_still_symlink);
ASSERT_TRUE(installed_is_regular);
ASSERT_TRUE(removed);
PASS();
#endif
}
TEST(ui_asset_pack_install_remove_is_hash_bound_and_dry_run_safe) {
char source_directory[] = "/tmp/cbm_ui_lifecycle_source_XXXXXX";
char install_directory[] = "/tmp/cbm_ui_lifecycle_target_XXXXXX";
ASSERT_NOT_NULL(cbm_mkdtemp(source_directory));
ASSERT_NOT_NULL(cbm_mkdtemp(install_directory));
size_t pack_length = 0U;
unsigned char *pack = ui_test_pack(&pack_length, false);
ASSERT_NOT_NULL(pack);
char source_pack[1024];
cbm_ui_assets_reset_for_testing();
ASSERT_TRUE(ui_test_publish_pack(source_directory, pack, pack_length, source_pack));
const char *pack_name = strrchr(source_pack, '/');
ASSERT_NOT_NULL(pack_name);
pack_name++;
char installed_pack[1024];
ASSERT_LT(
snprintf(installed_pack, sizeof(installed_pack), "%s/%s", install_directory, pack_name),
(int)sizeof(installed_pack));
char *old_assets = getenv("CBM_UI_ASSETS_DIR") ? strdup(getenv("CBM_UI_ASSETS_DIR")) : NULL;
ASSERT_EQ(cbm_setenv("CBM_UI_ASSETS_DIR", source_directory, 1), 0);
char error[512] = {0};
bool dry_install = cbm_ui_assets_install(install_directory, true, error, sizeof(error));
cbm_path_info_t info;
bool absent_after_dry_install = cbm_path_info_utf8(installed_pack, &info) != 0;
bool installed = cbm_ui_assets_install(install_directory, false, error, sizeof(error));
unsigned char *installed_bytes = malloc(pack_length);
FILE *installed_file = cbm_fopen(installed_pack, "rb");
size_t installed_length = installed_file && installed_bytes
? fread(installed_bytes, 1U, pack_length, installed_file)
: 0U;
bool installed_closed = !installed_file || fclose(installed_file) == 0;
bool exact_copy = installed_bytes && installed_length == pack_length &&
memcmp(installed_bytes, pack, pack_length) == 0;
free(installed_bytes);
bool dry_remove = cbm_ui_assets_remove(install_directory, true, error, sizeof(error));
bool present_after_dry_remove = cbm_path_info_utf8(installed_pack, &info) == 0;
bool removed = cbm_ui_assets_remove(install_directory, false, error, sizeof(error));
bool absent_after_remove = cbm_path_info_utf8(installed_pack, &info) != 0;
bool reinstalled = cbm_ui_assets_install(install_directory, false, error, sizeof(error));
FILE *tampered_file = cbm_fopen(installed_pack, "r+b");
bool tampered = tampered_file && fseek(tampered_file, -1L, SEEK_END) == 0;
int original_last_byte = tampered ? fgetc(tampered_file) : EOF;
tampered = tampered && original_last_byte != EOF && fseek(tampered_file, -1L, SEEK_END) == 0 &&
fputc(original_last_byte ^ 1, tampered_file) != EOF;
if (tampered_file && fclose(tampered_file) != 0) {
tampered = false;
}
bool refused_tampered_remove =
!cbm_ui_assets_remove(install_directory, false, error, sizeof(error));
bool tampered_preserved = cbm_path_info_utf8(installed_pack, &info) == 0;
bool preservation_explained = strstr(error, "not owned; preserved") != NULL;
cbm_ui_assets_reset_for_testing();
ui_test_restore_assets_env(old_assets);
(void)cbm_unlink(installed_pack);
(void)cbm_unlink(source_pack);
(void)cbm_rmdir(install_directory);
(void)cbm_rmdir(source_directory);
free(pack);
ASSERT_TRUE(dry_install);
ASSERT_TRUE(absent_after_dry_install);
ASSERT_TRUE(installed);
ASSERT_TRUE(installed_closed);
ASSERT_TRUE(exact_copy);
ASSERT_TRUE(dry_remove);
ASSERT_TRUE(present_after_dry_remove);
ASSERT_TRUE(removed);
ASSERT_TRUE(absent_after_remove);
ASSERT_TRUE(reinstalled);
ASSERT_TRUE(tampered);
ASSERT_TRUE(refused_tampered_remove);
ASSERT_TRUE(tampered_preserved);
ASSERT_TRUE(preservation_explained);
PASS();
}
TEST(ui_asset_pack_staged_removal_is_owned_and_transactional) {
char source_directory[] = "/tmp/cbm_ui_staged_remove_source_XXXXXX";
char install_directory[] = "/tmp/cbm_ui_staged_remove_target_XXXXXX";
ASSERT_NOT_NULL(cbm_mkdtemp(source_directory));
ASSERT_NOT_NULL(cbm_mkdtemp(install_directory));
size_t pack_length = 0U;
unsigned char *pack = ui_test_pack(&pack_length, false);
ASSERT_NOT_NULL(pack);
char source_pack[1024];
cbm_ui_assets_reset_for_testing();
ASSERT_TRUE(ui_test_publish_pack(source_directory, pack, pack_length, source_pack));
const char *pack_name = strrchr(source_pack, '/');
ASSERT_NOT_NULL(pack_name);
pack_name++;
char installed_pack[1024];
ASSERT_LT(
snprintf(installed_pack, sizeof(installed_pack), "%s/%s", install_directory, pack_name),
(int)sizeof(installed_pack));
char *old_assets = getenv("CBM_UI_ASSETS_DIR") ? strdup(getenv("CBM_UI_ASSETS_DIR")) : NULL;
ASSERT_EQ(cbm_setenv("CBM_UI_ASSETS_DIR", source_directory, 1), 0);
char error[512] = {0};
ASSERT_TRUE(cbm_ui_assets_install(install_directory, false, error, sizeof(error)));
cbm_activation_transaction_t *removal = NULL;
bool foreign = false;
bool staged =
cbm_ui_assets_stage_remove(install_directory, &removal, &foreign, error, sizeof(error));
ASSERT_TRUE(staged);
ASSERT_FALSE(foreign);
ASSERT_NOT_NULL(removal);
ASSERT_EQ(cbm_activation_transaction_commit(removal, NULL, NULL),
CBM_ACTIVATION_TRANSACTION_OK);
ASSERT_EQ(cbm_activation_transaction_finalize(removal), CBM_ACTIVATION_TRANSACTION_OK);
ASSERT_EQ(cbm_activation_transaction_close(&removal), CBM_ACTIVATION_TRANSACTION_OK);
cbm_path_info_t info;
bool exact_removed = cbm_path_info_utf8(installed_pack, &info) != 0;
ASSERT_TRUE(cbm_ui_assets_install(install_directory, false, error, sizeof(error)));
FILE *tampered_file = cbm_fopen(installed_pack, "r+b");
ASSERT_NOT_NULL(tampered_file);
ASSERT_EQ(fseek(tampered_file, -1L, SEEK_END), 0);
int last_byte = fgetc(tampered_file);
ASSERT_TRUE(last_byte != EOF);
ASSERT_EQ(fseek(tampered_file, -1L, SEEK_END), 0);
ASSERT_TRUE(fputc(last_byte ^ 1, tampered_file) != EOF);
ASSERT_EQ(fclose(tampered_file), 0);
removal = NULL;
foreign = false;
bool foreign_staged =
cbm_ui_assets_stage_remove(install_directory, &removal, &foreign, error, sizeof(error));
bool foreign_preserved = cbm_path_info_utf8(installed_pack, &info) == 0;
cbm_ui_assets_reset_for_testing();
ui_test_restore_assets_env(old_assets);
(void)cbm_activation_transaction_close(&removal);
(void)cbm_unlink(installed_pack);
(void)cbm_unlink(source_pack);
(void)cbm_rmdir(install_directory);
(void)cbm_rmdir(source_directory);
free(pack);
ASSERT_TRUE(exact_removed);
ASSERT_TRUE(foreign_staged);
ASSERT_TRUE(foreign);
ASSERT_NULL(removal);
ASSERT_TRUE(foreign_preserved);
PASS();
}
/* File identity alone does not detect a truncate/rewrite through an already
* open handle. Publication must validate the exact bytes at the public name,
* and removal must validate the exact object after moving it to the private
* retained name. Both mutations below preserve the inode on POSIX. */
TEST(ui_asset_pack_commit_rejects_in_place_rewrite_after_stage) {
char source_directory[] = "/tmp/cbm_ui_commit_source_XXXXXX";
char install_directory[] = "/tmp/cbm_ui_commit_target_XXXXXX";
ASSERT_NOT_NULL(cbm_mkdtemp(source_directory));
ASSERT_NOT_NULL(cbm_mkdtemp(install_directory));
size_t pack_length = 0U;
unsigned char *pack = ui_test_pack(&pack_length, false);
ASSERT_NOT_NULL(pack);
char source_pack[1024];
cbm_ui_assets_reset_for_testing();
ASSERT_TRUE(ui_test_publish_pack(source_directory, pack, pack_length, source_pack));
const char *pack_name = strrchr(source_pack, '/');
ASSERT_NOT_NULL(pack_name);
pack_name++;
char installed_pack[1024];
ASSERT_LT(
snprintf(installed_pack, sizeof(installed_pack), "%s/%s", install_directory, pack_name),
(int)sizeof(installed_pack));
char *old_assets = getenv("CBM_UI_ASSETS_DIR") ? strdup(getenv("CBM_UI_ASSETS_DIR")) : NULL;
ASSERT_EQ(cbm_setenv("CBM_UI_ASSETS_DIR", source_directory, 1), 0);
char error[512] = {0};
ASSERT_TRUE(cbm_ui_assets_install(install_directory, false, error, sizeof(error)));
ASSERT_TRUE(cbm_ui_assets_verify_file(installed_pack));
cbm_activation_transaction_t *publication = NULL;
ASSERT_TRUE(
cbm_ui_assets_stage_install(install_directory, &publication, error, sizeof(error)));
ASSERT_NOT_NULL(publication);
const char *staged = cbm_activation_transaction_staged_path(publication);
ASSERT_NOT_NULL(staged);
FILE *staged_file = cbm_fopen(staged, "r+b");
ASSERT_NOT_NULL(staged_file);
ASSERT_EQ(fseek(staged_file, -1L, SEEK_END), 0);
int staged_last = fgetc(staged_file);
ASSERT_TRUE(staged_last != EOF);
ASSERT_EQ(fseek(staged_file, -1L, SEEK_END), 0);
ASSERT_TRUE(fputc(staged_last ^ 1, staged_file) != EOF);
ASSERT_EQ(fclose(staged_file), 0);
ASSERT_EQ(cbm_ui_assets_commit_install(publication),
CBM_ACTIVATION_TRANSACTION_VALIDATION_FAILED);
ASSERT_EQ(cbm_activation_transaction_close(&publication), CBM_ACTIVATION_TRANSACTION_OK);
ASSERT_TRUE(cbm_ui_assets_verify_file(installed_pack));
cbm_activation_transaction_t *removal = NULL;
bool foreign = false;
ASSERT_TRUE(cbm_ui_assets_stage_remove(install_directory, &removal, &foreign, error,
sizeof(error)));
ASSERT_FALSE(foreign);
ASSERT_NOT_NULL(removal);
FILE *target_file = cbm_fopen(installed_pack, "r+b");
ASSERT_NOT_NULL(target_file);
ASSERT_EQ(fseek(target_file, -1L, SEEK_END), 0);
int target_last = fgetc(target_file);
ASSERT_TRUE(target_last != EOF);
ASSERT_EQ(fseek(target_file, -1L, SEEK_END), 0);
ASSERT_TRUE(fputc(target_last ^ 1, target_file) != EOF);
ASSERT_EQ(fclose(target_file), 0);
ASSERT_EQ(cbm_ui_assets_commit_removal(removal),
CBM_ACTIVATION_TRANSACTION_VALIDATION_FAILED);
ASSERT_EQ(cbm_activation_transaction_close(&removal), CBM_ACTIVATION_TRANSACTION_OK);
cbm_path_info_t preserved_info;
ASSERT_EQ(cbm_path_info_utf8(installed_pack, &preserved_info), 0);
ASSERT_TRUE(preserved_info.is_regular);
ASSERT_FALSE(cbm_ui_assets_verify_file(installed_pack));
cbm_ui_assets_reset_for_testing();
ui_test_restore_assets_env(old_assets);
(void)cbm_unlink(installed_pack);
(void)cbm_unlink(source_pack);
(void)cbm_rmdir(install_directory);
(void)cbm_rmdir(source_directory);
free(pack);
PASS();
}
TEST(ui_asset_standard_manifest_has_no_capability) {
cbm_ui_assets_reset_for_testing();
ASSERT_FALSE(cbm_ui_assets_supported());
ASSERT_EQ(cbm_ui_assets_state(), CBM_UI_ASSETS_UNAVAILABLE);
ASSERT_NULL(cbm_ui_asset_lookup("/index.html"));
TEST(embedded_stub_count) {
/* Stub should have 0 files */
ASSERT_EQ(CBM_EMBEDDED_FILE_COUNT, 0);
PASS();
}
@@ -1398,16 +870,9 @@ SUITE(ui) {
RUN_TEST(config_corrupt_file);
RUN_TEST(config_missing_fields);
/* External UI asset pack */
RUN_TEST(ui_asset_pack_validates_before_publication);
RUN_TEST(ui_asset_pack_wrong_hash_fails_closed);
RUN_TEST(ui_asset_pack_structural_corruption_fails_after_valid_hash);
RUN_TEST(ui_asset_pack_prestart_cancellation_is_not_lost);
RUN_TEST(ui_asset_pack_install_never_follows_a_predictable_temp_symlink);
RUN_TEST(ui_asset_pack_install_remove_is_hash_bound_and_dry_run_safe);
RUN_TEST(ui_asset_pack_staged_removal_is_owned_and_transactional);
RUN_TEST(ui_asset_pack_commit_rejects_in_place_rewrite_after_stage);
RUN_TEST(ui_asset_standard_manifest_has_no_capability);
/* Embedded assets (stub) */
RUN_TEST(embedded_lookup_not_found);
RUN_TEST(embedded_stub_count);
/* Layout engine */
RUN_TEST(layout_empty_graph);
-163
View File
@@ -1,163 +0,0 @@
#!/usr/bin/env bash
# Contract for the external UI asset pack. The pack deliberately stays
# uncompressed and independently scannable: this is an asset boundary, not an
# attempt to hide frontend bytes from security tooling.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORK="$(mktemp -d "${TMPDIR:-/tmp}/cbm-ui-pack-test.XXXXXX")"
trap 'rm -rf "$WORK"' EXIT
make_real_dir_link() {
local target="$1" link="$2"
if MSYS=winsymlinks:nativestrict ln -s -- "$target" "$link" 2>/dev/null &&
[ -L "$link" ]; then
return 0
fi
rm -rf -- "$link"
case "$(uname -s)" in
MINGW* | MSYS* | CYGWIN*)
cmd //c "mklink /J \"$(cygpath -w "$link")\" \"$(cygpath -w "$target")\"" \
>/dev/null 2>&1 || true
;;
esac
[ -L "$link" ]
}
mkdir -p "$WORK/a/assets" "$WORK/b/assets"
printf '%s\n' '<!doctype html><title>Codebase Memory — Graph</title>' > "$WORK/a/index.html"
printf '%s\n' 'console.log("transparent-ui-pack")' > "$WORK/a/assets/app.js"
printf '%s\n' 'body { color: #fff; }' > "$WORK/a/assets/app.css"
# Copy in a deliberately different creation order. Filesystem enumeration,
# mtimes, and source-root names must not affect the output.
cp "$WORK/a/assets/app.css" "$WORK/b/assets/app.css"
cp "$WORK/a/index.html" "$WORK/b/index.html"
cp "$WORK/a/assets/app.js" "$WORK/b/assets/app.js"
touch -t 200101010101 "$WORK/a/index.html"
touch -t 203001010101 "$WORK/b/index.html"
mkdir -p "$WORK/a-out" "$WORK/b-out"
node "$ROOT/scripts/pack-ui-assets.mjs" \
"$WORK/a" "$WORK/a-out" "$WORK/a-manifest.c"
node "$ROOT/scripts/pack-ui-assets.mjs" \
"$WORK/b" "$WORK/b-out" "$WORK/b-manifest.c"
PACK_A="$(find "$WORK/a-out" -maxdepth 1 -type f -name 'cbm-ui-*.pack')"
PACK_B="$(find "$WORK/b-out" -maxdepth 1 -type f -name 'cbm-ui-*.pack')"
[ -n "$PACK_A" ] && [ -n "$PACK_B" ]
[ "$(basename "$PACK_A")" = "$(basename "$PACK_B")" ]
cmp "$PACK_A" "$PACK_B"
cmp "$WORK/a-manifest.c" "$WORK/b-manifest.c"
python3 - "$PACK_A" "$WORK/a-manifest.c" <<'PY'
from __future__ import annotations
import hashlib
import pathlib
import re
import struct
import sys
pack_path = pathlib.Path(sys.argv[1])
manifest_path = pathlib.Path(sys.argv[2])
data = pack_path.read_bytes()
manifest = manifest_path.read_text(encoding="utf-8")
(
magic,
version,
header_bytes,
flags,
count,
entry_bytes,
index_offset,
index_size,
paths_offset,
paths_size,
payload_offset,
payload_size,
total_size,
) = struct.unpack_from("<8sHHIIIQQQQQQQ", data, 0)
assert magic == b"CBMUIPK\0", "wrong pack magic"
assert version == 1
assert header_bytes == 80
assert flags == 0
assert count == 3
assert entry_bytes == 24
assert index_offset == 80
assert index_size == count * entry_bytes
assert paths_offset == index_offset + index_size
assert payload_offset == paths_offset + paths_size
assert payload_size == len(data) - payload_offset
assert total_size == len(data)
cursor = index_offset
entries = []
for _ in range(count):
path_offset, path_len, mime_id, cache_id, offset, length = struct.unpack_from(
"<IHBBQQ", data, cursor
)
cursor += 24
path = data[
paths_offset + path_offset:paths_offset + path_offset + path_len
].decode("ascii")
entries.append((path, path_offset, path_len, mime_id, cache_id, offset, length))
assert cursor == paths_offset
assert [entry[0] for entry in entries] == [
"/assets/app.css", "/assets/app.js", "/index.html"
]
assert [entry[3] for entry in entries] == [3, 2, 1]
assert [entry[4] for entry in entries] == [2, 2, 1]
path_cursor = 0
for path, path_offset, path_len, *_rest in entries:
assert path_offset == path_cursor, f"non-contiguous path table for {path}"
path_cursor += path_len
assert path_cursor == paths_size
payload_cursor = 0
for path, _path_offset, _path_len, _mime, _cache, offset, length in entries:
assert offset == payload_cursor, f"non-contiguous payload for {path}"
payload_cursor += length
assert payload_cursor == payload_size
# The bytes remain visible to scanners and SBOM/license tooling. Compression
# or encoding would make this assertion fail.
assert b"transparent-ui-pack" in data
assert b"<!doctype html>" in data
digest = hashlib.sha256(data).hexdigest()
assert re.search(rf'CBM_UI_ASSET_SHA256\[\]\s*=\s*"{digest}"', manifest)
assert re.search(rf"CBM_UI_ASSET_SIZE\s*=\s*UINT64_C\({len(data)}\)", manifest)
assert re.search(
rf'CBM_UI_ASSET_PACK_NAME\[\]\s*=\s*"cbm-ui-{digest}\.pack"', manifest
)
assert pack_path.name == f"cbm-ui-{digest}.pack"
PY
# A source symlink makes the build depend on bytes outside the declared dist
# tree. MSYS2's default `ln -s` silently deep-copies its target, so require a
# real native link and fall back to an NTFS junction before testing rejection.
mkdir "$WORK/outside-assets"
printf '%s\n' 'outside the declared UI root' > "$WORK/outside-assets/linked.html"
if ! make_real_dir_link "$WORK/outside-assets" "$WORK/a/assets/linked"; then
echo "FAIL: could not create a real symlink/junction fixture" >&2
exit 1
fi
if rejected_output="$(node "$ROOT/scripts/pack-ui-assets.mjs" \
"$WORK/a" "$WORK/a-out" "$WORK/rejected.c" 2>&1)"; then
echo "FAIL: UI pack generator accepted a symlink" >&2
exit 1
fi
case "$rejected_output" in
*"symbolic links are not allowed"*) ;;
*)
echo "FAIL: UI pack generator rejected the symlink for the wrong reason" >&2
printf '%s\n' "$rejected_output" >&2
exit 1
;;
esac
echo "PASS: deterministic, transparent, hash-bound UI asset pack"
+79 -125
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env bash
# INVERTED release-surface contract: Windows ships ONE executable plus its
# authenticated runtime assets.
# INVERTED release-surface contract: Windows ships ONE binary.
#
# History this guards against. The Windows release used to be a PAIR — a small
# permanent launcher (codebase-memory-mcp.exe) plus the real product binary
@@ -8,15 +7,18 @@
# reason: a running .exe cannot replace its own image on Windows, so an
# in-process self-update needs a second resident binary to do the swap.
#
# That stub was a small unsigned PE whose whole job was verify-and-execute
# another binary: unnecessary loader-like behavior and a second artifact to
# audit. Historical variants received Microsoft Wacatac verdicts, but those
# observations did not expose a stable feature or prove classifier causation.
# That stub is statically indistinguishable from a dropper — a small unsigned
# PE whose whole job is verify-and-execute another binary — and Defender's ML
# scored it Trojan:Win32/Wacatac.B!ml on x64 regardless of what we changed
# (bcrypt-free, stripped, VERSIONINFO'd and even resource-free builds were all
# flagged), while the product binary itself scans clean on every platform.
#
# The fix removed the stub. The downloaded candidate now owns the complete
# runtime-set transaction under the native activation guard; install.ps1 only
# verifies/extracts and invokes it. In particular, the script must never retire
# the old executable or publish sidecars ahead of the guarded transaction.
# The fix removed the stub and moved self-update OUT of the process into
# install.ps1, which runs while CBM is NOT running. So this file asserts the
# ABSENCE of the flagged design, plus the one step that replaces it:
# install.ps1 must RETIRE the running binary (rename it out of the way) before
# publishing the new one — losing that step silently breaks every Windows
# update.
#
# The non-launcher release/security assertions from the previous contract
# (VM-driver hardening, archive allowlists, HTTPS-only downloads, profile-rooted
@@ -73,32 +75,22 @@ def yaml_run_blocks(text: str) -> list[str]:
binary = "codebase-memory-mcp.exe"
payload = "codebase-memory-mcp.payload.exe"
# cbm-integrations.json: the integration templates the binary verifies by its
# embedded SHA-256 before installing anything. install.ps1 runs `install` from
# the extract dir, so the file must sit NEXT TO the .exe or every install
# fails closed with "integration assets missing or modified".
windows_archive_names = (
binary, "cbm-integrations.json", "LICENSE", "install.ps1", "THIRD_PARTY_NOTICES.md",
)
ui_pack_pattern = r"cbm-ui-[0-9a-f]{64}\.pack"
windows_archive_names = (binary, "LICENSE", "install.ps1", "THIRD_PARTY_NOTICES.md")
# ── 1. Standard is five files; UI adds exactly one root pack ────────────────
# ── 1. The archive is exactly four files, defined in ONE place ───────────────
# Every venue (release build, local artifact-flow smoke) produces archives
# through scripts/package-release.sh, so the layout is asserted where it is
# defined and cannot fork per venue.
package_release = read("scripts/package-release.sh")
member_blocks = re.findall(
r"ARCHIVE_MEMBERS=\(\s*(?P<members>.*?)\s*\)", package_release, re.DOTALL
zip_call = re.search(
r"zip -q \"\$OUT_DIR/\$NAME\.zip\" \\\n(?P<members>(?:.|\n)*?)\n", package_release
)
normalized_member_blocks = [block.split() for block in member_blocks]
require(zip_call is not None, "package-release.sh must build the Windows zip in one zip call")
zip_members = zip_call.group("members").split() if zip_call else []
require(
list(windows_archive_names) in normalized_member_blocks,
"package-release.sh must define the exact five-member Windows standard archive",
)
require(
package_release.count('ARCHIVE_MEMBERS+=("$UI_PACK_NAME")') == 2
and "^cbm-ui-[0-9a-f]{64}\\.pack$" in package_release,
"package-release.sh must append exactly one hash-shaped root pack only to UI archives",
zip_members == list(windows_archive_names),
"package-release.sh must archive EXACTLY "
f"{' '.join(windows_archive_names)} (found: {' '.join(zip_members) or 'nothing'})",
)
# ── 2. No shipped surface may name the payload or build a launcher ───────────
@@ -124,8 +116,8 @@ for relative in shipped_surfaces:
for marker in launcher_markers:
require(
marker not in source,
f"{relative} must not reference '{marker}': Windows ships one executable and the "
"retired launcher/payload split must not return",
f"{relative} must not reference '{marker}': Windows ships ONE binary and the "
"launcher stub is what Defender flagged as Trojan:Win32/Wacatac.B!ml",
)
# The stub sources themselves must stay deleted, and the build must not carry a
@@ -147,27 +139,30 @@ require(
"Makefile.cbm must not expose a Windows launcher target or variable",
)
# The single shipped Windows runtime is UI-enabled and is produced through the
# ONE canonical packaging entry, so the six-file layout above governs it.
# Each archive variant is still produced through the ONE canonical packaging
# entry, so the four-file layout above governs all of them.
build_workflow = read(".github/workflows/_build.yml")
for archive, call in (
for archive, call, ui in (
("codebase-memory-mcp-windows-amd64.zip", "scripts/package-release.sh windows amd64", False),
("codebase-memory-mcp-ui-windows-amd64.zip",
"scripts/package-release.sh windows amd64 --variant ui"),
"scripts/package-release.sh windows amd64 --variant ui", True),
("codebase-memory-mcp-windows-arm64.zip", "scripts/package-release.sh windows arm64", False),
("codebase-memory-mcp-ui-windows-arm64.zip",
"scripts/package-release.sh windows arm64 --variant ui"),
"scripts/package-release.sh windows arm64 --variant ui", True),
):
# The lookahead keeps a ui call from satisfying a standard check.
pattern = re.escape(call) + ("" if ui else r"(?!\s+--variant)")
require(
build_workflow.count(call) == 1,
re.search(pattern, build_workflow) is not None,
f"_build.yml must produce {archive} via the canonical packaging entry ('{call}')",
)
require(
re.search(r"scripts/package-release\.sh windows (?:amd64|arm64)(?!\s+--variant)", build_workflow) is None
and "ui_only" not in build_workflow
and "Build standard binary" not in build_workflow,
"_build.yml must not retain a standard/non-UI Windows release path",
)
# ── 3. install.ps1 delegates every runtime mutation to the candidate ─────────
# ── 3. install.ps1 retires the running binary before publishing ──────────────
# This is THE step that replaces the launcher. Windows keeps an image lock on a
# running .exe: it cannot be overwritten, but it CAN be renamed out of the way.
# install.ps1 runs while CBM is not running, renames the installed binary to
# "<dest>.retired-<timestamp>", then installs over the freed name. Without the
# retire step every Windows update fails on a locked destination.
installer = read("install.ps1")
# Windows PowerShell 5.1 decodes a BOM-less .ps1 as ANSI, so a UTF-8 em-dash
# arrives as three cp1252 characters ending in a double quote. Inside a string
@@ -182,76 +177,43 @@ require(
+ ", ".join(f"U+{ord(ch):04X}" for ch in non_ascii)
+ ")",
)
require(
"$Dest = Join-Path $InstallDir $BinName" in installer,
"install.ps1 must resolve the canonical install destination as $Dest",
)
retire_match = re.search(
r"if \(Test-Path -LiteralPath \$Dest -PathType Leaf\) \{(?P<body>(?:.|\n)*?)\n\}\n",
installer,
)
retire = retire_match.group("body") if retire_match else ""
require(
retire_match is not None
and re.search(r"\$retired\s*=\s*\"\$Dest\.retired-", retire) is not None
and "Move-Item -LiteralPath $Dest -Destination $retired" in retire,
"install.ps1 must RETIRE the running binary (Move-Item $Dest -> $retired) before "
"publishing the new one — a running .exe cannot be overwritten in place",
)
require(
retire_match is not None and "$renamed" in retire and "exit 1" in retire,
"install.ps1 must fail loudly when the running binary cannot be retired",
)
publish_index = installer.find("& $DownloadedBinary @InstallArgs")
require(
publish_index >= 0,
"install.ps1 must invoke the downloaded candidate's native install command",
publish_index > (retire_match.start() if retire_match else -1) >= 0,
"install.ps1 must retire BEFORE it publishes the downloaded binary",
)
require(
"& $DownloadedBinary --version" in installer
and "& $DownloadedBinary @InstallArgs" in installer,
"install.ps1 must verify and install through the downloaded binary",
)
require(
"Move-Item -LiteralPath $Dest" not in installer
and ".retired-" not in installer
and "& $Dest daemon stop" not in installer,
"install.ps1 must not retire, delete, or stop the installed executable before candidate install",
)
require(
"Publish-CbmSidecarAtomically" not in installer
and "$AssetDest" not in installer
and "$packDestination" not in installer
and 'Get-ChildItem -LiteralPath $InstallDir -Filter "cbm-ui-*.pack"' not in installer,
"install.ps1 must not publish or garbage-collect runtime sidecars outside the native guard",
)
# The download workspace is recursively deleted on every exit path, so the
# installer must create it itself and must never adopt an existing name. Bind
# the reservation to this directory specifically; the updater-file publisher
# below has a separate exclusive-sibling helper.
temp_dir_factory = re.search(
r"function\s+New-CbmExclusiveTempDirectory\s*\{(?P<body>.*?)\n\}\s*\n\s*# Detect variant",
installer,
re.DOTALL,
)
temp_dir_body = temp_dir_factory.group("body") if temp_dir_factory else ""
require(
temp_dir_factory is not None
and '[guid]::NewGuid().ToString("N")' in temp_dir_body
and "for ($attempt = 0; $attempt -lt 32; $attempt++)" in temp_dir_body
and "New-Item -ItemType Directory -Path $candidate -ErrorAction Stop" in temp_dir_body
and "catch [System.IO.IOException]" in temp_dir_body
and re.search(r"New-Item[^\n]*-Force", temp_dir_body) is None
and "Remove-Item" not in temp_dir_body
and "Test-Path" not in temp_dir_body,
"install.ps1 must reserve a high-entropy temporary directory exclusively with bounded "
"collision retries, never adopt or pre-clean an existing path",
)
require(
"$TmpDir = New-CbmExclusiveTempDirectory -ParentDirectory "
"([System.IO.Path]::GetTempPath())" in installer
and '"cbm-install-$(Get-Random)"' not in installer
and "New-Item -ItemType Directory -Path $TmpDir -Force" not in installer,
"install.ps1 must clean up only the temporary directory it successfully reserved",
)
# Bind validation to the opened handle, including its exact 64-bit size. A
# raced replacement with a valid prefix plus trailing data must be rejected.
asset_pack = read("src/ui/asset_pack.c")
require(
"handle_info.nFileSizeHigh" in asset_pack
and "handle_info.nFileSizeLow" in asset_pack
and re.search(r"handle_size\s*==\s*expected", asset_pack) is not None,
"Windows UI pack validation must compare the opened handle's 64-bit size to the manifest",
)
# ── 4. Package-manager shims resolve the single Windows binary ───────────────
single_binary_contracts = {
"pkg/npm/install.js": (
r"const\s+WINDOWS_BINARY_NAME\s*=\s*['\"]codebase-memory-mcp\.exe['\"]",
r"publishRuntimeSetWithRecovery\(",
r"runtimeSetReady\(",
r"installWindowsBinaryAtomically\(",
r"windowsBinaryReady\(",
),
"pkg/npm/bin.js": (
r"binName\s*=\s*isWindows\s*\?\s*['\"]codebase-memory-mcp\.exe['\"]",
@@ -259,8 +221,7 @@ single_binary_contracts = {
),
"pkg/pypi/src/codebase_memory_mcp/_cli.py": (
r"_WINDOWS_BINARY_NAME\s*=\s*['\"]codebase-memory-mcp\.exe['\"]",
r"def\s+_runtime_set_ready\(",
r"def\s+_publish_runtime_set\(",
r"def\s+_windows_binary_ready\(",
),
}
for relative, patterns in single_binary_contracts.items():
@@ -274,21 +235,18 @@ for relative, patterns in single_binary_contracts.items():
f"{relative} must remain portable and not own managed launcher state",
)
# All package downloaders parse the Windows archive against an exact official
# root allowlist; direct install.ps1 additionally distinguishes standard from
# UI by the single hash-shaped pack.
# All package downloaders parse the Windows archive against the exact official
# four-root-file allowlist; they may not silently ignore an attacker-controlled
# fifth member.
exact_archive_guards = {
"install.ps1": (
"$seen.Count -ne $expectedArchiveCount",
"$uiPackCount -ne $expectedUiPackCount",
"$UiPackPattern",
"$seen.Count -ne $WindowsArchiveNames.Count",
'"LICENSE"',
'"install.ps1"',
"THIRD_PARTY_NOTICES.md",
),
"pkg/npm/install.js": (
"seen.size !== expectedCount",
"UI_PACK_PATTERN",
"$seen.Count -ne $requiredNames.Count",
"WINDOWS_BINARY_NAME",
"'LICENSE'",
"'install.ps1'",
@@ -296,8 +254,7 @@ exact_archive_guards = {
),
"pkg/pypi/src/codebase_memory_mcp/_cli.py": (
"name not in required_set",
"len(seen) != expected_count",
"_UI_PACK_RE",
"len(seen) != len(required)",
"_WINDOWS_BINARY_NAME",
'"LICENSE"',
'"install.ps1"',
@@ -308,7 +265,7 @@ for relative, needles in exact_archive_guards.items():
source = read(relative)
require(
all(needle in source for needle in needles),
f"{relative} must reject every Windows zip namespace except its official exact "
f"{relative} must reject every Windows zip namespace except the official four-file "
"allowlist",
)
@@ -593,7 +550,7 @@ require(
"cmd.exe",
)
# ── 8. Release smoke stays profile-rooted and one-executable ─────────────────
# ── 8. Release smoke stays profile-rooted and single-binary ──────────────────
smoke_workflow = read(".github/workflows/_smoke.yml")
windows_match = re.search(
r"(?ms)^ smoke-windows:\s*(.*?)(?=^ [A-Za-z0-9_-]+:\s*$|\Z)", smoke_workflow
@@ -644,7 +601,7 @@ require(
'"$LAUNCH_DIR/codebase-memory-mcp.exe" --version',
)
),
"Windows release version checks must execute the runtime set's one executable beneath the current "
"Windows release version checks must execute the single binary beneath the current "
"account profile",
)
# RUNNER_TEMP is legitimate ONLY for artifact provisioning/scanning; every
@@ -681,7 +638,7 @@ require(
'scripts/security-install.sh "$SECURITY_DIR/codebase-memory-mcp.exe"',
)
),
"Windows release install audit must execute the runtime set's one executable beneath the current "
"Windows release install audit must execute the single binary beneath the current "
"account profile",
)
@@ -689,11 +646,8 @@ require(
smoke_script = read("scripts/smoke-test.sh")
require(
'copy_smoke_binary "$FAKE_HOME/.local/bin/codebase-memory-mcp.exe"' not in smoke_script
and 'copy_smoke_binary "$UPDATE_HOME/.local/bin/codebase-memory-mcp.exe"' in smoke_script
and 'cp "$SMOKE_UI_PACK" "$(dirname "$destination")/$(basename "$SMOKE_UI_PACK")"'
in smoke_script,
"Windows smoke must leave the authenticated-install target absent while staging the complete "
"binary-plus-pack runtime set for the explicit update fixture",
and 'copy_smoke_binary "$UPDATE_HOME/.local/bin/codebase-memory-mcp.exe"' not in smoke_script,
"Windows smoke must leave canonical targets absent for an authenticated install",
)
require(
"smoke_mktemp_file" in smoke_script
@@ -722,7 +676,7 @@ require(
'HOME="$WIN_HOME" TEMP="$WIN_HOME" TMP="$WIN_HOME"' in smoke_script
and "MSYS2_ARG_CONV_EXCL='*'" in smoke_script
and "powershell.exe -NoProfile -ExecutionPolicy Bypass -File" in smoke_script
and '"$WIN_SCRIPT" "$DL_VARIANT_ARG" "--dir=$WIN_DIR"' in smoke_script
and '"$WIN_SCRIPT" "--dir=$WIN_DIR"' in smoke_script
and "& $args[1]" not in smoke_script,
"Windows install.ps1 smoke must pass native HOME/TEMP/TMP and execute the script directly",
)
@@ -850,10 +804,10 @@ if pr_windows_blocks:
)
if failures:
print("Windows one-executable runtime-set contract FAILED:", file=sys.stderr)
print("Windows single-binary bundle contract FAILED:", file=sys.stderr)
for failure in failures:
print(f" - {failure}", file=sys.stderr)
raise SystemExit(1)
print("Windows one-executable runtime-set contract passed")
print("Windows single-binary bundle contract passed")
PY
+5 -214
View File
@@ -1,4 +1,4 @@
"""GREEN regression guard — non-ASCII runtime and repo paths work on Windows.
"""GREEN regression guard — non-ASCII repo paths keep all definitions on Windows.
Guards the fix for issue #636 / #357 (landed on main via #700) at the product
surface (real codebase-memory-mcp process, real SQLite DB, real stdio). Two
@@ -17,8 +17,8 @@ code page, so a non-ASCII path could not be opened and the parser received
nothing. #700 routed the per-pass reads through cbm_fopen (→ _wfopen with a wide
path, src/foundation/compat_fs.c), so non-ASCII paths now parse identically.
This Windows guard also exercises native runtime publication beyond legacy
MAX_PATH; it fails (red) if either path contract regresses.
This guard fails (red) if that fix regresses. It also passes on Linux/macOS
(byte-transparent UTF-8 filesystem).
Exit code: 0 == invariant holds (green), 1 == invariant violated (regression),
2 == environment/setup error.
@@ -29,7 +29,6 @@ Usage:
import json
import os
import shutil
import subprocess
import sys
import tempfile
@@ -63,204 +62,6 @@ NON_ASCII_SEGMENTS = {
}
def windows_extended_path(path):
"""Return a Python/Win32 path that is independent of LongPathsEnabled."""
absolute = os.path.abspath(path).replace("/", "\\")
if absolute.startswith("\\\\?\\"):
return absolute
if absolute.startswith("\\\\"):
return "\\\\?\\UNC\\" + absolute[2:]
return "\\\\?\\" + absolute
def run_product(argv, cwd, env, timeout=120):
options = {
"cwd": cwd,
"env": env,
"stdout": subprocess.PIPE,
"stderr": subprocess.STDOUT,
"text": True,
"timeout": timeout,
}
# With lpApplicationName=NULL, CreateProcess applies the legacy MAX_PATH
# limit while deriving the module name from the command line. Python's
# explicit `executable` argument fills lpApplicationName, allowing the
# extended path to reach the product exactly as a native launcher would.
if os.name == "nt" and argv and argv[0].startswith("\\\\?\\"):
options["executable"] = argv[0]
return subprocess.run(argv, **options)
def verify_relocated_runtime(binary, work):
"""Probe relocation, then install/probe/uninstall beyond legacy MAX_PATH.
This binds Windows self-discovery to GetModuleFileNameW: both the hidden
probe and a process launched from an extended-length path must find the
adjacent integration/UI assets without an ANSI-code-page fallback or an
environment override. The real install sequence also binds target
existence checks: after a secure first install, the target is rewritten in
place and a non-force `--no` install must preserve those exact bytes.
"""
source_dir = os.path.dirname(binary)
runtime_dir = os.path.join(work, "café_日本語_runtime")
os.makedirs(runtime_dir, exist_ok=True)
relocated = os.path.join(runtime_dir, os.path.basename(binary))
shutil.copy2(binary, relocated)
integration = os.path.join(source_dir, "cbm-integrations.json")
if not os.path.isfile(integration):
return "setup: source runtime is missing cbm-integrations.json"
shutil.copy2(integration, os.path.join(runtime_dir, "cbm-integrations.json"))
packs = []
for name in os.listdir(source_dir):
if name.startswith("cbm-ui-") and name.endswith(".pack"):
source = os.path.join(source_dir, name)
if os.path.isfile(source):
shutil.copy2(source, os.path.join(runtime_dir, name))
packs.append(name)
env = os.environ.copy()
env.pop("CBM_ASSETS_DIR", None)
env.pop("CBM_UI_ASSETS_DIR", None)
env["HOME"] = os.path.join(work, "runtime_home")
env["USERPROFILE"] = env["HOME"]
env["APPDATA"] = os.path.join(work, "runtime_appdata")
env["LOCALAPPDATA"] = os.path.join(work, "runtime_localappdata")
env["XDG_CONFIG_HOME"] = os.path.join(work, "runtime_xdg_config")
env["XDG_CACHE_HOME"] = os.path.join(work, "runtime_xdg_cache")
env["XDG_DATA_HOME"] = os.path.join(work, "runtime_xdg_data")
env["CBM_CACHE_DIR"] = os.path.join(work, "runtime_cache")
for inherited_config in (
"CLAUDE_CONFIG_DIR", "CODEX_HOME", "COPILOT_HOME",
"CRUSH_GLOBAL_CONFIG", "OPENCLAW_CONFIG_PATH", "OPENCLAW_HOME",
"OPENCLAW_PROFILE", "OPENCLAW_STATE_DIR", "OPENCLAW_WORKSPACE_DIR",
"OPENCODE_CONFIG", "OPENCODE_CONFIG_DIR", "VIBE_HOME"):
env.pop(inherited_config, None)
for directory in (
env["HOME"], env["APPDATA"], env["LOCALAPPDATA"],
env["XDG_CONFIG_HOME"], env["XDG_CACHE_HOME"],
env["XDG_DATA_HOME"], env["CBM_CACHE_DIR"]):
os.makedirs(directory, exist_ok=True)
def stop_runtime_daemon():
try:
first = run_product(
[relocated, "daemon", "stop"], runtime_dir, env, timeout=30)
second = run_product(
[relocated, "daemon", "stop"], runtime_dir, env, timeout=30)
return first.returncode == 0 and second.returncode == 0
except (OSError, subprocess.TimeoutExpired):
return False
def fail(message):
stop_runtime_daemon()
return message
probe = run_product([relocated, "--verify-runtime-assets"], runtime_dir, env)
if probe.returncode != 0:
return fail("runtime probe rc=%d output=%r packs=%r" % (
probe.returncode, probe.stdout[-800:], packs))
# Keep every component below the per-component limit while making the
# complete UTF-16 path decisively longer than legacy MAX_PATH. The product,
# not Python, creates this tree during the first real install.
target = os.path.join(
work,
"install_Ωμέγα",
*("segment_%02d_%s" % (i, "x" * 32) for i in range(7)),
"bin",
)
installed = os.path.join(target, "codebase-memory-mcp.exe")
if len(os.path.abspath(installed)) <= 260:
return fail("setup: intended long install path is only %d characters" % len(
os.path.abspath(installed)))
first_install = run_product(
[relocated, "install", "--force", "--skip-config", "--yes",
"--dir=" + target],
runtime_dir,
env,
)
if first_install.returncode != 0:
return fail("long-path install rc=%d output=%r path_len=%d" % (
first_install.returncode,
first_install.stdout[-1200:],
len(os.path.abspath(installed)),
))
installed_extended = windows_extended_path(installed)
if not os.path.isfile(installed_extended):
return fail("long-path install did not publish %r" % installed)
integration_installed = os.path.join(target, "cbm-integrations.json")
if not os.path.isfile(windows_extended_path(integration_installed)):
return fail("long-path install omitted cbm-integrations.json")
for name in packs:
if not os.path.isfile(windows_extended_path(os.path.join(target, name))):
return fail("long-path install omitted UI pack %s" % name)
installed_probe = run_product(
[installed_extended, "--verify-runtime-assets"], runtime_dir, env)
if installed_probe.returncode != 0:
return fail("installed long-path probe rc=%d output=%r" % (
installed_probe.returncode, installed_probe.stdout[-1200:]))
# Preserve the file identity/owner/DACL created by the native transaction,
# but make the bytes visibly foreign. A UTF-8-aware existence check sees
# this target and honors --no; the legacy narrow stat() treated it as absent
# and silently replaced it.
sentinel = b"cbm-long-path-existing-target-must-be-preserved\n"
with open(installed_extended, "wb") as stream:
stream.write(sentinel)
keep = run_product(
[relocated, "install", "--skip-config", "--no", "--dir=" + target],
runtime_dir,
env,
)
if keep.returncode != 0:
return fail("long-path non-force install rc=%d output=%r" % (
keep.returncode, keep.stdout[-1200:]))
with open(installed_extended, "rb") as stream:
retained = stream.read()
if retained != sentinel:
return fail("long-path existing target was overwritten despite --no "
"(target existence probe is not extended-path safe)")
restore = run_product(
[relocated, "install", "--force", "--skip-config", "--yes",
"--dir=" + target],
runtime_dir,
env,
)
if restore.returncode != 0:
return fail("long-path restore install rc=%d output=%r" % (
restore.returncode, restore.stdout[-1200:]))
restored_probe = run_product(
[installed_extended, "--verify-runtime-assets"], runtime_dir, env)
if restored_probe.returncode != 0:
return fail("restored long-path probe rc=%d output=%r" % (
restored_probe.returncode, restored_probe.stdout[-1200:]))
if not stop_runtime_daemon():
return fail("could not retire the isolated daemon before uninstall")
uninstall = run_product(
[installed_extended, "uninstall", "--yes", "--dir=" + target],
runtime_dir,
env,
)
if uninstall.returncode != 0:
return fail("long-path uninstall rc=%d output=%r" % (
uninstall.returncode, uninstall.stdout[-1200:]))
if os.path.exists(installed_extended):
return fail("long-path uninstall retained the installed executable")
if os.path.exists(windows_extended_path(integration_installed)):
return fail("long-path uninstall retained owned integration assets")
for name in packs:
if os.path.exists(windows_extended_path(os.path.join(target, name))):
return fail("long-path uninstall retained owned UI pack %s" % name)
stop_runtime_daemon()
return None
def make_fixture(root):
src = os.path.join(root, "src")
os.makedirs(src, exist_ok=True)
@@ -318,16 +119,7 @@ def main():
work = tempfile.mkdtemp(prefix="cbm_win_nonascii_")
failures = []
runtime_error = None
try:
runtime_error = verify_relocated_runtime(binary, work)
if runtime_error:
print("[FAIL] non-ASCII runtime relocation: %s" % runtime_error)
print("\nREGRESSION (red): Unicode/extended-length runtime publication "
"or exact adjacent-asset resolution failed.")
return 1
print("[PASS] Unicode relocation + extended-length install/probe/uninstall")
ascii_repo = os.path.join(work, "ascii_repo")
make_fixture(ascii_repo)
base = index_and_count(binary, ascii_repo, os.path.join(work, "c_ascii"))
@@ -360,15 +152,14 @@ def main():
shutil.rmtree(work, ignore_errors=True)
if failures:
print("\nREGRESSION (red): %d/%d non-ASCII repo path variants lost "
print("\nREGRESSION (red): %d/%d non-ASCII path variants lost "
"definitions: %s" %
(len(failures), len(NON_ASCII_SEGMENTS), ", ".join(failures)))
print("Invariant violated: byte-identical fixtures under non-ASCII paths "
"must extract the same definitions as the ASCII baseline (fixed by "
"#700 — has the cbm_fopen routing in the pass readers regressed?).")
return 1
print("\nGREEN: Unicode/extended-length runtime operations passed and all "
"non-ASCII repo variants matched the ASCII baseline.")
print("\nGREEN: all non-ASCII path variants matched the ASCII baseline.")
return 0