Merge pull request #1508 from DeusData/revert/recentralize-runtime-assets

Re-embed runtime assets, ship one archive set, and tolerate a single Microsoft !ml verdict
This commit is contained in:
Martin Vogel
2026-08-10 05:51:39 +02:00
committed by GitHub
78 changed files with 3007 additions and 11546 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:
+1 -3
View File
@@ -151,11 +151,9 @@ jobs:
- name: Extract archive artifacts
run: |
scripts/ci/extract-release-archives.sh "$RUNNER_TEMP/release-archives" binaries \
--archive-scope=ui \
--expect-archives=8 \
--expect-binaries=8 \
--expect-packs=8 \
--expect-runtime-files=32
--expect-runtime-files=24
- name: VirusTotal scan
uses: crazy-max/ghaction-virustotal@936d8c5c00afe97d3d9a1af26d017cfdf26800a2 # v5.0.0
+1 -3
View File
@@ -216,11 +216,9 @@ jobs:
mkdir -p "$ARCHIVE_DIR" binaries
gh release download "$VERSION" --dir "$ARCHIVE_DIR" --repo "$GITHUB_REPOSITORY" --pattern '*.tar.gz' --pattern '*.zip'
scripts/ci/extract-release-archives.sh "$ARCHIVE_DIR" binaries \
--archive-scope=ui \
--expect-archives=8 \
--expect-binaries=8 \
--expect-packs=8 \
--expect-runtime-files=32
--expect-runtime-files=24
- name: Security audits on all unique extracted release objects
run: |
+3
View File
@@ -48,6 +48,9 @@ private/
build/
node_modules/
graph-ui/dist/
# Generated by scripts/embed-frontend.sh into the source tree (the linker needs
# it beside the other UI sources). Removed by scripts/clean.sh; never committed.
src/ui/embedded_assets.c
# Generated reports
BENCHMARK_REPORT.md
+32 -54
View File
@@ -162,10 +162,18 @@ endif
# link), this flag fixes the OUTCOME, and the composition gate proves it on the
# shipped artifact. ELF-only: Apple's ld rejects -z noexecstack outright and it
# is meaningless for PE, so it is gated rather than made "common".
#
# -z separate-code additionally keeps read-only DATA out of the executable
# segment. GNU ld enables it by default on x86-64 but NOT on aarch64, so the
# arm64 binaries emitted ONE R E PT_LOAD spanning the whole image: .rodata is
# correctly marked A (not AX), but the kernel applies SEGMENT permissions, so
# ~259 MB of tree-sitter parse tables was mapped executable at runtime while the
# amd64 build of the same source mapped them R only. That is a large ROP gadget
# surface for data that is never executed, and it is invisible to section flags.
ELF_HARDENING_FLAGS :=
ifeq ($(IS_LINUX),yes)
ifneq ($(IS_MINGW),yes)
ELF_HARDENING_FLAGS := -Wl,-z,noexecstack
ELF_HARDENING_FLAGS := -Wl,-z,noexecstack -Wl,-z,separate-code
endif
endif
@@ -358,19 +366,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 +464,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 +481,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 +696,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 +717,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 +860,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 +888,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 +948,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 +1037,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 +1092,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 \
+26 -27
View File
@@ -25,7 +25,7 @@ High-quality parsing through [tree-sitter](https://tree-sitter.github.io/tree-si
<p align="center">
<img src="docs/graph-ui-screenshot.png" alt="Graph visualization UI showing the codebase-memory-mcp knowledge graph" width="800">
<br>
<em>Built-in 3D graph visualization (UI variant) — explore your knowledge graph at localhost:9749</em>
<em>Built-in 3D graph visualization — explore your knowledge graph at localhost:9749</em>
</p>
## Why codebase-memory-mcp
@@ -35,7 +35,7 @@ High-quality parsing through [tree-sitter](https://tree-sitter.github.io/tree-si
- **158 languages** — vendored tree-sitter grammars compiled into the binary. Nothing to install, nothing that breaks.
- **120x fewer tokens** — 5 structural queries: ~3,400 tokens vs ~412,000 via file-by-file search. One graph query replaces dozens of grep/read cycles.
- **43 supported automatic/conditional client surfaces** — `install` configures detected clients and safely activates conditional clients only when their documented platform, marker, or explicit existing config path is present. See [Multi-Agent Support](#multi-agent-support) for the complete matrix and manual/UI-only boundaries.
- **Built-in graph visualization** — 3D interactive UI at `localhost:9749` (optional UI binary variant).
- **Built-in graph visualization** — 3D interactive UI at `localhost:9749`, served from the binary itself.
- **Infrastructure-as-code indexing** — Dockerfiles, Kubernetes manifests, and Kustomize overlays indexed as graph nodes with cross-references. `Resource` nodes for K8s kinds, `Module` nodes for Kustomize overlays with `IMPORTS` edges to referenced resources.
- **15 MCP tools** — search, trace, architecture, impact analysis, targeted index-coverage checks, Cypher queries, dead code detection, cross-service HTTP linking, ADR management, and more.
@@ -48,7 +48,7 @@ curl -fsSL https://raw.githubusercontent.com/DeusData/codebase-memory-mcp/main/i
With graph visualization UI:
```bash
curl -fsSL https://raw.githubusercontent.com/DeusData/codebase-memory-mcp/main/install.sh | bash -s -- --ui
curl -fsSL https://raw.githubusercontent.com/DeusData/codebase-memory-mcp/main/install.sh | bash
```
**Windows** (PowerShell):
@@ -69,7 +69,15 @@ Unblock-File .\install.ps1
> **Note:** If you see a script execution policy error, run `Set-ExecutionPolicy -Scope Process Bypass` first, or invoke with `PowerShell -ExecutionPolicy Bypass -File .\install.ps1`.
Options: `--ui` (graph visualization), `--skip-config` (binary only, no agent setup), `--dir=<path>` (custom location).
Options: `--skip-config` (binary only, no agent setup), `--dir=<path>` (custom location).
> **Antivirus note:** Microsoft Defender may flag a release binary as
> `Trojan:Script/Wacatac.B!ml`. This is a known false positive — typically 61 of
> ~62 engines return clean, and the same detection family hits `gh`, llama.cpp,
> Godot and Microsoft's own Go toolchain. See
> [Antivirus False Positives](SECURITY.md#antivirus-false-positives) for the
> evidence, how to verify the artifacts yourself, and how to report it if you
> think we are wrong.
Restart your coding agent. Say **"Index this project"** — done.
@@ -77,8 +85,7 @@ Restart your coding agent. Say **"Index this project"** — done.
<summary>Manual install</summary>
1. **Download** the archive for your platform from the [latest release](https://github.com/DeusData/codebase-memory-mcp/releases/latest):
- `codebase-memory-mcp-<os>-<arch>.tar.gz` (macOS/Linux) or `.zip` (Windows) — standard
- `codebase-memory-mcp-ui-<os>-<arch>.tar.gz` / `.zip` — with graph visualization
- `codebase-memory-mcp-<os>-<arch>.tar.gz` (macOS/Linux) or `.zip` (Windows)
2. **Extract and install** (each archive includes `install.sh` or `install.ps1`):
@@ -126,15 +133,7 @@ The ordinary `cli` mode is intentionally separate: it runs one command locally a
### Graph Visualization UI
The UI currently ships as a separate `ui` build whose executable embeds only a content-addressed manifest; the deterministic frontend pack is a verified sidecar beside it. The default install on every channel is the lean, headless server; opt into the UI build with:
- **install.sh:** add `--ui` (see [Quick Start](#quick-start))
- **npm:** `CBM_VARIANT=ui npm install -g codebase-memory-mcp`
- **PyPI:** `CBM_VARIANT=ui pip install codebase-memory-mcp`
- **Go:** after `go install github.com/DeusData/codebase-memory-mcp/pkg/go/cmd/codebase-memory-mcp@latest`, run the wrapper with `CBM_VARIANT=ui`
- **Manual:** download the `codebase-memory-mcp-ui-<os>-<arch>` archive
Then run it:
The graph UI is built into the binary — every install on every channel has it. Then run it:
```bash
codebase-memory-mcp --ui=true --port=9749
@@ -311,15 +310,15 @@ When you open a memory/performance issue, **attach the `.ndjson` trajectory**
### Pre-built Binaries
| Platform | Standard | With Graph UI |
|----------|----------|---------------|
| macOS (Apple Silicon) | `codebase-memory-mcp-darwin-arm64.tar.gz` | `codebase-memory-mcp-ui-darwin-arm64.tar.gz` |
| macOS (Intel) | `codebase-memory-mcp-darwin-amd64.tar.gz` | `codebase-memory-mcp-ui-darwin-amd64.tar.gz` |
| Linux (x86_64) | `codebase-memory-mcp-linux-amd64.tar.gz` | `codebase-memory-mcp-ui-linux-amd64.tar.gz` |
| Linux (ARM64) | `codebase-memory-mcp-linux-arm64.tar.gz` | `codebase-memory-mcp-ui-linux-arm64.tar.gz` |
| Windows (x86_64) | `codebase-memory-mcp-windows-amd64.zip` | `codebase-memory-mcp-ui-windows-amd64.zip` |
| Platform | Archive |
|----------|---------|
| macOS (Apple Silicon) | `codebase-memory-mcp-darwin-arm64.tar.gz` |
| macOS (Intel) | `codebase-memory-mcp-darwin-amd64.tar.gz` |
| Linux (x86_64) | `codebase-memory-mcp-linux-amd64.tar.gz` |
| Linux (ARM64) | `codebase-memory-mcp-linux-arm64.tar.gz` |
| Windows (x86_64) | `codebase-memory-mcp-windows-amd64.zip` |
Every release includes `checksums.txt` with SHA-256 hashes. Keep the native executable together with the authenticated `cbm-integrations.json` asset and, for the UI variant, its single content-addressed `cbm-ui-<sha256>.pack`. Linux `-portable` archives contain the fully static builds; ordinary platform archives use their native system ABI.
Every release includes `checksums.txt` with SHA-256 hashes. The executable is self-contained — no adjacent data file is required. Linux `-portable` archives contain the fully static builds; ordinary platform archives use their native system ABI.
> **Windows note**: SmartScreen may show a warning for unsigned software. Click **"More info"** → **"Run anyway"**. Verify integrity with `checksums.txt`.
@@ -377,12 +376,12 @@ You: "Install this MCP server: https://github.com/DeusData/codebase-memory-mcp"
```bash
git clone https://github.com/DeusData/codebase-memory-mcp.git
cd codebase-memory-mcp
scripts/build.sh # standard binary
scripts/build.sh --with-ui # with graph visualization
scripts/build.sh --with-ui # the shipped composition (graph UI embedded)
scripts/build.sh # without the UI (development only)
# Binary at: build/c/codebase-memory-mcp (codebase-memory-mcp.exe on Windows)
```
Every platform builds a **verified runtime set**: the native executable, `cbm-integrations.json`, and—when `--with-ui` is selected—exactly one content-addressed `cbm-ui-<sha256>.pack`. These files are authenticated and published together; do not separate the executable from its sidecars.
Every platform ships **one self-contained executable**: the graph UI and the agent integration templates are linked into the binary, so an extracted archive is immediately complete.
Run the test suite (6,768 tests across 120 suites):
@@ -724,7 +723,7 @@ SQLite databases stored at `~/.cache/codebase-memory-mcp/`. Persists across rest
| `trace_path` returns 0 results | Use `search_graph(name_pattern=".*PartialName.*")` first to find the exact name. |
| Queries return wrong project results | Add `project="name"` parameter. Use `list_projects` to see names. |
| Binary not found after install | Add to PATH: `export PATH="$HOME/.local/bin:$PATH"` |
| UI not loading | Ensure you downloaded the `ui` variant and ran `--ui=true`. Check `http://localhost:9749`. |
| UI not loading | Ensure you ran `--ui=true`. Check `http://localhost:9749`. |
## Hybrid LSP
+115 -3
View File
@@ -148,9 +148,8 @@ is no exception path in this release gate.
- **CORS locked to localhost** — graph UI only accessible from localhost origins
- **Path containment** — `realpath()` check prevents reading files outside project root
- **Process-kill restriction** — only server-spawned PIDs can be terminated
- **Release-set verification** — installers verify downloaded archives, then the
candidate executable authenticates its adjacent runtime assets before
activation
- **Release-set verification** — installers verify the downloaded archive's exact
member set and SHA-256 before the candidate executable is activated
### Verification
@@ -172,6 +171,119 @@ sha256sum -c checksums.txt
# https://www.virustotal.com/
```
## Antivirus False Positives
Some release binaries are reported by **one** engine — Microsoft — as
`Trojan:Script/Wacatac.B!ml`. We believe this is a false positive, we do not hide
it, and this section exists so you can check that judgement yourself rather than
take our word for it.
### What the detection is
The `!ml` suffix marks a **machine-learning / heuristic** classification, not a
signature match. The `Script` token is a generic bucket in that naming scheme and
says nothing about script content — GitHub's own `gh` CLI and Anthropic's Claude
installer have both carried `Trojan:Script/Wacatac.H!ml` on native binaries.
Typically 61 of ~62 engines on VirusTotal return clean for the same file.
### What we measured
We dissected a full release matrix built from one commit. The verdicts split
across every axis at once:
| Binary | Link | Verdict |
|---|---|---|
| linux-amd64 | dynamic | flagged |
| linux-amd64-portable | static | clean |
| linux-arm64 | dynamic | clean |
| linux-arm64-portable | static | flagged |
| darwin-amd64 | — | flagged |
| darwin-arm64 | — | clean |
| windows amd64 / arm64 | — | clean |
The static/dynamic axis **inverts** between architectures, so no build or link
property explains it. The two macOS binaries have identical segment structure and
still split. Sibling artifacts from one build landed in *different* variant
buckets (`.B` vs `.C`).
We also tested and rejected the obvious structural hypothesis: entropy is low
everywhere (embedded vectors 4.17, parse tables 3.46 bits/byte, against 7.5–8.0
for genuinely packed payloads), so the binary does not resemble a packed dropper.
### What we changed, and what we reverted
We removed every embedded shell script from the binary and moved the UI bundle
and the agent integration templates out into separate verified files. **The
detection count did not drop** — it simply moved between artifacts. We reverted
both changes rather than keep permanent complexity that bought nothing. We are
documenting that here because a negative result is still evidence.
### This is endemic, not specific to us
The same `!ml` family repeatedly hits large, unsigned, native open-source
binaries:
- [llama.cpp #15874](https://github.com/ggml-org/llama.cpp/issues/15874),
[#24487](https://github.com/ggml-org/llama.cpp/issues/24487),
[#24558](https://github.com/ggml-org/llama.cpp/issues/24558) — including one
DLL of many in a single archive, with nothing found on reverse engineering
- [GitHub CLI #13306](https://github.com/cli/cli/issues/13306)
- [Microsoft's own Go toolchain #1255](https://github.com/microsoft/go/issues/1255)
- [Anthropic Claude Code #36796](https://github.com/anthropics/claude-code/issues/36796)
- [yt-dlp #7532](https://github.com/yt-dlp/yt-dlp/issues/7532),
[Godot #110612](https://github.com/godotengine/godot/issues/110612),
[PyInstaller #5854](https://github.com/pyinstaller/pyinstaller/issues/5854),
[Tauri #2486](https://github.com/tauri-apps/tauri/issues/2486),
[OpenAI Codex #2228](https://github.com/openai/codex/issues/2228),
[rust-lang/rust #88297](https://github.com/rust-lang/rust/issues/88297)
A Microsoft engineer on the Go team [put it plainly](https://github.com/microsoft/go/issues/1255):
*"we're aware of Windows Security/Defender issues with Go apps… we can't exactly
go fix something and solve all Go false positives."*
### Why we do not obfuscate around it
Deliberately reshaping a binary to avoid a classifier is what malware does, and
it measurably backfires — the same Microsoft engineer reports that obfuscation
*"increases scrutiny rather than avoiding it."* We would rather be scannable and
explain a false positive than be unreadable and score well.
### Verify it yourself
Every release ships the material needed to check our artifacts independently.
Use the commands in [Verification](#verification) above: SLSA Build Level 3
provenance ties the archive to the workflow run that produced it, Sigstore cosign
verifies the signature, and `checksums.txt` pins the bytes. The release notes
carry a durable per-object VirusTotal link for every scanned file, including any
tolerated detection — we publish those results whether or not they are clean.
You can also rebuild from source and compare: `scripts/build.sh --with-ui`
produces the shipped composition.
### Our release policy
A release may ship with **at most one** detection, and only when the engine is
Microsoft and the label ends in `!ml`. Two or more engines, any signature-based
label, any other vendor, or any "suspicious" verdict blocks the release. That
rule is enforced in `scripts/ci/check-virustotal.sh` and pinned by
`tests/test_vt_gate_policy_contract.sh`.
### If you find something real
We would genuinely rather be wrong in public than confidently wrong. If you find
anything that explains or contradicts the assessment above:
- Open an issue with the **`av-analysis`** label — include the artifact SHA-256,
the engine and label, and what you found.
- If it looks like an actual compromise rather than a classifier artifact, use
the private process in [Reporting a Vulnerability](#reporting-a-vulnerability)
instead.
A concrete finding changes our position. Signing is on the roadmap and will help
on Windows, but note that no code-signing scheme exists that AV engines honour
for Linux ELF binaries, so it is not a complete answer either.
## Supported Versions
| Version | Supported |
+7 -11
View File
@@ -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"]
+49 -171
View File
@@ -35,10 +35,9 @@ import (
)
const (
repo = "DeusData/codebase-memory-mcp"
version = "0.8.1"
windowsBinaryName = "codebase-memory-mcp.exe"
integrationsFileName = "cbm-integrations.json"
repo = "DeusData/codebase-memory-mcp"
version = "0.8.1"
windowsBinaryName = "codebase-memory-mcp.exe"
maxRedirects = 5
requestTimeout = 2 * time.Minute
@@ -154,7 +153,7 @@ func main() {
func ensureBinary() (string, error) {
binary := binPath()
ready, err := runtimeSetReadyLocked(
filepath.Dir(binary), filepath.Base(binary), runtimeVariant(), verifyCandidate,
filepath.Dir(binary), filepath.Base(binary), verifyCandidate,
)
if err != nil {
return "", err
@@ -170,7 +169,7 @@ func ensureBinary() (string, error) {
func binPath() string {
return filepath.Join(
cacheDir(), version, runtimeVariant(), binaryNameForOS(runtime.GOOS),
cacheDir(), version, binaryNameForOS(runtime.GOOS),
)
}
@@ -185,13 +184,6 @@ func executionPathForOS(binary, targetOS string) string {
return binary
}
func runtimeVariant() string {
if strings.EqualFold(os.Getenv("CBM_VARIANT"), "ui") {
return "ui"
}
return "standard"
}
func printPortableMutationGuidance(args []string) {
action := runtimeMutationAction(args)
if action != "uninstall" {
@@ -361,7 +353,6 @@ func archiveNamesForOS(platform, binaryName string) []string {
}
return []string{
binaryName,
integrationsFileName,
"LICENSE",
installer,
"THIRD_PARTY_NOTICES.md",
@@ -371,7 +362,6 @@ func archiveNamesForOS(platform, binaryName string) []string {
func download(dest string) error {
platform := goos()
arch := goarch()
selectedVariant := runtimeVariant()
ext := "tar.gz"
if platform == "windows" {
ext = "zip"
@@ -381,13 +371,9 @@ func download(dest string) error {
if platform == "linux" {
portable = "-portable"
}
uiPrefix := ""
if selectedVariant == "ui" {
uiPrefix = "ui-"
}
archive := fmt.Sprintf(
"codebase-memory-mcp-%s%s-%s%s.%s",
uiPrefix, platform, arch, portable, ext,
"codebase-memory-mcp-%s-%s%s.%s",
platform, arch, portable, ext,
)
url := fmt.Sprintf("https://github.com/%s/releases/download/v%s/%s", repo, version, archive)
checksumURL := fmt.Sprintf("https://github.com/%s/releases/download/v%s/checksums.txt", repo, version)
@@ -421,19 +407,19 @@ func download(dest string) error {
binName := binaryNameForOS(platform)
archiveNames := archiveNamesForOS(platform, binName)
extractNames := []string{binName, integrationsFileName}
extractNames := []string{binName}
var runtimeNames []string
if ext == "tar.gz" {
runtimeNames, err = extractTarGz(
archivePath, tmp, archiveNames, extractNames, selectedVariant,
archivePath, tmp, archiveNames, extractNames,
)
if err != nil {
return fmt.Errorf("extraction failed: %w", err)
}
} else {
runtimeNames, err = extractZip(
archivePath, tmp, archiveNames, extractNames, selectedVariant,
archivePath, tmp, archiveNames, extractNames,
)
if err != nil {
return fmt.Errorf("extraction failed: %w", err)
@@ -458,7 +444,7 @@ func download(dest string) error {
}
if err := publishRuntimeSetWithRecovery(
tmp, filepath.Dir(dest), binName, selectedVariant, verifyCandidate,
tmp, filepath.Dir(dest), binName, verifyCandidate,
); err != nil {
return fmt.Errorf("could not install runtime set: %w", err)
}
@@ -611,81 +597,41 @@ func verifyChecksum(path, expected string) error {
return nil
}
func isUIPackName(name string) bool {
const prefix = "cbm-ui-"
const suffix = ".pack"
if len(name) != len(prefix)+sha256.Size*2+len(suffix) ||
!strings.HasPrefix(name, prefix) || !strings.HasSuffix(name, suffix) {
return false
}
digest := strings.TrimSuffix(strings.TrimPrefix(name, prefix), suffix)
if digest != strings.ToLower(digest) {
return false
}
decoded, err := hex.DecodeString(digest)
return err == nil && len(decoded) == sha256.Size
}
func validateArchiveMemberNames(
names, archiveNames []string, variant string, caseInsensitive bool,
) (string, error) {
names, archiveNames []string, caseInsensitive bool,
) error {
required := make(map[string]struct{}, len(archiveNames))
for _, name := range archiveNames {
required[name] = struct{}{}
}
seen := make(map[string]struct{}, len(names))
found := make(map[string]struct{}, len(archiveNames))
uiPack := ""
for _, name := range names {
key := name
if caseInsensitive {
key = strings.ToLower(name)
}
if _, exists := seen[key]; exists {
return "", fmt.Errorf(
return fmt.Errorf(
"duplicate or case-conflicting archive entry: %q", name,
)
}
seen[key] = struct{}{}
_, isRequired := required[name]
isPack := variant == "ui" && isUIPackName(name)
if !isRequired && !isPack {
return "", fmt.Errorf(
if _, isRequired := required[name]; !isRequired {
return fmt.Errorf(
"archive must contain only the exact root files: %s",
strings.Join(archiveNames, ", "),
)
}
if isPack {
if uiPack != "" {
return "", fmt.Errorf(
"archive must contain exactly one content-addressed UI pack",
)
}
uiPack = name
} else {
found[name] = struct{}{}
}
found[name] = struct{}{}
}
expectedCount := len(archiveNames)
if variant == "ui" {
expectedCount++
}
if len(seen) != expectedCount || len(found) != len(required) ||
(variant == "ui" && uiPack == "") {
return "", fmt.Errorf(
if len(seen) != len(archiveNames) || len(found) != len(required) {
return fmt.Errorf(
"archive must contain exactly one of each required root file: %s",
strings.Join(archiveNames, ", "),
)
}
return uiPack, nil
}
func runtimeArchiveNames(extractNames []string, uiPack string) []string {
result := append([]string(nil), extractNames...)
if uiPack != "" {
result = append(result, uiPack)
}
return result
return nil
}
func validateArchiveResourceLimits(limits archiveResourceLimits) error {
@@ -837,14 +783,12 @@ func tarGzMemberNamesWithLimits(
func extractTarGz(
archivePath, destDir string,
archiveNames, extractNames []string,
variant string,
) ([]string, error) {
return extractTarGzWithLimits(
archivePath,
destDir,
archiveNames,
extractNames,
variant,
defaultArchiveResourceLimits,
)
}
@@ -852,20 +796,16 @@ func extractTarGz(
func extractTarGzWithLimits(
archivePath, destDir string,
archiveNames, extractNames []string,
variant string,
limits archiveResourceLimits,
) ([]string, error) {
names, err := tarGzMemberNamesWithLimits(archivePath, limits)
if err != nil {
return nil, err
}
uiPack, err := validateArchiveMemberNames(
names, archiveNames, variant, false,
)
if err != nil {
if err := validateArchiveMemberNames(names, archiveNames, false); err != nil {
return nil, err
}
runtimeNames := runtimeArchiveNames(extractNames, uiPack)
runtimeNames := append([]string(nil), extractNames...)
targets := make(map[string]struct{}, len(runtimeNames))
for _, name := range runtimeNames {
targets[name] = struct{}{}
@@ -948,14 +888,12 @@ func extractTarGzWithLimits(
func extractZip(
archivePath, destDir string,
archiveNames, extractNames []string,
variant string,
) ([]string, error) {
return extractZipWithLimits(
archivePath,
destDir,
archiveNames,
extractNames,
variant,
defaultArchiveResourceLimits,
)
}
@@ -963,7 +901,6 @@ func extractZip(
func extractZipWithLimits(
archivePath, destDir string,
archiveNames, extractNames []string,
variant string,
limits archiveResourceLimits,
) ([]string, error) {
if err := validateArchiveResourceLimits(limits); err != nil {
@@ -1010,13 +947,10 @@ func extractZipWithLimits(
names = append(names, name)
declaredSizes[name] = declaredSize
}
uiPack, err := validateArchiveMemberNames(
names, archiveNames, variant, true,
)
if err != nil {
if err := validateArchiveMemberNames(names, archiveNames, true); err != nil {
return nil, err
}
runtimeNames := runtimeArchiveNames(extractNames, uiPack)
runtimeNames := append([]string(nil), extractNames...)
targetNames := make(map[string]struct{}, len(runtimeNames))
for _, name := range runtimeNames {
targetNames[name] = struct{}{}
@@ -1103,7 +1037,7 @@ func validateWindowsZipMember(raw string) (string, error) {
func verifyCandidate(path string) error {
ctx, cancel := context.WithTimeout(context.Background(), candidateTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, path, "--verify-runtime-assets")
cmd := exec.CommandContext(ctx, path, "--version")
cmd.Stdin = nil
cmd.Stdout = io.Discard
cmd.Stderr = io.Discard
@@ -1163,58 +1097,17 @@ func regularRuntimeFile(path string) bool {
platformRuntimeSetFileLinkCountOne(path, status)
}
func uiPackMatchesDigest(directory, name string) bool {
digest, err := fileSHA256(filepath.Join(directory, name))
if err != nil {
return false
}
expected := strings.TrimSuffix(strings.TrimPrefix(name, "cbm-ui-"), ".pack")
return hex.EncodeToString(digest[:]) == expected
}
func runtimeSetNames(
directory, binaryName, variant string,
) ([]string, bool) {
if !regularRuntimeFile(filepath.Join(directory, binaryName)) ||
!regularRuntimeFile(filepath.Join(directory, integrationsFileName)) {
func runtimeSetNames(directory, binaryName string) ([]string, bool) {
if !regularRuntimeFile(filepath.Join(directory, binaryName)) {
return nil, false
}
entries, err := os.ReadDir(directory)
if err != nil {
return nil, false
}
var packLike []string
var validPacks []string
for _, entry := range entries {
name := entry.Name()
if strings.HasPrefix(name, "cbm-ui-") && strings.HasSuffix(name, ".pack") {
packLike = append(packLike, name)
if isUIPackName(name) &&
regularRuntimeFile(filepath.Join(directory, name)) &&
uiPackMatchesDigest(directory, name) {
validPacks = append(validPacks, name)
}
}
}
if variant == "ui" {
if len(packLike) != 1 || len(validPacks) != 1 {
return nil, false
}
} else if len(packLike) != 0 {
return nil, false
}
names := []string{integrationsFileName}
if variant == "ui" {
names = append(names, validPacks[0])
}
names = append(names, binaryName)
return names, true
return []string{binaryName}, true
}
func runtimeSetReady(
directory, binaryName, variant string, verifier func(string) error,
directory, binaryName string, verifier func(string) error,
) bool {
if _, ok := runtimeSetNames(directory, binaryName, variant); !ok {
if _, ok := runtimeSetNames(directory, binaryName); !ok {
return false
}
return verifier == nil || verifier(filepath.Join(directory, binaryName)) == nil
@@ -1232,7 +1125,7 @@ func requireSafeRuntimeDirectory(directory string) error {
}
func runtimeSetReadyLocked(
directory, binaryName, variant string, verifier func(string) error,
directory, binaryName string, verifier func(string) error,
) (ready bool, result error) {
if err := os.MkdirAll(directory, 0755); err != nil {
return false, err
@@ -1251,11 +1144,11 @@ func runtimeSetReadyLocked(
return false, err
}
if err := reconcileRuntimeBackups(
directory, binaryName, variant, verifier, lock,
directory, binaryName, verifier, lock,
); err != nil {
return false, err
}
return runtimeSetReady(directory, binaryName, variant, verifier), nil
return runtimeSetReady(directory, binaryName, verifier), nil
}
func copyRuntimeStage(
@@ -1338,8 +1231,7 @@ func runtimeBackupDirectoryName(name string) bool {
}
func runtimeBackupTargetName(name, binaryName string) bool {
return name == binaryName || name == integrationsFileName ||
(strings.HasPrefix(name, "cbm-ui-") && strings.HasSuffix(name, ".pack"))
return name == binaryName
}
func createRuntimeBackupDirectory(directory string) (string, error) {
@@ -1567,7 +1459,7 @@ func copyRuntimeBackupFile(
}
func reconcileRuntimeBackups(
directory, binaryName, variant string,
directory, binaryName string,
verifier func(string) error,
lock *runtimeSetLock,
) error {
@@ -1592,7 +1484,7 @@ func reconcileRuntimeBackups(
if len(backups) == 0 {
return nil
}
if runtimeSetReady(directory, binaryName, variant, verifier) {
if runtimeSetReady(directory, binaryName, verifier) {
for _, backup := range backups {
if err := cleanupRuntimeBackup(backup, lock); err != nil {
return err
@@ -2051,11 +1943,11 @@ func attachRuntimeLockReleaseError(result *error, releaseErr error) {
}
func publishRuntimeSetWithRecoveryAndRenamer(
sourceDirectory, destinationDirectory, binaryName, variant string,
sourceDirectory, destinationDirectory, binaryName string,
verifier func(string) error,
renameFile func(string, string) error,
) (result error) {
sourceNames, ok := runtimeSetNames(sourceDirectory, binaryName, variant)
sourceNames, ok := runtimeSetNames(sourceDirectory, binaryName)
if !ok {
return fmt.Errorf("source release does not contain a complete runtime set")
}
@@ -2081,13 +1973,13 @@ func publishRuntimeSetWithRecoveryAndRenamer(
return err
}
if err := reconcileRuntimeBackups(
destinationDirectory, binaryName, variant, verifier, lock,
destinationDirectory, binaryName, verifier, lock,
); err != nil {
return err
}
// A contender may have committed while this process waited. Its complete
// runtime set wins and is never retired or mixed with this contender.
if runtimeSetReady(destinationDirectory, binaryName, variant, verifier) {
if runtimeSetReady(destinationDirectory, binaryName, verifier) {
return nil
}
@@ -2121,17 +2013,7 @@ func publishRuntimeSetWithRecoveryAndRenamer(
return err
}
entries, err := os.ReadDir(destinationDirectory)
if err != nil {
return err
}
retireNames := []string{binaryName, integrationsFileName}
for _, entry := range entries {
name := entry.Name()
if strings.HasPrefix(name, "cbm-ui-") && strings.HasSuffix(name, ".pack") {
retireNames = append(retireNames, name)
}
}
retireNames := []string{binaryName}
retired := make(map[string]struct{}, len(retireNames))
// Retire the executable first so a partial publication is never ready.
for _, name := range retireNames {
@@ -2190,7 +2072,7 @@ func publishRuntimeSetWithRecoveryAndRenamer(
}
delete(staged, name)
}
if !runtimeSetReady(destinationDirectory, binaryName, variant, verifier) {
if !runtimeSetReady(destinationDirectory, binaryName, verifier) {
return fmt.Errorf("published package-cache runtime set failed verification")
}
if err := assertRuntimeSetLockOwner(lock); err != nil {
@@ -2217,11 +2099,11 @@ func publishRuntimeSetWithRecoveryAndRenamer(
return nil
}
preserveWinner := runtimeSetReady(
destinationDirectory, binaryName, variant, verifier,
destinationDirectory, binaryName, verifier,
)
if backupDirectory != "" {
if recoveryErr := reconcileRuntimeBackups(
destinationDirectory, binaryName, variant, verifier, lock,
destinationDirectory, binaryName, verifier, lock,
); recoveryErr != nil {
return fmt.Errorf(
"%v; package-cache recovery failed: %w", operationErr, recoveryErr,
@@ -2235,14 +2117,13 @@ func publishRuntimeSetWithRecoveryAndRenamer(
}
func publishRuntimeSetWithRecovery(
sourceDirectory, destinationDirectory, binaryName, variant string,
sourceDirectory, destinationDirectory, binaryName string,
verifier func(string) error,
) error {
return publishRuntimeSetWithRecoveryAndRenamer(
sourceDirectory,
destinationDirectory,
binaryName,
variant,
verifier,
os.Rename,
)
@@ -2260,13 +2141,13 @@ func execBinary(executable string, args []string) error {
}
func createMutationRuntimeSnapshot(
executable, variant string,
executable string,
verifier func(string) error,
maintainLease func() error,
) (snapshotDirectory, snapshotExecutable string, result error) {
sourceDirectory := filepath.Dir(executable)
binaryName := filepath.Base(executable)
names, ok := runtimeSetNames(sourceDirectory, binaryName, variant)
names, ok := runtimeSetNames(sourceDirectory, binaryName)
if !ok {
return "", "", fmt.Errorf(
"cached runtime assets changed before mutation snapshot",
@@ -2323,7 +2204,7 @@ func createMutationRuntimeSnapshot(
snapshotExecutable = filepath.Join(snapshotDirectory, binaryName)
if !runtimeSetReady(
snapshotDirectory, binaryName, variant, verifier,
snapshotDirectory, binaryName, verifier,
) {
return "", "", fmt.Errorf(
"private mutation runtime snapshot failed verification",
@@ -2344,7 +2225,6 @@ func runMutationBinary(executable string, args []string) error {
func execBinaryWithRuntimeLockAndRunner(
executable string,
args []string,
variant string,
verifier func(string) error,
runner func(string, []string) error,
) (result error) {
@@ -2376,13 +2256,12 @@ func execBinaryWithRuntimeLockAndRunner(
return err
}
if !runtimeSetReady(
directory, filepath.Base(executable), variant, verifier,
directory, filepath.Base(executable), verifier,
) {
return fmt.Errorf("cached runtime assets changed before mutation launch")
}
snapshotDirectory, snapshotExecutable, err := createMutationRuntimeSnapshot(
executable,
variant,
verifier,
func() error { return refreshRuntimeSetLock(lock) },
)
@@ -2414,7 +2293,6 @@ func execBinaryWithRuntimeLock(executable string, args []string) error {
return execBinaryWithRuntimeLockAndRunner(
executable,
args,
runtimeVariant(),
verifyCandidate,
runMutationBinary,
)
File diff suppressed because it is too large Load Diff
Binary file not shown.
@@ -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
+37 -117
View File
@@ -4,34 +4,30 @@
// If the binary is missing (e.g. --ignore-scripts), attempts a one-time download.
const path = require('path');
const os = require('os');
const { spawn, spawnSync } = require('child_process');
const {
acquireRuntimeLock,
refreshRuntimeLock,
releaseRuntimeLock,
runtimeSetReady,
runtimeSetReadyLocked,
} = require('./install.js');
const fs = require('fs');
const { spawnSync } = require('child_process');
const isWindows = process.platform === 'win32';
const variant = (process.env.CBM_VARIANT || '').toLowerCase() === 'ui'
? 'ui'
: 'standard';
// npm is a portable one-shot wrapper. Every platform ships exactly one binary,
// which is executed directly beside its authenticated runtime sidecars.
// npm is a portable one-shot wrapper. Every platform ships exactly ONE binary,
// so the cached file is the executed file — there is no launcher pair to keep
// in sync and no adjacent payload to validate.
const binName = isWindows ? 'codebase-memory-mcp.exe' : 'codebase-memory-mcp';
const binDir = path.join(__dirname, 'bin', variant);
const binPath = path.join(binDir, binName);
const binPath = path.join(__dirname, 'bin', binName);
const executionPath = binPath;
const cacheReady = () => runtimeSetReadyLocked(binDir, binName, variant);
const cacheReady = () => {
if (!fs.existsSync(binPath)) {
return false;
}
if (!isWindows) return true;
// A cached Windows binary that cannot report its own version is not usable.
const probe = spawnSync(binPath, ['--version'], {
stdio: 'ignore',
timeout: 15_000,
windowsHide: true,
});
return !probe.error && probe.status === 0;
};
function main() {
const requestedMutation = runtimeMutationAction(process.argv.slice(2));
if (requestedMutation === 'update') {
printUpdateGuidance();
process.exit(2);
}
if (!cacheReady()) {
// Binary missing — try running the install script (handles --ignore-scripts case)
process.stderr.write('codebase-memory-mcp: binary not found, downloading...\n');
@@ -47,105 +43,17 @@ if (!cacheReady()) {
}
}
function runtimeMutationAction(args) {
function portableMutationAction(args) {
for (const argument of args) {
if (argument === '--help' || argument === '-h' || argument === '--version') {
if (['cli', 'hook-augment', 'config', 'install', '--help', '-h', '--version'].includes(argument)) {
return null;
}
if (argument.startsWith('-')) continue;
if (argument === 'install' || argument === 'update' || argument === 'uninstall') {
return argument;
}
if (argument === 'cli' || argument === 'hook-augment' || argument === 'config') {
return null;
}
return null;
if (argument === 'update' || argument === 'uninstall') return argument;
}
return null;
}
function defaultManagedInstallDir() {
if (!isWindows) return path.join(os.homedir(), '.local', 'bin');
const localAppData = process.env.LOCALAPPDATA ||
path.join(os.homedir(), 'AppData', 'Local');
return path.join(localAppData, 'Programs', 'codebase-memory-mcp');
}
function nativeArgs(args) {
const result = [...args];
if (runtimeMutationAction(result) === 'uninstall' &&
!result.some((argument) => argument === '--dir' || argument.startsWith('--dir='))) {
result.push('--dir', defaultManagedInstallDir());
}
return result;
}
function printUpdateGuidance() {
process.stderr.write(
'This npm copy is maintained by npm. Update it with ' +
'"npm install codebase-memory-mcp@latest" (add -g for a global install). ' +
'For a managed standalone installation, run ' +
'"codebase-memory-mcp install --yes".\n',
);
}
const forwardedArgs = nativeArgs(process.argv.slice(2));
const mutation = requestedMutation;
if (mutation === 'install' || mutation === 'uninstall') {
const lock = acquireRuntimeLock(binDir);
let heartbeatError = null;
try {
refreshRuntimeLock(lock);
if (!runtimeSetReady(binDir, binName, variant)) {
throw new Error('cached runtime assets changed before mutation launch');
}
} catch (error) {
try { releaseRuntimeLock(lock); } catch (_) { /* preserve first error */ }
process.stderr.write(`codebase-memory-mcp: ${error.message}\n`);
process.exit(1);
}
let child = null;
const heartbeat = setInterval(() => {
try {
refreshRuntimeLock(lock);
} catch (error) {
heartbeatError = error;
if (child) child.kill();
}
}, 20_000);
child = spawn(executionPath, forwardedArgs, {
stdio: 'inherit',
windowsHide: false,
});
child.on('error', (error) => {
clearInterval(heartbeat);
try { releaseRuntimeLock(lock); } catch (_) { /* report spawn error */ }
process.stderr.write(`codebase-memory-mcp: ${error.message}\n`);
process.exit(1);
});
child.on('close', (status) => {
clearInterval(heartbeat);
let releaseError = heartbeatError;
try { releaseRuntimeLock(lock); } catch (error) { releaseError ||= error; }
if (releaseError) {
process.stderr.write(`codebase-memory-mcp: ${releaseError.message}\n`);
process.exit(1);
}
if (isWindows && status !== 0 && mutation === 'uninstall') {
process.stderr.write(
'This npm Windows copy is portable. Use "npm uninstall codebase-memory-mcp" ' +
'for package maintenance (add -g for a global install). ' +
'To create or repair a managed standalone installation, run ' +
'"codebase-memory-mcp install --yes".\n',
);
}
process.exit(status ?? 0);
});
return;
}
const result = spawnSync(executionPath, forwardedArgs, {
const result = spawnSync(executionPath, process.argv.slice(2), {
stdio: 'inherit',
windowsHide: false,
});
@@ -155,7 +63,19 @@ if (result.error) {
process.exit(1);
}
process.exit(result.status ?? 0);
const mutation = isWindows && result.status !== 0
? portableMutationAction(process.argv.slice(2))
: null;
if (mutation) {
const packageCommand = mutation === 'update'
? 'npm install codebase-memory-mcp@latest'
: 'npm uninstall codebase-memory-mcp';
process.stderr.write(
`This npm Windows copy is portable. Use "${packageCommand}" for package ` +
'maintenance (add -g for a global install), or run ' +
'"codebase-memory-mcp install --yes" once for a managed install with its ' +
'own install.ps1 for updates.\n',
);
}
main();
process.exit(result.status ?? 0);
+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/);
});
+41 -159
View File
@@ -21,18 +21,14 @@ from pathlib import Path
REPO = "DeusData/codebase-memory-mcp"
_WINDOWS_BINARY_NAME = "codebase-memory-mcp.exe"
_INTEGRATIONS_NAME = "cbm-integrations.json"
_UI_PACK_RE = re.compile(r"cbm-ui-[0-9a-f]{64}\.pack\Z")
_UNIX_ARCHIVE_NAMES = (
"codebase-memory-mcp",
_INTEGRATIONS_NAME,
"LICENSE",
"install.sh",
"THIRD_PARTY_NOTICES.md",
)
_WINDOWS_ARCHIVE_NAMES = (
_WINDOWS_BINARY_NAME,
_INTEGRATIONS_NAME,
"LICENSE",
"install.ps1",
"THIRD_PARTY_NOTICES.md",
@@ -142,7 +138,7 @@ def _verify_candidate(path: Path) -> None:
"""Require a staged native candidate to execute successfully."""
try:
subprocess.run(
[str(path), "--verify-runtime-assets"],
[str(path), "--version"],
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
@@ -173,13 +169,12 @@ def _files_equal_sha256(left: Path, right: Path) -> bool:
return False
def _validate_archive_names(names, archive_names, ui: bool, casefold: bool = False):
"""Require the exact release root namespace and return its UI pack name."""
def _validate_archive_names(names, archive_names, casefold: bool = False):
"""Require the exact release root namespace."""
required = tuple(archive_names)
required_set = set(required)
found = set()
seen = set()
ui_pack = None
for name in names:
key = name.casefold() if casefold else name
if key in seen:
@@ -188,28 +183,17 @@ def _validate_archive_names(names, archive_names, ui: bool, casefold: bool = Fal
f"(duplicate or case conflict: {name!r})"
)
seen.add(key)
is_ui_pack = ui and _UI_PACK_RE.fullmatch(name) is not None
if name not in required_set and not is_ui_pack:
if name not in required_set:
sys.exit(
f"codebase-memory-mcp: archive must contain only the exact "
f"root files: {', '.join(required)}"
)
if is_ui_pack:
if ui_pack is not None:
sys.exit(
"codebase-memory-mcp: archive must contain exactly one "
"content-addressed UI pack"
)
ui_pack = name
else:
found.add(name)
expected_count = len(required) + (1 if ui else 0)
if found != required_set or len(seen) != expected_count or (ui and ui_pack is None):
found.add(name)
if found != required_set or len(seen) != len(required):
sys.exit(
f"codebase-memory-mcp: archive must contain exactly one of each "
f"required root file: {', '.join(required)}"
)
return ui_pack
def _safe_extract_tar(
@@ -223,12 +207,8 @@ def _safe_extract_tar(
f"codebase-memory-mcp: refusing unsafe tar entry "
f"(not a regular root file: {member.name!r})"
)
ui_pack = _validate_archive_names(
[member.name for member in members], archive_names, ui
)
_validate_archive_names([member.name for member in members], archive_names)
runtime_names = list(extract_names)
if ui_pack is not None:
runtime_names.append(ui_pack)
by_name = {member.name: member for member in members}
dest_abs = os.path.abspath(dest)
for name in runtime_names:
@@ -281,13 +261,11 @@ def _safe_extract_zip(
f"codebase-memory-mcp: refusing unsafe zip entry "
f"(escapes dest: {raw_name!r})"
)
ui_pack = _validate_archive_names(names, archive_names, ui, casefold=True)
_validate_archive_names(names, archive_names, casefold=True)
# Extract only the validated runtime files. This avoids relying on
# platform-specific zip path rewriting and always creates regular files.
runtime_names = list(extract_names)
if ui_pack is not None:
runtime_names.append(ui_pack)
for name in runtime_names:
target = os.path.join(dest_abs, name)
with zf.open(name) as source, open(target, "xb") as output:
@@ -393,22 +371,18 @@ def _cache_dir() -> Path:
return base / "codebase-memory-mcp"
def _variant() -> str:
return "ui" if os.environ.get("CBM_VARIANT", "").lower() == "ui" else "standard"
def _runtime_dir(version: str) -> Path:
return _cache_dir() / version
def _runtime_dir(version: str, variant: str = None) -> Path:
return _cache_dir() / version / (variant or _variant())
def _bin_path(version: str, variant: str = None) -> Path:
def _bin_path(version: str) -> Path:
# One binary per platform: the cached file is the executed file.
name = (
_WINDOWS_BINARY_NAME
if sys.platform == "win32"
else "codebase-memory-mcp"
)
return _runtime_dir(version, variant) / name
return _runtime_dir(version) / name
def _execution_path(binary: Path, target_platform: str) -> Path:
@@ -428,54 +402,15 @@ def _regular_file(path: Path) -> bool:
return False
def _ui_pack_path_matches_name(path: Path, name: str) -> bool:
try:
expected = name[len("cbm-ui-") : -len(".pack")]
return _file_sha256(path) == expected
except OSError:
return False
def _ui_pack_matches_digest(path: Path) -> bool:
return _ui_pack_path_matches_name(path, path.name)
def _runtime_set_names(directory: Path, binary_name: str, variant: str):
if not _regular_file(directory / binary_name) or not _regular_file(
directory / _INTEGRATIONS_NAME
):
def _runtime_set_names(directory: Path, binary_name: str):
if not _regular_file(directory / binary_name):
return None
try:
pack_like = [
path
for path in directory.iterdir()
if path.name.startswith("cbm-ui-") and path.name.endswith(".pack")
]
except OSError:
return None
valid_packs = [
path
for path in pack_like
if _UI_PACK_RE.fullmatch(path.name) is not None
and _regular_file(path)
and _ui_pack_matches_digest(path)
]
if variant == "ui":
if len(pack_like) != 1 or len(valid_packs) != 1:
return None
elif pack_like:
return None
return (
_INTEGRATIONS_NAME,
*((valid_packs[0].name,) if variant == "ui" else ()),
binary_name,
)
return (binary_name,)
def _runtime_set_ready(version: str, verifier=None) -> bool:
variant = _variant()
binary = _bin_path(version, variant)
if _runtime_set_names(binary.parent, binary.name, variant) is None:
binary = _bin_path(version)
if _runtime_set_names(binary.parent, binary.name) is None:
return False
if verifier is not None:
try:
@@ -499,16 +434,16 @@ def _runtime_set_ready_locked(version: str) -> bool:
try:
_refresh_runtime_lock(lock)
_reconcile_runtime_backups(
binary.parent, binary.name, _variant(), _verify_candidate, lock
binary.parent, binary.name, _verify_candidate, lock
)
return _runtime_set_ready(version, _verify_candidate)
finally:
_release_runtime_lock(lock)
def _runtime_set_fingerprint(directory: Path, binary_name: str, variant: str):
def _runtime_set_fingerprint(directory: Path, binary_name: str):
"""Identify one complete set so a lock waiter can preserve its winner."""
names = _runtime_set_names(directory, binary_name, variant)
names = _runtime_set_names(directory, binary_name)
if names is None:
return None
try:
@@ -519,33 +454,12 @@ def _runtime_set_fingerprint(directory: Path, binary_name: str, variant: str):
return None
def _staged_runtime_names(staged_paths, binary_name: str, variant: str):
def _staged_runtime_names(staged_paths, binary_name: str):
names = set(staged_paths)
required = {binary_name, _INTEGRATIONS_NAME}
pack_names = [
name
for name in names
if name.startswith("cbm-ui-") and name.endswith(".pack")
]
if variant == "ui":
if len(pack_names) != 1 or _UI_PACK_RE.fullmatch(pack_names[0]) is None:
raise RuntimeError("staged UI runtime set must contain exactly one pack")
pack_name = pack_names[0]
if not _regular_file(staged_paths[pack_name]) or not _ui_pack_path_matches_name(
staged_paths[pack_name], pack_name
):
raise RuntimeError("staged UI pack digest does not match its filename")
required.add(pack_name)
runtime_names = (_INTEGRATIONS_NAME, pack_name, binary_name)
else:
if pack_names:
raise RuntimeError("standard runtime set must not contain a UI pack")
runtime_names = (_INTEGRATIONS_NAME, binary_name)
if names != required or any(
not _regular_file(staged_paths[name]) for name in required
):
required = {binary_name}
if names != required or not _regular_file(staged_paths[binary_name]):
raise RuntimeError("staged package cache is incomplete")
return runtime_names
return (binary_name,)
def _windows_process_api():
@@ -890,11 +804,7 @@ def _release_runtime_lock(lock):
# partial retirement from partial publication; cleanup-only makes deletion
# retryable after the destination has been accepted or restored.
def _runtime_backup_target_name(name: str, binary_name: str) -> bool:
return (
name == binary_name
or name == _INTEGRATIONS_NAME
or (name.startswith("cbm-ui-") and name.endswith(".pack"))
)
return name == binary_name
def _create_runtime_backup_directory(directory: Path) -> Path:
@@ -994,10 +904,8 @@ def _current_runtime_targets(directory: Path, binary_name: str):
return targets
def _runtime_set_ready_at(
directory: Path, binary_name: str, variant: str, verifier=None
) -> bool:
if _runtime_set_names(directory, binary_name, variant) is None:
def _runtime_set_ready_at(directory: Path, binary_name: str, verifier=None) -> bool:
if _runtime_set_names(directory, binary_name) is None:
return False
if verifier is not None:
try:
@@ -1068,9 +976,7 @@ def _copy_runtime_backup_file(
pass
def _reconcile_runtime_backups(
directory: Path, binary_name: str, variant: str, verifier, lock
):
def _reconcile_runtime_backups(directory: Path, binary_name: str, verifier, lock):
_refresh_runtime_lock(lock)
backups = _list_runtime_backups(directory, binary_name)
for backup in [item for item in backups if item["cleanup_only"]]:
@@ -1079,7 +985,7 @@ def _reconcile_runtime_backups(
if not backups:
return
if _runtime_set_ready_at(directory, binary_name, variant, verifier):
if _runtime_set_ready_at(directory, binary_name, verifier):
for backup in backups:
_cleanup_runtime_backup(backup, lock)
return
@@ -1151,31 +1057,24 @@ def _publish_runtime_set(
staged_paths,
dest: Path,
binary_name: str,
variant: str,
replace_file=None,
verifier=None,
):
"""Publish sidecars first, retaining exact-byte backups for recovery."""
runtime_names = _staged_runtime_names(staged_paths, binary_name, variant)
runtime_names = _staged_runtime_names(staged_paths, binary_name)
if replace_file is None:
replace_file = os.replace
_require_safe_runtime_directory(dest.parent)
initial_fingerprint = _runtime_set_fingerprint(
dest.parent, binary_name, variant
)
initial_fingerprint = _runtime_set_fingerprint(dest.parent, binary_name)
lock, contended = _acquire_runtime_lock(dest.parent)
operation_error = None
try:
_refresh_runtime_lock(lock)
_reconcile_runtime_backups(
dest.parent, binary_name, variant, verifier, lock
)
_reconcile_runtime_backups(dest.parent, binary_name, verifier, lock)
# Preserve a complete set that appeared or changed while this publisher
# waited. An unchanged pre-existing set may still be repaired/replaced,
# with its bytes retained below for rollback.
locked_fingerprint = _runtime_set_fingerprint(
dest.parent, binary_name, variant
)
locked_fingerprint = _runtime_set_fingerprint(dest.parent, binary_name)
if (
locked_fingerprint is not None
and (contended or locked_fingerprint != initial_fingerprint)
@@ -1185,13 +1084,7 @@ def _publish_runtime_set(
backup_directory = None
try:
backup_directory = _create_runtime_backup_directory(dest.parent)
pack_like = sorted(
path.name
for path in dest.parent.iterdir()
if path.name.startswith("cbm-ui-")
and path.name.endswith(".pack")
)
retire_names = (binary_name, _INTEGRATIONS_NAME, *pack_like)
retire_names = (binary_name,)
retired = set()
# Retire the executable first: until the final rename, readiness is false.
for name in retire_names:
@@ -1227,7 +1120,7 @@ def _publish_runtime_set(
except OSError as publish_error:
if not _files_equal_sha256(staged_paths[name], target):
raise publish_error
if _runtime_set_names(dest.parent, binary_name, variant) is None:
if _runtime_set_names(dest.parent, binary_name) is None:
raise RuntimeError("published package cache is incomplete")
if verifier is not None:
verifier(dest.parent / binary_name)
@@ -1235,14 +1128,10 @@ def _publish_runtime_set(
except Exception as publication_error:
_assert_runtime_lock_owner(lock)
# Preserve a complete non-cooperating winner; never delete its bytes.
preserve_winner = _runtime_set_ready_at(
dest.parent, binary_name, variant, verifier
)
preserve_winner = _runtime_set_ready_at(dest.parent, binary_name, verifier)
if backup_directory is not None:
try:
_reconcile_runtime_backups(
dest.parent, binary_name, variant, verifier, lock
)
_reconcile_runtime_backups(dest.parent, binary_name, verifier, lock)
except Exception as recovery_error:
raise RuntimeError(
f"{publication_error}; package-cache recovery failed: "
@@ -1271,19 +1160,15 @@ def _download(version: str) -> Path:
os_name = _os_name()
arch = _arch()
ext = "zip" if os_name == "windows" else "tar.gz"
selected_variant = _variant()
# Linux ships a fully-static "-portable" build; the standard linux binary
# dynamically links glibc 2.38+ and fails on older distros. macOS/Windows
# have no such variant. Keep in sync with install.sh / install.js / cli.c.
portable = "-portable" if os_name == "linux" else ""
# Opt into the UI build, whose verified asset pack is published beside the
# binary, with CBM_VARIANT=ui. Default is the standard (headless) build.
ui = "ui-" if selected_variant == "ui" else ""
archive = f"codebase-memory-mcp-{ui}{os_name}-{arch}{portable}.{ext}"
archive = f"codebase-memory-mcp-{os_name}-{arch}{portable}.{ext}"
url = f"https://github.com/{REPO}/releases/download/v{version}/{archive}"
_validate_url_scheme(url)
dest = _bin_path(version, selected_variant)
dest = _bin_path(version)
dest.parent.mkdir(parents=True, exist_ok=True)
print(
@@ -1307,7 +1192,7 @@ def _download(version: str) -> Path:
bin_name = (
_WINDOWS_BINARY_NAME if os_name == "windows" else "codebase-memory-mcp"
)
extraction_names = (bin_name, _INTEGRATIONS_NAME)
extraction_names = (bin_name,)
archive_names = (
_WINDOWS_ARCHIVE_NAMES
if os_name == "windows"
@@ -1321,7 +1206,6 @@ def _download(version: str) -> Path:
tmp,
archive_names,
extraction_names,
selected_variant == "ui",
)
else:
import zipfile
@@ -1331,7 +1215,6 @@ def _download(version: str) -> Path:
tmp,
archive_names,
extraction_names,
selected_variant == "ui",
)
extracted_paths = {}
@@ -1348,7 +1231,7 @@ def _download(version: str) -> Path:
current | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
)
extracted_paths[name] = extracted_path
if _runtime_set_names(Path(tmp), bin_name, selected_variant) is None:
if _runtime_set_names(Path(tmp), bin_name) is None:
sys.exit(
"codebase-memory-mcp: extracted runtime set failed content "
"verification"
@@ -1387,7 +1270,6 @@ def _download(version: str) -> Path:
staged_paths,
dest,
bin_name,
selected_variant,
verifier=_verify_candidate,
)
_verify_candidate(dest)
+323 -605
View File
@@ -87,16 +87,13 @@ class BinarySelectionTests(unittest.TestCase):
["uninstall", "--dir", str(explicit_dir)],
)
def test_standard_and_ui_cache_paths_do_not_collide(self):
with mock.patch.dict("os.environ", {"CBM_VARIANT": "standard"}):
standard = _cli._bin_path("0.8.1")
with mock.patch.dict("os.environ", {"CBM_VARIANT": "ui"}):
ui = _cli._bin_path("0.8.1")
self.assertNotEqual(standard, ui)
def test_release_archives_require_the_integrations_sidecar(self):
self.assertIn("cbm-integrations.json", _cli._WINDOWS_ARCHIVE_NAMES)
def test_release_archives_carry_no_sidecars(self):
# The binary is self-contained; a returning sidecar would silently make
# `pip install` reject every archive against this exact-set allowlist.
for names in (_cli._WINDOWS_ARCHIVE_NAMES, _cli._UNIX_ARCHIVE_NAMES):
self.assertNotIn("cbm-integrations.json", names)
self.assertFalse([n for n in names if n.startswith("cbm-ui-")])
self.assertEqual(len(names), 4)
def test_update_guidance_uses_pip_and_names_managed_install(self):
with mock.patch.object(_cli.sys, "argv", ["cbm", "update"]), \
@@ -207,7 +204,7 @@ class ProcessLivenessTests(unittest.TestCase):
class RuntimeSetTests(unittest.TestCase):
def test_candidate_probe_authenticates_adjacent_runtime_assets(self):
def test_candidate_probe_requires_the_binary_to_execute(self):
candidates = (
Path("/tmp/codebase-memory-mcp"),
PureWindowsPath("/tmp/codebase-memory-mcp"),
@@ -221,21 +218,321 @@ class RuntimeSetTests(unittest.TestCase):
self.assertEqual(
run.call_args.args[0],
[str(candidate), "--verify-runtime-assets"],
[str(candidate), "--version"],
)
def test_runtime_readiness_rejects_multiply_linked_cache_leaves(self):
with tempfile.TemporaryDirectory() as root, mock.patch.object(
_cli, "_cache_dir", return_value=Path(root)
):
binary = _cli._bin_path("0.8.1", "standard")
binary.parent.mkdir(parents=True)
binary.write_text("binary")
integrations = binary.parent / _cli._INTEGRATIONS_NAME
integrations.write_text("integrations")
os.link(integrations, binary.parent / "integrations-hardlink")
def test_concurrent_publishers_preserve_the_first_complete_winner(self):
with tempfile.TemporaryDirectory() as root:
directory = Path(root)
binary_name = "codebase-memory-mcp"
self.assertFalse(_cli._runtime_set_ready("0.8.1"))
def stages(tag):
staged = directory / f".stage-{tag}-{binary_name}"
staged.write_bytes(f"binary:{tag}".encode())
return {binary_name: staged}
first_stages = stages("first")
second_stages = stages("second")
first_holds_lock = threading.Event()
allow_first = threading.Event()
second_observed_lock = threading.Event()
errors = []
replace = os.replace
read_lock_owner = _cli._read_runtime_lock_owner
def pause_first(source, destination):
replace(source, destination)
if Path(destination) == directory / binary_name:
first_holds_lock.set()
if not allow_first.wait(5):
raise RuntimeError("test did not release first publisher")
def observe_waiter(lock_path):
if threading.current_thread().name == "publisher-second":
second_observed_lock.set()
return read_lock_owner(lock_path)
def publish(label, staged_paths, replace_file=None):
try:
_cli._publish_runtime_set(
staged_paths,
directory / binary_name,
binary_name,
replace_file=replace_file,
)
except Exception as exc: # surfaced on the test thread below
errors.append((label, exc))
first = threading.Thread(
name="publisher-first",
target=publish,
args=("first", first_stages, pause_first),
)
second = threading.Thread(
name="publisher-second",
target=publish,
args=("second", second_stages),
)
with mock.patch.object(
_cli,
"_read_runtime_lock_owner",
side_effect=observe_waiter,
):
first.start()
try:
self.assertTrue(first_holds_lock.wait(2))
second.start()
self.assertTrue(second_observed_lock.wait(2))
finally:
allow_first.set()
first.join(5)
if second.ident is not None:
second.join(5)
self.assertFalse(first.is_alive())
self.assertFalse(second.is_alive())
self.assertEqual(errors, [])
self.assertEqual(
(directory / binary_name).read_bytes(), b"binary:first"
)
self.assertFalse((directory / _cli._RUNTIME_LOCK_NAME).exists())
def test_killed_publisher_is_reconciled_by_locked_readiness(self):
helper_source = r"""
import os
import sys
import time
from pathlib import Path
source_root, directory_raw, staged_raw, marker_raw, crash_name = sys.argv[1:]
sys.path.insert(0, source_root)
from codebase_memory_mcp import _cli
directory = Path(directory_raw)
staged = Path(staged_raw)
marker = Path(marker_raw)
binary_name = "codebase-memory-mcp"
staged_paths = {
binary_name: staged / binary_name,
}
replace = os.replace
def pausing_replace(source, destination):
replace(source, destination)
if Path(destination) == directory / crash_name:
marker.write_text("reached\n")
while True:
time.sleep(60)
def verifier(binary):
if not Path(binary).read_text().startswith("binary:"):
raise RuntimeError("test binary failed verification")
_cli._publish_runtime_set(
staged_paths,
directory / binary_name,
binary_name,
replace_file=pausing_replace,
verifier=verifier,
)
"""
binary_name = "codebase-memory-mcp"
for crash_name, expected_ready in (
(binary_name, True),
):
with self.subTest(crash_name=crash_name), tempfile.TemporaryDirectory() as root:
root_path = Path(root)
directory = root_path / "destination"
staged = root_path / "staged"
marker = root_path / "crash-reached"
directory.mkdir()
staged.mkdir()
(directory / binary_name).write_text("corrupt:old")
(staged / binary_name).write_text("binary:candidate")
environment = os.environ.copy()
environment["PYTHONPYCACHEPREFIX"] = str(root_path / "pycache")
process = subprocess.Popen(
[
sys.executable,
"-c",
helper_source,
str(PACKAGE_ROOT / "src"),
str(directory),
str(staged),
str(marker),
crash_name,
],
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
env=environment,
)
try:
deadline = time.monotonic() + 10
while not marker.exists() and time.monotonic() < deadline:
if process.poll() is not None:
stderr = process.communicate()[1]
self.fail(
"publication helper exited before crash gate: "
f"{stderr}"
)
time.sleep(0.01)
self.assertTrue(
marker.exists(),
"publication helper did not reach crash gate",
)
backups = [
path
for path in directory.iterdir()
if path.name.startswith(".cbm-runtime-backup-")
]
self.assertEqual(len(backups), 1)
self.assertTrue(backups[0].is_dir())
self.assertTrue(
(backups[0] / ".retirement-complete").is_file()
)
process.kill()
process.wait(5)
self.assertNotEqual(process.returncode, 0)
def verifier(binary):
if not Path(binary).read_text().startswith("binary:"):
raise RuntimeError("test binary failed verification")
with mock.patch.object(
_cli, "_bin_path", return_value=directory / binary_name
), mock.patch.object(
_cli, "_verify_candidate", side_effect=verifier
):
ready = _cli._runtime_set_ready_locked("test-version")
self.assertEqual(ready, expected_ready)
if not ready:
self.assertEqual(
(directory / binary_name).read_text(),
"corrupt:old",
)
retry_staged = root_path / "retry-staged"
retry_staged.mkdir()
retry_paths = {
binary_name: retry_staged / binary_name,
}
retry_paths[binary_name].write_text("binary:candidate")
_cli._publish_runtime_set(
retry_paths,
directory / binary_name,
binary_name,
verifier=verifier,
)
self.assertEqual(
(directory / binary_name).read_text(), "binary:candidate"
)
self.assertFalse(
any(
path.name.startswith(".cbm-runtime-backup-")
for path in directory.iterdir()
)
)
self.assertFalse(
(directory / _cli._RUNTIME_LOCK_NAME).exists()
)
finally:
if process.poll() is None:
process.kill()
process.wait(5)
if process.stderr is not None:
process.stderr.close()
def test_publication_failure_restores_prior_complete_runtime_set(self):
with tempfile.TemporaryDirectory() as root:
directory = Path(root)
binary_name = "codebase-memory-mcp"
(directory / binary_name).write_text("binary:old")
staged_paths = {}
for name, contents in (
(binary_name, b"binary:candidate"),
):
staged = directory / f"stage-{name}"
staged.write_bytes(contents)
staged_paths[name] = staged
replace = os.replace
failed = False
retired = []
def fail_binary_publish(source, destination):
nonlocal failed
source = Path(source)
destination = Path(destination)
if destination.parent.name.startswith(".cbm-runtime-backup-"):
retired.append(source.name)
if destination == directory / binary_name and not failed:
failed = True
raise OSError("injected binary publication failure")
replace(source, destination)
with self.assertRaises(OSError):
_cli._publish_runtime_set(
staged_paths,
directory / binary_name,
binary_name,
replace_file=fail_binary_publish,
)
self.assertTrue(failed)
self.assertEqual(retired[0], binary_name)
self.assertEqual((directory / binary_name).read_text(), "binary:old")
self.assertFalse(
any(
path.name.startswith(".cbm-runtime-backup-")
for path in directory.iterdir()
)
)
def test_failure_never_deletes_a_complete_foreign_winner(self):
with tempfile.TemporaryDirectory() as root:
directory = Path(root)
binary_name = "codebase-memory-mcp"
(directory / binary_name).write_bytes(b"binary:old")
staged_paths = {}
for name, contents in (
(binary_name, b"binary:candidate"),
):
staged = directory / f".stage-candidate-{name}"
staged.write_bytes(contents)
staged_paths[name] = staged
replace = os.replace
injected = False
def install_foreign_winner(source, destination):
nonlocal injected
source = Path(source)
destination = Path(destination)
if destination == directory / binary_name and not injected:
injected = True
(directory / binary_name).write_bytes(b"binary:foreign")
raise OSError("simulated non-cooperating winner")
replace(source, destination)
_cli._publish_runtime_set(
staged_paths,
directory / binary_name,
binary_name,
replace_file=install_foreign_winner,
)
self.assertTrue(injected)
self.assertEqual(
(directory / binary_name).read_bytes(), b"binary:foreign"
)
self.assertFalse(
any(
path.name.startswith(".cbm-runtime-backup-")
for path in directory.iterdir()
)
)
def test_orphan_reconciliation_rejects_multiply_linked_backup_members(self):
with tempfile.TemporaryDirectory() as root:
@@ -250,8 +547,6 @@ class RuntimeSetTests(unittest.TestCase):
with mock.patch.object(
_cli, "_bin_path", return_value=binary
), mock.patch.object(
_cli, "_variant", return_value="standard"
), self.assertRaisesRegex(
RuntimeError, "unsafe package-cache backup member"
):
@@ -484,58 +779,6 @@ class RuntimeSetTests(unittest.TestCase):
)
self.assertFalse(lock_path.exists())
def test_archive_namespace_is_exact_and_ui_has_one_pack(self):
pack = f"cbm-ui-{'a' * 64}.pack"
names = (*_cli._WINDOWS_ARCHIVE_NAMES, pack)
self.assertEqual(
_cli._validate_archive_names(names, _cli._WINDOWS_ARCHIVE_NAMES, True),
pack,
)
with self.assertRaises(SystemExit):
_cli._validate_archive_names(
(*names, "unexpected-root-file"),
_cli._WINDOWS_ARCHIVE_NAMES,
True,
)
with self.assertRaises(SystemExit):
_cli._validate_archive_names(
(*names, f"cbm-ui-{'b' * 64}.pack"),
_cli._WINDOWS_ARCHIVE_NAMES,
True,
)
with self.assertRaises(SystemExit):
_cli._validate_archive_names(
names, _cli._WINDOWS_ARCHIVE_NAMES, False
)
def test_ui_zip_extracts_only_the_runtime_set(self):
pack = f"cbm-ui-{'a' * 64}.pack"
with tempfile.TemporaryDirectory() as root:
archive = Path(root) / "release.zip"
destination = Path(root) / "extract"
destination.mkdir()
with zipfile.ZipFile(archive, "w") as zf:
for name in (*_cli._WINDOWS_ARCHIVE_NAMES, pack):
zf.writestr(name, name.encode())
with zipfile.ZipFile(archive) as zf:
names = _cli._safe_extract_zip(
zf,
str(destination),
_cli._WINDOWS_ARCHIVE_NAMES,
(_cli._WINDOWS_BINARY_NAME, _cli._INTEGRATIONS_NAME),
True,
)
self.assertEqual(
names,
(_cli._WINDOWS_BINARY_NAME, _cli._INTEGRATIONS_NAME, pack),
)
self.assertEqual(
{path.name for path in destination.iterdir()}, set(names)
)
def test_unix_tar_rejects_an_unexpected_root_member(self):
with tempfile.TemporaryDirectory() as root:
archive = Path(root) / "release.tar.gz"
@@ -551,8 +794,7 @@ class RuntimeSetTests(unittest.TestCase):
tf,
str(destination),
_cli._UNIX_ARCHIVE_NAMES,
("codebase-memory-mcp", _cli._INTEGRATIONS_NAME),
False,
("codebase-memory-mcp",),
)
def test_unix_tar_rejects_hardlink_members(self):
@@ -561,7 +803,7 @@ class RuntimeSetTests(unittest.TestCase):
with tarfile.open(archive, "w:gz") as tf:
info = tarfile.TarInfo("codebase-memory-mcp")
info.type = tarfile.LNKTYPE
info.linkname = "cbm-integrations.json"
info.linkname = "LICENSE"
tf.addfile(info)
destination = Path(root) / "extract"
destination.mkdir()
@@ -581,7 +823,7 @@ class RuntimeSetTests(unittest.TestCase):
info = zipfile.ZipInfo(_cli._WINDOWS_BINARY_NAME)
info.create_system = 3
info.external_attr = (stat.S_IFLNK | 0o777) << 16
zf.writestr(info, "cbm-integrations.json")
zf.writestr(info, "LICENSE")
destination = Path(root) / "extract"
destination.mkdir()
with zipfile.ZipFile(archive) as zf, self.assertRaises(SystemExit):
@@ -593,527 +835,3 @@ class RuntimeSetTests(unittest.TestCase):
False,
)
def test_readiness_requires_the_complete_selected_variant(self):
with tempfile.TemporaryDirectory() as root, mock.patch.object(
_cli, "_cache_dir", return_value=Path(root)
):
with mock.patch.dict(os.environ, {"CBM_VARIANT": "standard"}):
standard_binary = _cli._bin_path("0.8.1")
standard_binary.parent.mkdir(parents=True)
standard_binary.write_text("binary")
(standard_binary.parent / _cli._INTEGRATIONS_NAME).write_text(
"integrations"
)
self.assertTrue(_cli._runtime_set_ready("0.8.1"))
(standard_binary.parent / _cli._INTEGRATIONS_NAME).unlink()
self.assertFalse(_cli._runtime_set_ready("0.8.1"))
with mock.patch.dict(os.environ, {"CBM_VARIANT": "ui"}):
ui_binary = _cli._bin_path("0.8.1")
ui_binary.parent.mkdir(parents=True)
ui_binary.write_text("binary")
(ui_binary.parent / _cli._INTEGRATIONS_NAME).write_text(
"integrations"
)
pack_bytes = b"pack"
pack_name = (
f"cbm-ui-{hashlib.sha256(pack_bytes).hexdigest()}.pack"
)
(ui_binary.parent / pack_name).write_bytes(pack_bytes)
self.assertTrue(_cli._runtime_set_ready("0.8.1"))
second_pack = f"cbm-ui-{'b' * 64}.pack"
(ui_binary.parent / second_pack).write_text("second pack")
self.assertFalse(_cli._runtime_set_ready("0.8.1"))
def test_ui_readiness_rejects_digest_mismatched_pack_bytes(self):
with tempfile.TemporaryDirectory() as root, mock.patch.object(
_cli, "_cache_dir", return_value=Path(root)
), mock.patch.dict(os.environ, {"CBM_VARIANT": "ui"}):
binary = _cli._bin_path("0.8.1")
binary.parent.mkdir(parents=True)
binary.write_text("binary")
(binary.parent / _cli._INTEGRATIONS_NAME).write_text("integrations")
authenticated = b"authenticated pack"
digest = hashlib.sha256(authenticated).hexdigest()
pack = binary.parent / f"cbm-ui-{digest}.pack"
pack.write_bytes(b"corrupt pack")
self.assertFalse(
_cli._runtime_set_ready("0.8.1"),
"a well-shaped filename must not authenticate different bytes",
)
def test_ui_publication_rejects_digest_mismatched_staged_pack(self):
with tempfile.TemporaryDirectory() as root:
directory = Path(root)
binary_name = "codebase-memory-mcp"
authenticated = b"authenticated pack"
pack = (
f"cbm-ui-{hashlib.sha256(authenticated).hexdigest()}.pack"
)
staged_paths = {}
for name, contents in (
(binary_name, b"binary"),
(_cli._INTEGRATIONS_NAME, b"integrations"),
(pack, b"corrupt pack"),
):
staged = directory / f".stage-{name}"
staged.write_bytes(contents)
staged_paths[name] = staged
with self.assertRaisesRegex(RuntimeError, "digest"):
_cli._publish_runtime_set(
staged_paths, directory / binary_name, binary_name, "ui"
)
self.assertFalse((directory / binary_name).exists())
self.assertFalse((directory / _cli._RUNTIME_LOCK_NAME).exists())
def test_publication_commits_sidecars_before_binary(self):
with tempfile.TemporaryDirectory() as root:
directory = Path(root)
binary_name = "codebase-memory-mcp"
pack_bytes = b"pack"
pack = f"cbm-ui-{hashlib.sha256(pack_bytes).hexdigest()}.pack"
staged_paths = {}
for name, contents in (
(binary_name, b"binary"),
(_cli._INTEGRATIONS_NAME, b"integrations"),
(pack, pack_bytes),
):
staged = directory / f"stage-{name}"
staged.write_bytes(contents)
staged_paths[name] = staged
committed = []
replace = os.replace
def record_replace(source, destination):
committed.append(Path(destination).name)
replace(source, destination)
with mock.patch.object(_cli.os, "replace", side_effect=record_replace):
_cli._publish_runtime_set(
staged_paths, directory / binary_name, binary_name, "ui"
)
self.assertEqual(
committed, [_cli._INTEGRATIONS_NAME, pack, binary_name]
)
def test_killed_publisher_is_reconciled_by_locked_readiness(self):
helper_source = r"""
import os
import sys
import time
from pathlib import Path
source_root, directory_raw, staged_raw, marker_raw, crash_name = sys.argv[1:]
sys.path.insert(0, source_root)
from codebase_memory_mcp import _cli
directory = Path(directory_raw)
staged = Path(staged_raw)
marker = Path(marker_raw)
binary_name = "codebase-memory-mcp"
staged_paths = {
_cli._INTEGRATIONS_NAME: staged / _cli._INTEGRATIONS_NAME,
binary_name: staged / binary_name,
}
replace = os.replace
def pausing_replace(source, destination):
replace(source, destination)
if Path(destination) == directory / crash_name:
marker.write_text("reached\n")
while True:
time.sleep(60)
def verifier(binary):
if not Path(binary).read_text().startswith("binary:"):
raise RuntimeError("test binary failed verification")
_cli._publish_runtime_set(
staged_paths,
directory / binary_name,
binary_name,
"standard",
replace_file=pausing_replace,
verifier=verifier,
)
"""
binary_name = "codebase-memory-mcp"
for crash_name, expected_ready in (
(_cli._INTEGRATIONS_NAME, False),
(binary_name, True),
):
with self.subTest(crash_name=crash_name), tempfile.TemporaryDirectory() as root:
root_path = Path(root)
directory = root_path / "destination"
staged = root_path / "staged"
marker = root_path / "crash-reached"
directory.mkdir()
staged.mkdir()
(directory / binary_name).write_text("corrupt:old")
(directory / _cli._INTEGRATIONS_NAME).write_text(
"integrations:old"
)
(staged / binary_name).write_text("binary:candidate")
(staged / _cli._INTEGRATIONS_NAME).write_text(
"integrations:candidate"
)
environment = os.environ.copy()
environment["PYTHONPYCACHEPREFIX"] = str(root_path / "pycache")
process = subprocess.Popen(
[
sys.executable,
"-c",
helper_source,
str(PACKAGE_ROOT / "src"),
str(directory),
str(staged),
str(marker),
crash_name,
],
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
env=environment,
)
try:
deadline = time.monotonic() + 10
while not marker.exists() and time.monotonic() < deadline:
if process.poll() is not None:
stderr = process.communicate()[1]
self.fail(
"publication helper exited before crash gate: "
f"{stderr}"
)
time.sleep(0.01)
self.assertTrue(
marker.exists(),
"publication helper did not reach crash gate",
)
backups = [
path
for path in directory.iterdir()
if path.name.startswith(".cbm-runtime-backup-")
]
self.assertEqual(len(backups), 1)
self.assertTrue(backups[0].is_dir())
self.assertTrue(
(backups[0] / ".retirement-complete").is_file()
)
process.kill()
process.wait(5)
self.assertNotEqual(process.returncode, 0)
def verifier(binary):
if not Path(binary).read_text().startswith("binary:"):
raise RuntimeError("test binary failed verification")
with mock.patch.object(
_cli, "_bin_path", return_value=directory / binary_name
), mock.patch.object(
_cli, "_variant", return_value="standard"
), mock.patch.object(
_cli, "_verify_candidate", side_effect=verifier
):
ready = _cli._runtime_set_ready_locked("test-version")
self.assertEqual(ready, expected_ready)
if not ready:
self.assertEqual(
(directory / binary_name).read_text(),
"corrupt:old",
)
self.assertEqual(
(directory / _cli._INTEGRATIONS_NAME).read_text(),
"integrations:old",
)
retry_staged = root_path / "retry-staged"
retry_staged.mkdir()
retry_paths = {
binary_name: retry_staged / binary_name,
_cli._INTEGRATIONS_NAME: retry_staged
/ _cli._INTEGRATIONS_NAME,
}
retry_paths[binary_name].write_text("binary:candidate")
retry_paths[_cli._INTEGRATIONS_NAME].write_text(
"integrations:candidate"
)
_cli._publish_runtime_set(
retry_paths,
directory / binary_name,
binary_name,
"standard",
verifier=verifier,
)
self.assertEqual(
(directory / binary_name).read_text(), "binary:candidate"
)
self.assertEqual(
(directory / _cli._INTEGRATIONS_NAME).read_text(),
"integrations:candidate",
)
self.assertFalse(
any(
path.name.startswith(".cbm-runtime-backup-")
for path in directory.iterdir()
)
)
self.assertFalse(
(directory / _cli._RUNTIME_LOCK_NAME).exists()
)
finally:
if process.poll() is None:
process.kill()
process.wait(5)
if process.stderr is not None:
process.stderr.close()
def test_publication_failure_restores_prior_complete_runtime_set(self):
with tempfile.TemporaryDirectory() as root:
directory = Path(root)
binary_name = "codebase-memory-mcp"
old_pack_bytes = b"old pack"
old_pack = f"cbm-ui-{hashlib.sha256(old_pack_bytes).hexdigest()}.pack"
(directory / binary_name).write_text("binary:old")
(directory / _cli._INTEGRATIONS_NAME).write_text("integrations:old")
(directory / old_pack).write_bytes(old_pack_bytes)
candidate_pack_bytes = b"candidate pack"
candidate_pack = (
f"cbm-ui-{hashlib.sha256(candidate_pack_bytes).hexdigest()}.pack"
)
staged_paths = {}
for name, contents in (
(binary_name, b"binary:candidate"),
(_cli._INTEGRATIONS_NAME, b"integrations:candidate"),
(candidate_pack, candidate_pack_bytes),
):
staged = directory / f"stage-{name}"
staged.write_bytes(contents)
staged_paths[name] = staged
replace = os.replace
failed = False
retired = []
def fail_binary_publish(source, destination):
nonlocal failed
source = Path(source)
destination = Path(destination)
if destination.parent.name.startswith(".cbm-runtime-backup-"):
retired.append(source.name)
if destination == directory / binary_name and not failed:
failed = True
raise OSError("injected binary publication failure")
replace(source, destination)
with self.assertRaises(OSError):
_cli._publish_runtime_set(
staged_paths,
directory / binary_name,
binary_name,
"ui",
replace_file=fail_binary_publish,
)
self.assertTrue(failed)
self.assertEqual(retired[0], binary_name)
self.assertEqual((directory / binary_name).read_text(), "binary:old")
self.assertEqual(
(directory / _cli._INTEGRATIONS_NAME).read_text(),
"integrations:old",
)
self.assertEqual((directory / old_pack).read_bytes(), old_pack_bytes)
self.assertFalse((directory / candidate_pack).exists())
self.assertFalse(
any(
path.name.startswith(".cbm-runtime-backup-")
for path in directory.iterdir()
)
)
def test_concurrent_publishers_preserve_the_first_complete_winner(self):
with tempfile.TemporaryDirectory() as root:
directory = Path(root)
binary_name = "codebase-memory-mcp"
def stages(tag):
pack_bytes = f"pack:{tag}".encode()
pack = (
f"cbm-ui-{hashlib.sha256(pack_bytes).hexdigest()}.pack"
)
result = {}
for name, contents in (
(binary_name, f"binary:{tag}".encode()),
(_cli._INTEGRATIONS_NAME, f"integrations:{tag}".encode()),
(pack, pack_bytes),
):
staged = directory / f".stage-{tag}-{name}"
staged.write_bytes(contents)
result[name] = staged
return result, pack
first_stages, first_pack = stages("first")
second_stages, second_pack = stages("second")
first_holds_lock = threading.Event()
allow_first = threading.Event()
second_observed_lock = threading.Event()
errors = []
replace = os.replace
read_lock_owner = _cli._read_runtime_lock_owner
def pause_first(source, destination):
replace(source, destination)
if Path(destination) == directory / binary_name:
first_holds_lock.set()
if not allow_first.wait(5):
raise RuntimeError("test did not release first publisher")
def observe_waiter(lock_path):
if threading.current_thread().name == "publisher-second":
second_observed_lock.set()
return read_lock_owner(lock_path)
def publish(label, staged_paths, replace_file=None):
try:
_cli._publish_runtime_set(
staged_paths,
directory / binary_name,
binary_name,
"ui",
replace_file=replace_file,
)
except Exception as exc: # surfaced on the test thread below
errors.append((label, exc))
first = threading.Thread(
name="publisher-first",
target=publish,
args=("first", first_stages, pause_first),
)
second = threading.Thread(
name="publisher-second",
target=publish,
args=("second", second_stages),
)
with mock.patch.object(
_cli,
"_read_runtime_lock_owner",
side_effect=observe_waiter,
):
first.start()
try:
self.assertTrue(first_holds_lock.wait(2))
second.start()
self.assertTrue(second_observed_lock.wait(2))
finally:
allow_first.set()
first.join(5)
if second.ident is not None:
second.join(5)
self.assertFalse(first.is_alive())
self.assertFalse(second.is_alive())
self.assertEqual(errors, [])
self.assertEqual(
(directory / binary_name).read_bytes(), b"binary:first"
)
self.assertEqual(
(directory / _cli._INTEGRATIONS_NAME).read_bytes(),
b"integrations:first",
)
self.assertEqual(
(directory / first_pack).read_bytes(), b"pack:first"
)
self.assertFalse((directory / second_pack).exists())
self.assertFalse((directory / _cli._RUNTIME_LOCK_NAME).exists())
def test_failure_never_deletes_a_complete_foreign_winner(self):
with tempfile.TemporaryDirectory() as root:
directory = Path(root)
binary_name = "codebase-memory-mcp"
old_pack_bytes = b"pack:old"
old_pack = (
f"cbm-ui-{hashlib.sha256(old_pack_bytes).hexdigest()}.pack"
)
(directory / binary_name).write_bytes(b"binary:old")
(directory / _cli._INTEGRATIONS_NAME).write_bytes(
b"integrations:old"
)
(directory / old_pack).write_bytes(old_pack_bytes)
candidate_pack_bytes = b"pack:candidate"
candidate_pack = (
f"cbm-ui-{hashlib.sha256(candidate_pack_bytes).hexdigest()}.pack"
)
staged_paths = {}
for name, contents in (
(binary_name, b"binary:candidate"),
(_cli._INTEGRATIONS_NAME, b"integrations:candidate"),
(candidate_pack, candidate_pack_bytes),
):
staged = directory / f".stage-candidate-{name}"
staged.write_bytes(contents)
staged_paths[name] = staged
foreign_pack_bytes = b"pack:foreign"
foreign_pack = (
f"cbm-ui-{hashlib.sha256(foreign_pack_bytes).hexdigest()}.pack"
)
replace = os.replace
injected = False
def install_foreign_winner(source, destination):
nonlocal injected
source = Path(source)
destination = Path(destination)
if destination == directory / binary_name and not injected:
injected = True
for path in directory.iterdir():
if path.name.startswith("cbm-ui-") and path.name.endswith(
".pack"
):
path.unlink()
(directory / _cli._INTEGRATIONS_NAME).write_bytes(
b"integrations:foreign"
)
(directory / foreign_pack).write_bytes(foreign_pack_bytes)
(directory / binary_name).write_bytes(b"binary:foreign")
raise OSError("simulated non-cooperating winner")
replace(source, destination)
_cli._publish_runtime_set(
staged_paths,
directory / binary_name,
binary_name,
"ui",
replace_file=install_foreign_winner,
)
self.assertTrue(injected)
self.assertEqual(
(directory / binary_name).read_bytes(), b"binary:foreign"
)
self.assertEqual(
(directory / _cli._INTEGRATIONS_NAME).read_bytes(),
b"integrations:foreign",
)
self.assertEqual(
(directory / foreign_pack).read_bytes(), foreign_pack_bytes
)
self.assertFalse((directory / candidate_pack).exists())
self.assertFalse((directory / old_pack).exists())
self.assertFalse(
any(
path.name.startswith(".cbm-runtime-backup-")
for path in directory.iterdir()
)
)
if __name__ == "__main__":
unittest.main()
+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 ==="
+33 -9
View File
@@ -131,6 +131,7 @@ executable_paths = {
for association in associations:
scan_path = association["scan_path"]
association_counts[scan_path] = association_counts.get(scan_path, 0) + 1
tolerated_paths: List[str] = []
for result in results:
scan_path = result["scan_path"]
if scan_path in results_by_path:
@@ -145,8 +146,18 @@ for result in results:
):
if not result[numeric].isdigit():
fail(f"malformed numeric result field {numeric}: {scan_path}")
if int(result["malicious"]) != 0 or int(result["suspicious"]) != 0:
# Mirrors the gate's release policy: a single Microsoft machine-learning
# verdict is tolerated and disclosed in the notes; anything else must never
# have reached publication.
tolerated = (
int(result["malicious"]) == 1
and int(result["suspicious"]) == 0
and result["microsoft_category"] == "malicious"
)
if (int(result["malicious"]) != 0 or int(result["suspicious"]) != 0) and not tolerated:
fail(f"detected object reached release-note publication: {scan_path}")
if tolerated:
tolerated_paths.append(scan_path)
if (
re.fullmatch(r"[0-9a-f]{64}", result["sha256"]) is None
or result["virustotal_url"]
@@ -156,8 +167,8 @@ for result in results:
if int(result["association_count"]) != association_counts.get(scan_path):
fail(f"result association count mismatch: {scan_path}")
if scan_path in executable_paths:
if result["microsoft_category"] not in {"undetected", "harmless"}:
fail(f"executable result lacks a decisive clean Microsoft verdict: {scan_path}")
if result["microsoft_category"] not in {"undetected", "harmless", "malicious"}:
fail(f"executable result lacks a decisive Microsoft verdict: {scan_path}")
if not result["microsoft_engine_version"] or not result["microsoft_engine_update"]:
fail(f"executable result lacks Microsoft version evidence: {scan_path}")
results_by_path[scan_path] = result
@@ -172,15 +183,14 @@ for association in associations:
fail(f"result hash/size differs from association: {association['scan_path']}")
member_rows = [row for row in associations if row["association_type"] == "member"]
asset_rows = [row for row in associations if row["association_type"] == "pack_asset"]
if len(member_rows) + len(asset_rows) != len(associations):
if len(member_rows) != len(associations):
fail("association manifest contains a non-extracted scan target")
archive_hashes: Dict[str, str] = {}
for association in associations:
archive = association["archive"]
archive_sha256 = association["archive_sha256"]
if not archive or re.fullmatch(r"[0-9a-f]{64}", archive_sha256) is None:
fail("member/asset association lacks valid archive SHA provenance")
fail("member association lacks valid archive SHA provenance")
previous_sha256 = archive_hashes.get(archive)
if previous_sha256 is not None and previous_sha256 != archive_sha256:
fail(f"conflicting archive SHA provenance: {archive}")
@@ -202,9 +212,7 @@ section = [
"",
(
f"VirusTotal completed **{len(results)} distinct extracted byte objects** covering "
f"**{len(associations)} exact extracted-file associations**: "
f"{len(member_rows)} archive members and "
f"{len(asset_rows)} independently extracted UI-pack assets."
f"**{len(associations)} exact extracted archive members**."
),
(
f"The extraction manifest binds those associations to "
@@ -216,10 +224,26 @@ section = [
(
f"Every scanned object returned **0 malicious and 0 suspicious** verdicts with "
f"{engine_range} decisive engine results (required minimum: {policy})."
if not tolerated_paths
else (
f"{len(results) - len(tolerated_paths)} of {len(results)} scanned objects returned "
f"**0 malicious and 0 suspicious** verdicts with {engine_range} decisive engine "
f"results (required minimum: {policy})."
)
),
(
f"Microsoft returned a decisive clean verdict for all "
f"{len(executable_paths)} executable objects."
if not tolerated_paths
else (
f"**{len(tolerated_paths)} object(s) carry a single Microsoft machine-learning "
f"detection** (`!ml`), which this project treats as a known false positive and "
f"publishes rather than hides. Every other engine returned clean. See "
f"[Antivirus False Positives](https://github.com/{repository}"
f"/blob/main/SECURITY.md#antivirus-false-positives) for the evidence and for how to "
f"verify these artifacts yourself: "
+ ", ".join(f"`{path}`" for path in sorted(tolerated_paths))
)
),
"",
(
+59 -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
@@ -183,6 +147,29 @@ resolve_elf_reader() {
# Echoes the GNU_STACK flag field ("RWE", "RW", "rwx", "rw-"), empty if the
# header is absent or unparseable.
exec_load_bytes() {
# Total size of PT_LOAD segments carrying the execute bit. With
# -z separate-code the executable segment holds only code; without it the
# linker merges .rodata in, so this number balloons to nearly the whole file.
case "$ELF_READER_KIND" in
readelf)
# NOT strtonum(): that is a gawk extension, and CI's awk is mawk, where
# it is undefined -- the sum would silently be 0 and this gate would
# pass vacuously on exactly the artifacts it exists to catch. Emit the
# hex MemSiz fields and convert in the shell.
total=0
for hex in $("$ELF_READER" -lW "$1" 2>/dev/null |
awk '/^ LOAD/ && $0 ~ /R E/ { print $6 }'); do
total=$((total + 16#${hex#0x}))
done
echo "$total"
;;
objdump)
echo unsupported
;;
esac
}
gnu_stack_flags() {
case "$ELF_READER_KIND" in
readelf)
@@ -249,8 +236,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 +246,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.
@@ -312,6 +282,32 @@ check_file() {
printf 'n/a %-22s %s: executable-stack check is ELF-only\n' A1-noexec-stack "$token"
fi
# A1b — read-only DATA must not live in the executable mapping. GNU ld
# enables -z separate-code by default on x86-64 but NOT on aarch64, so the
# arm64 binaries shipped ONE R E PT_LOAD spanning the whole image: 259 MB of
# tree-sitter parse tables mapped executable while amd64 mapped the same
# bytes R only. Section flags said A, not AX -- the kernel applies SEGMENT
# permissions, so section flags were never the control. Heuristic on
# purpose: if executable segments cover most of the file, .rodata is in them.
if [ "$fmt" = elf ]; then
exec_bytes=$(exec_load_bytes "$file")
file_bytes=$(wc -c < "$file" | tr -d ' ')
if [ "$exec_bytes" = unsupported ]; then
printf 'n/a %-22s %s: %s cannot report segment sizes\n' \
A1b-rodata-noexec "$token" "$ELF_READER_KIND"
elif [ "${exec_bytes:-0}" -gt 0 ] && [ "$file_bytes" -gt 0 ] &&
[ $((exec_bytes * 100 / file_bytes)) -gt 60 ]; then
report FAIL A1b-rodata-noexec "$token" \
"executable PT_LOAD segments cover $((exec_bytes * 100 / file_bytes))% of the file ($exec_bytes/$file_bytes bytes) — read-only data is mapped executable (link with -z separate-code)"
else
report PASS A1b-rodata-noexec "$token" \
"executable PT_LOAD segments cover $((exec_bytes * 100 / file_bytes))% of the file (read-only data is outside them)"
fi
else
printf 'n/a %-22s %s: segment-permission check is ELF-only\n' A1b-rodata-noexec "$token"
fi
# A2 — test-only seams.
for needle in "${SEAM_NEEDLES[@]}"; do
assert_absent "$file" "$token" A2-no-test-seams "$needle"
@@ -362,46 +358,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 ────────────────────────────────────────────────
+33 -14
View File
@@ -217,7 +217,7 @@ def load_expected(path: pathlib.Path) -> Tuple[List[ExpectedObject], int]:
size = int(row["size"])
association_count = int(row["association_count"])
kinds = row["association_kinds"].split(",")
allowed_kinds = {"binary", "pack", "runtime", "ui_asset"}
allowed_kinds = {"binary", "runtime"}
if (
association_count < 1
or any(not kind or kind not in allowed_kinds for kind in kinds)
@@ -288,14 +288,11 @@ def validate_associations(
if key in seen:
raise GateError(f"duplicate release association: {key}")
seen.add(key)
if association_type not in {"member", "pack_asset"} or not row["archive"]:
if association_type != "member" or not row["archive"]:
raise GateError(f"malformed release association: {key}")
if archive_hashes.get(row["archive"]) != row["archive_sha256"]:
raise GateError(f"association is not bound to its archive provenance: {key}")
if association_type == "member":
semantic_valid = bool(row["member"]) and not row["asset_path"]
else:
semantic_valid = bool(row["member"] and row["asset_path"] and row["mime"]) and row["kind"] == "ui_asset"
semantic_valid = bool(row["member"]) and not row["asset_path"]
if not semantic_valid:
raise GateError(f"malformed release association semantics: {key}")
expected = expected_by_path.get(row["scan_path"])
@@ -460,7 +457,7 @@ def parse_completed(document: object, submission: Submission) -> Tuple[str, Opti
malicious = normalized["malicious"]
suspicious = normalized["suspicious"]
detections: List[str] = []
detections: List[Tuple[str, str, str, str, str]] = []
results = attributes.get("results")
microsoft_category = ""
microsoft_engine_version = ""
@@ -480,9 +477,7 @@ def parse_completed(document: object, submission: Submission) -> Tuple[str, Opti
label = one_line(result.get("result") or category)
version = one_line(result.get("engine_version") or "?")
updated = one_line(result.get("engine_update") or "?")
detections.append(
f"{one_line(engine)} = {label} (engine {version}, defs {updated})"
)
detections.append((one_line(engine), label, str(category), version, updated))
if engine == "Microsoft":
microsoft_category = str(category)
microsoft_engine_version = required_one_line_string(
@@ -611,6 +606,26 @@ def write_results(
temporary.unlink()
# Release policy: exactly ONE Microsoft machine-learning verdict is tolerated.
#
# Microsoft's `!ml` suffix marks a heuristic/ML classification rather than a
# signature match, and it is an endemic false positive on large unsigned native
# binaries -- llama.cpp, GitHub's own `gh`, Microsoft's own Go toolchain and
# Anthropic's Claude installer have all carried the same family. Our own
# evidence is that the verdict is not a property of our bytes: it inverts across
# architectures and link modes, moves between sibling artifacts of one build,
# and lands in different variant buckets for the same source.
#
# Everything else still fails the release: two or more engines, any label that
# is not `!ml` (a signature hit is a real finding), any non-Microsoft engine,
# any suspicious verdict, and every infrastructure error.
def is_tolerated_detection(result, detections) -> bool:
if result.suspicious or result.malicious != 1 or len(detections) != 1:
return False
engine, label, category, _version, _updated = detections[0]
return engine == "Microsoft" and category == "malicious" and label.endswith("!ml")
def main() -> None:
api_key = required_env("VT_API_KEY")
raw_analysis = required_env("VT_ANALYSIS")
@@ -674,6 +689,7 @@ def main() -> None:
print(f" {submission.expected.scan_path}: {status}; will retry round-robin")
continue
completed.append(result)
tolerated = is_tolerated_detection(result, detections)
if result.completed_engines < min_engines:
message = (
f"{submission.expected.scan_path} completed with only "
@@ -688,11 +704,14 @@ def main() -> None:
f"({result.malicious} malicious, {result.suspicious} suspicious / "
f"{result.completed_engines} decisive engines)"
)
failures.append(message)
print(f"BLOCKED: {message}")
if tolerated:
print(f"TOLERATED: {message}")
else:
failures.append(message)
print(f"BLOCKED: {message}")
if detections:
for detection in detections:
print(f" detected by: {detection}")
for engine, label, _category, version, updated in detections:
print(f" detected by: {engine} = {label} (engine {version}, defs {updated})")
else:
print(" detected by: <engine names unavailable in the analysis response>")
print(
+27 -218
View File
@@ -4,23 +4,22 @@
#
# Usage:
# extract-release-archives.sh <archive-dir> <output-dir> \
# [--archive-scope=all|ui] [--expect-archives=N] [--expect-binaries=N] [--expect-packs=N] \
# [--expect-archives=N] [--expect-binaries=N] \
# [--expect-runtime-files=N]
#
# The output directory is published as one atomic bundle:
# objects/ one file per distinct byte sequence
# associations.tsv every extracted member and CBMUIPK asset -> scan object
# associations.tsv every extracted archive member -> scan object
# scan-set.tsv the exact path/hash/size set the VT action must return
#
# Every archive is validated and hashed for provenance, but downloadable
# .tar.gz/.zip release containers are not scanned. Their exact members and the
# uncompressed HTML/JavaScript/CSS/etc payloads inside every CBMUIPK v1 pack are
# covered. Identical bytes are uploaded once, but no extracted member/asset
# .tar.gz/.zip release containers are not scanned. Their exact members are
# covered instead. Identical bytes are uploaded once, but no extracted member
# association is discarded.
set -euo pipefail
if [ "$#" -lt 2 ]; then
echo "Usage: $0 <archive-dir> <output-dir> [--archive-scope=all|ui] [--expect-archives=N] [--expect-binaries=N] [--expect-packs=N] [--expect-runtime-files=N]" >&2
echo "Usage: $0 <archive-dir> <output-dir> [--expect-archives=N] [--expect-binaries=N] [--expect-runtime-files=N]" >&2
exit 2
fi
@@ -72,28 +71,18 @@ UNIX_TARGETS = (
"linux-arm64-portable",
)
WINDOWS_TARGETS = ("windows-amd64", "windows-arm64")
UI_ARCHIVES = frozenset(
[
f"codebase-memory-mcp-ui-{target}.tar.gz"
for target in UNIX_TARGETS
]
+ [
f"codebase-memory-mcp-ui-{target}.zip"
for target in WINDOWS_TARGETS
]
)
STANDARD_ARCHIVES = frozenset(
CANONICAL_ARCHIVES = frozenset(
[f"codebase-memory-mcp-{target}.tar.gz" for target in UNIX_TARGETS]
+ [f"codebase-memory-mcp-{target}.zip" for target in WINDOWS_TARGETS]
)
CANONICAL_ARCHIVES = UI_ARCHIVES | STANDARD_ARCHIVES
PACK_NAME = re.compile(r"cbm-ui-([0-9a-f]{64})\.pack\Z")
# One composition ships; the association column is retained so the schema stays
# stable for the gate and release-notes consumers.
RELEASE_VARIANT = "release"
SAFE_LABEL = re.compile(r"[^A-Za-z0-9._-]+")
SAFE_ASSET_PATH = re.compile(rb"\A[A-Za-z0-9._/-]+\Z")
COUNT_OPTIONS = {
"--expect-archives": "archives",
"--expect-binaries": "binaries",
"--expect-packs": "packs",
"--expect-runtime-files": "runtime_files",
}
MIME_BY_EXTENSION = {
@@ -295,22 +284,13 @@ class ObjectStore:
def parse_arguments(
argv: Sequence[str],
) -> Tuple[pathlib.Path, pathlib.Path, Dict[str, Optional[int]], str]:
) -> Tuple[pathlib.Path, pathlib.Path, Dict[str, Optional[int]]]:
archive_dir = pathlib.Path(argv[1]).absolute()
output_dir = pathlib.Path(argv[2]).absolute()
expected: Dict[str, Optional[int]] = {value: None for value in COUNT_OPTIONS.values()}
seen: set[str] = set()
archive_scope = "all"
for argument in argv[3:]:
option, separator, raw_value = argument.partition("=")
if separator and option == "--archive-scope":
if "archive_scope" in seen:
raise ContractError(f"duplicate option: {option}")
if raw_value not in {"all", "ui"}:
raise ContractError("--archive-scope must be all or ui")
archive_scope = raw_value
seen.add("archive_scope")
continue
if not separator or option not in COUNT_OPTIONS:
raise ContractError(f"unknown option: {argument}")
key = COUNT_OPTIONS[option]
@@ -320,164 +300,33 @@ def parse_arguments(
raise ContractError(f"{option} requires a non-negative integer")
expected[key] = int(raw_value)
seen.add(key)
return archive_dir, output_dir, expected, archive_scope
return archive_dir, output_dir, expected
def validate_namespace(archive_name: str, names: Iterable[str]) -> Tuple[str, Dict[str, str]]:
def validate_namespace(archive_name: str, names: Iterable[str]) -> Dict[str, str]:
names_list = list(names)
if len(names_list) != len(set(names_list)):
duplicate = next(name for name in names_list if names_list.count(name) > 1)
raise ContractError(f"duplicate archive member in {archive_name}: {duplicate}")
windows = archive_name.endswith(".zip")
variant = "ui" if archive_name.startswith("codebase-memory-mcp-ui-") else "standard"
binary = "codebase-memory-mcp.exe" if windows else "codebase-memory-mcp"
installer = "install.ps1" if windows else "install.sh"
fixed = {
binary: "binary",
"cbm-integrations.json": "runtime",
"LICENSE": "runtime",
installer: "runtime",
"THIRD_PARTY_NOTICES.md": "runtime",
}
name_set = set(names_list)
extras = name_set - set(fixed)
if variant == "ui":
invalid = sorted(name for name in extras if PACK_NAME.fullmatch(name) is None)
if invalid:
raise ContractError(f"unexpected archive member in {archive_name}: {invalid[0]}")
if len(extras) != 1:
raise ContractError(
f"UI archive must contain exactly one UI pack in {archive_name}; got {len(extras)}"
)
if len(name_set) != 6 or not set(fixed).issubset(name_set):
missing = sorted(set(fixed) - name_set)
raise ContractError(
f"member namespace mismatch in {archive_name}: expected exactly 6 root files; missing={missing}"
)
kinds = dict(fixed)
kinds[next(iter(extras))] = "pack"
return variant, kinds
if extras:
raise ContractError(f"unexpected archive member in {archive_name}: {sorted(extras)[0]}")
if len(name_set) != 5 or name_set != set(fixed):
if name_set != set(fixed):
missing = sorted(set(fixed) - name_set)
raise ContractError(
f"member namespace mismatch in {archive_name}: expected exactly 5 root files; missing={missing}"
f"member namespace mismatch in {archive_name}: expected exactly 4 root files; missing={missing}"
)
return variant, fixed
def valid_asset_path(raw: bytes) -> bool:
if (
len(raw) < 2
or len(raw) > PACK_MAX_PATH_BYTES
or not SAFE_ASSET_PATH.fullmatch(raw)
or not raw.startswith(b"/")
or raw.endswith(b"/")
or b"//" in raw
):
return False
parts = raw[1:].split(b"/")
if any(part in (b"", b".", b"..") for part in parts):
return False
return raw == b"/index.html" or raw.startswith(b"/assets/")
def parse_pack_assets(path: pathlib.Path) -> List[PackAsset]:
size = path.stat().st_size
if size < PACK_HEADER_BYTES or size > MAX_PACK_BYTES:
raise ContractError(f"CBMUIPK size is outside bounds: {size}")
with path.open("rb") as handle:
header = handle.read(PACK_HEADER_BYTES)
if len(header) != PACK_HEADER_BYTES:
raise ContractError("CBMUIPK header is truncated")
try:
(
magic,
version,
header_bytes,
flags,
count,
entry_bytes,
index_offset,
index_size,
paths_offset,
paths_size,
payload_offset,
payload_size,
total_size,
) = struct.unpack("<8sHHIIIQQQQQQQ", header)
except struct.error as error:
raise ContractError(f"CBMUIPK header is malformed: {error}") from error
if (
magic != b"CBMUIPK\0"
or version != 1
or header_bytes != PACK_HEADER_BYTES
or flags != 0
or count < 1
or count > PACK_MAX_FILES
or entry_bytes != PACK_ENTRY_BYTES
or index_offset != PACK_HEADER_BYTES
or index_size != count * PACK_ENTRY_BYTES
or paths_offset != index_offset + index_size
or paths_size > count * PACK_MAX_PATH_BYTES
or payload_offset != paths_offset + paths_size
or total_size != payload_offset + payload_size
or total_size != size
):
raise ContractError("CBMUIPK header/offset contract failed")
handle.seek(index_offset)
index = handle.read(index_size)
paths = handle.read(paths_size)
if len(index) != index_size or len(paths) != paths_size:
raise ContractError("CBMUIPK index or path table is truncated")
assets: List[PackAsset] = []
expected_path_offset = 0
expected_data_offset = 0
previous_path: Optional[bytes] = None
has_index = False
for item in range(count):
entry = struct.unpack_from("<IHBBQQ", index, item * PACK_ENTRY_BYTES)
path_offset, path_length, mime_id, cache_id, data_offset, data_length = entry
if (
path_offset != expected_path_offset
or path_length == 0
or path_length > paths_size - expected_path_offset
or data_offset != expected_data_offset
or data_length == 0
or data_length > payload_size - expected_data_offset
):
raise ContractError("CBMUIPK entry bounds/contiguity contract failed")
raw_path = paths[path_offset : path_offset + path_length]
if not valid_asset_path(raw_path) or (previous_path is not None and previous_path >= raw_path):
raise ContractError("CBMUIPK asset path is invalid, duplicate or unsorted")
try:
asset_path = raw_path.decode("ascii")
except UnicodeDecodeError as error:
raise ContractError("CBMUIPK asset path is not ASCII") from error
extension = pathlib.PurePosixPath(asset_path).suffix
mapping = MIME_BY_EXTENSION.get(extension)
if mapping is None or mime_id != mapping[0]:
raise ContractError(f"CBMUIPK MIME/path contract failed: {asset_path}")
is_index = asset_path == "/index.html"
if cache_id != (1 if is_index else 2) or (is_index and has_index):
raise ContractError(f"CBMUIPK cache/index contract failed: {asset_path}")
has_index = has_index or is_index
assets.append(
PackAsset(
path=asset_path,
mime=mapping[1],
offset=payload_offset + data_offset,
size=data_length,
)
)
previous_path = raw_path
expected_path_offset += path_length
expected_data_offset += data_length
if not has_index or expected_path_offset != paths_size or expected_data_offset != payload_size:
raise ContractError("CBMUIPK tables do not cover exactly one complete payload")
return assets
return fixed
def add_association(
@@ -487,7 +336,6 @@ def add_association(
association_type: str,
archive: str,
archive_sha256: str,
variant: str,
kind: str,
member: str = "",
asset_path: str = "",
@@ -500,7 +348,7 @@ def add_association(
"association_type": association_type,
"archive": archive,
"archive_sha256": archive_sha256,
"variant": variant,
"variant": RELEASE_VARIANT,
"kind": kind,
"member": member,
"asset_path": asset_path,
@@ -537,7 +385,7 @@ def process_member(
kind: str,
archive_name: str,
archive_sha256: str,
variant: str,
variant: str = RELEASE_VARIANT,
) -> None:
scan_object = store.ingest_stream(
source,
@@ -545,43 +393,15 @@ def process_member(
ceiling=MAX_MEMBER_BYTES,
label=member,
)
if kind == "pack":
match = PACK_NAME.fullmatch(member)
if match is None or scan_object.sha256 != match.group(1):
raise ContractError(
f"UI pack digest does not match its filename in {archive_name}: {member}"
)
add_association(
rows,
scan_object,
association_type="member",
archive=archive_name,
archive_sha256=archive_sha256,
variant=variant,
kind=kind,
member=member,
)
if kind != "pack":
return
for asset in parse_pack_assets(scan_object.path):
asset_object = store.ingest_slice(
scan_object.path,
offset=asset.offset,
size=asset.size,
label=asset.path,
)
add_association(
rows,
asset_object,
association_type="pack_asset",
archive=archive_name,
archive_sha256=archive_sha256,
variant=variant,
kind="ui_asset",
member=member,
asset_path=asset.path,
mime=asset.mime,
)
def process_tar(
@@ -605,7 +425,7 @@ def process_tar(
total = validate_member_metadata(archive_name, ((info.name, info.size) for info in infos))
if total > remaining_member_bytes:
raise ContractError("release matrix exceeds total uncompressed byte ceiling")
variant, kinds = validate_namespace(archive_name, [info.name for info in infos])
kinds = validate_namespace(archive_name, [info.name for info in infos])
for info in sorted(infos, key=lambda item: item.name):
source = archive.extractfile(info)
if source is None:
@@ -620,9 +440,8 @@ def process_tar(
kind=kinds[info.name],
archive_name=archive_name,
archive_sha256=archive_sha256,
variant=variant,
)
return variant, kinds, total
return kinds, total
def process_zip(
@@ -651,7 +470,7 @@ def process_zip(
total = validate_member_metadata(archive_name, ((info.filename, info.file_size) for info in infos))
if total > remaining_member_bytes:
raise ContractError("release matrix exceeds total uncompressed byte ceiling")
variant, kinds = validate_namespace(archive_name, [info.filename for info in infos])
kinds = validate_namespace(archive_name, [info.filename for info in infos])
for info in sorted(infos, key=lambda item: item.filename):
with archive.open(info, "r") as source:
process_member(
@@ -663,9 +482,8 @@ def process_zip(
kind=kinds[info.filename],
archive_name=archive_name,
archive_sha256=archive_sha256,
variant=variant,
)
return variant, kinds, total
return kinds, total
def write_tsv(
@@ -688,7 +506,7 @@ def write_tsv(
def main(argv: Sequence[str]) -> None:
archive_dir, output_dir, expected, archive_scope = parse_arguments(argv)
archive_dir, output_dir, expected = parse_arguments(argv)
if not archive_dir.is_dir() or archive_dir.is_symlink():
raise ContractError(f"archive directory is not a regular directory: {archive_dir}")
if output_dir.name in ("", ".", ".."):
@@ -701,13 +519,13 @@ def main(argv: Sequence[str]) -> None:
archive_paths = sorted(archive_dir.iterdir(), key=lambda candidate: candidate.name)
actual_names = {path.name for path in archive_paths}
expected_names = UI_ARCHIVES if archive_scope == "ui" else CANONICAL_ARCHIVES
expected_names = CANONICAL_ARCHIVES
if actual_names != expected_names or len(archive_paths) != len(expected_names):
missing = sorted(expected_names - actual_names)
unexpected = sorted(actual_names - expected_names)
raise ContractError(
f"archive namespace mismatch: expected exact canonical {len(expected_names)} "
f"({archive_scope}); missing={missing}, unexpected={unexpected}"
f"missing={missing}, unexpected={unexpected}"
)
for path in archive_paths:
mode = path.lstat().st_mode
@@ -723,9 +541,7 @@ def main(argv: Sequence[str]) -> None:
counts = {
"archives": 0,
"binaries": 0,
"packs": 0,
"runtime_files": 0,
"pack_assets": 0,
"associations": 0,
"scan_objects": 0,
}
@@ -738,14 +554,13 @@ def main(argv: Sequence[str]) -> None:
archive_store = ObjectStore(pathlib.Path(temporary) / "archives")
for archive_path in archive_paths:
archive_name = archive_path.name
variant = "ui" if archive_name.startswith("codebase-memory-mcp-ui-") else "standard"
archive_object = archive_store.ingest_path(
archive_path,
ceiling=MAX_ARCHIVE_BYTES,
label=archive_name,
)
archive_sha256 = archive_object.sha256
_, kinds, member_total = (
kinds, member_total = (
process_tar(
archive_object.path,
archive_name=archive_name,
@@ -769,13 +584,11 @@ def main(argv: Sequence[str]) -> None:
raise ContractError("release matrix exceeds total uncompressed byte ceiling")
counts["archives"] += 1
counts["binaries"] += sum(kind == "binary" for kind in kinds.values())
counts["packs"] += sum(kind == "pack" for kind in kinds.values())
counts["runtime_files"] += sum(kind == "runtime" for kind in kinds.values())
counts["pack_assets"] = sum(row["association_type"] == "pack_asset" for row in rows)
counts["associations"] = len(rows)
counts["scan_objects"] = len(store.objects)
for key in ("archives", "binaries", "packs", "runtime_files"):
for key in ("archives", "binaries", "runtime_files"):
wanted = expected[key]
if wanted is not None and counts[key] != wanted:
display = key.replace("_files", " files")
@@ -786,7 +599,6 @@ def main(argv: Sequence[str]) -> None:
rows.sort(
key=lambda row: (
str(row["archive"]),
{"member": 0, "pack_asset": 1}[str(row["association_type"])],
str(row["member"]),
str(row["asset_path"]),
)
@@ -794,9 +606,7 @@ def main(argv: Sequence[str]) -> None:
metadata_order = (
"archives",
"binaries",
"packs",
"runtime_files",
"pack_assets",
"associations",
"scan_objects",
)
@@ -837,8 +647,7 @@ def main(argv: Sequence[str]) -> None:
print(
f"validated exact {counts['archives']}-archive matrix: "
f"{counts['binaries']} binaries, {counts['packs']} UI packs, "
f"{counts['runtime_files']} runtime files and {counts['pack_assets']} pack assets"
f"{counts['binaries']} binaries and {counts['runtime_files']} runtime files"
)
print(
f"scan bundle: {counts['scan_objects']} distinct byte objects cover "
+11 -22
View File
@@ -17,8 +17,7 @@ cd "$ROOT"
usage() {
cat <<'EOF'
Usage: scripts/ci/smoke-artifact.sh <goos> <goarch> [--variant standard|ui]
[VAR=VAL ...]
Usage: scripts/ci/smoke-artifact.sh <goos> <goarch> [VAR=VAL ...]
Build → package (scripts/package-release.sh) → extract → smoke the EXTRACTED
artifact through the canonical wrapper, exactly like the release venue:
@@ -26,9 +25,6 @@ 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
assets become mandatory — a standard binary cannot pass a ui run).
Make passthrough (VAR=VAL): CC= CXX= STATIC=1 ... forwarded to build steps.
Environment: BUILD_DIR (default build/c) — build tree used for the archive.
On failure the work directory is preserved for post-mortem (path printed).
@@ -37,17 +33,11 @@ EOF
GOOS=""
GOARCH=""
VARIANT="standard"
BUILD_ARGS=()
expect_value=""
for arg in "$@"; do
if [ "$expect_value" = "variant" ]; then
VARIANT="$arg"; expect_value=""; continue
fi
case "$arg" in
-h | --help) usage; exit 0 ;;
--variant) expect_value="variant" ;;
--variant=*) VARIANT="${arg#--variant=}" ;;
-*)
echo "smoke-artifact: unknown option '$arg'. Please consult --help." >&2
exit 2
@@ -64,11 +54,10 @@ for arg in "$@"; do
esac
done
[ -n "$GOOS" ] && [ -n "$GOARCH" ] || { usage >&2; exit 2; }
case "$VARIANT" in
standard) SUFFIX=""; UI_FLAG=() ;;
ui) SUFFIX="-ui"; UI_FLAG=(--with-ui) ;;
*) echo "smoke-artifact: variant must be 'standard' or 'ui'. Please consult --help." >&2; exit 2 ;;
esac
UI_FLAG=(--with-ui)
# This lane builds and packages the SHIPPED composition, so a binary that serves
# no frontend is a defect here, not a documented skip.
export SMOKE_REQUIRE_UI=1
case "$GOARCH" in
*-portable) BUILD_ARGS+=("STATIC=1") ;;
esac
@@ -95,20 +84,20 @@ cleanup() {
}
trap cleanup EXIT
scripts/package-release.sh "$GOOS" "$GOARCH" --variant "$VARIANT" \
scripts/package-release.sh "$GOOS" "$GOARCH" \
--out-dir "$WORK_DIR" ${BUILD_ARGS[@]+"${BUILD_ARGS[@]}"}
NAME="codebase-memory-mcp${SUFFIX}-${GOOS}-${GOARCH}"
NAME="codebase-memory-mcp-${GOOS}-${GOARCH}"
EXTRACT_DIR="$WORK_DIR/extract"
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" \
SMOKE_ARCH="$GOARCH" \
CBM_SMOKE_ARTIFACT_DIR="$EXTRACT_DIR" \
bash test-infrastructure/vm/vm-smoke.sh
else
@@ -116,6 +105,6 @@ else
chmod +x "$EXTRACT_DIR/codebase-memory-mcp"
echo "=== smoke-artifact: smoking EXTRACTED $NAME.tar.gz via smoke-local.sh ==="
CBM_SMOKE_ARTIFACT_DIR="$EXTRACT_DIR" \
scripts/smoke-local.sh "$EXTRACT_DIR/codebase-memory-mcp" "$VARIANT"
scripts/smoke-local.sh "$EXTRACT_DIR/codebase-memory-mcp"
fi
echo "=== smoke-artifact: $NAME passed ==="
+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;
});
+13 -109
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,47 +174,23 @@ 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
THIRD_PARTY_NOTICES.md
)
[ "$VARIANT" = "ui" ] && ARCHIVE_MEMBERS+=("$UI_PACK_NAME")
zip -q "$OUT_DIR/$NAME.zip" "${ARCHIVE_MEMBERS[@]}"
zip -q "$OUT_DIR/$NAME.zip" \
codebase-memory-mcp.exe LICENSE install.ps1 THIRD_PARTY_NOTICES.md
)
echo "=== package-release: $OUT_DIR/$NAME.zip ==="
else
ARCHIVE_MEMBERS=(
codebase-memory-mcp cbm-integrations.json 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[@]}"
codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md
echo "=== package-release: $OUT_DIR/$NAME.tar.gz ==="
fi
+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"
+24 -66
View File
@@ -21,15 +21,17 @@ 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:
Phase 15's "no assets" outcome becomes a FAILURE
(SMOKE_REQUIRE_UI=1), so a standard binary cannot pass a ui run.
Environment:
SMOKE_REQUIRE_UI=1 Phase 15's "no embedded assets" SKIP becomes a FAILURE.
Set by scripts/ci/smoke-artifact.sh, which builds and
packages the shipped --with-ui composition. Unset in the
fast PR lane, which builds without the frontend.
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).
@@ -49,7 +51,6 @@ if [ $# -gt 2 ]; then
fi
BINARY="${1:?smoke-local: missing <binary> argument. Please consult --help.}"
VARIANT="${2:-standard}"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BINARY="$(cd "$(dirname "$BINARY")" && pwd)/$(basename "$BINARY")"
@@ -57,15 +58,12 @@ 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
# supplying a standard binary can no longer pass quietly.
case "$VARIANT" in
standard) SUFFIX="" ; REQUIRE_UI=0 ;;
ui) SUFFIX="-ui" ; REQUIRE_UI=1 ;;
*) echo "smoke-local: variant must be 'standard' or 'ui'. Please consult --help." >&2; exit 2 ;;
esac
# Whether the UI must be present is the CALLER's claim, not this script's:
# scripts/ci/smoke-artifact.sh builds --with-ui and packages the real archive, so
# it sets SMOKE_REQUIRE_UI=1 and Phase 15's "no embedded assets" SKIP becomes a
# FAILURE there. The fast PR lane deliberately builds without the frontend to
# skip an npm build on every product PR, so it must not assert UI presence.
REQUIRE_UI="${SMOKE_REQUIRE_UI:-0}"
# Unset (the local + PR default): synthesize the release sidecars from this
# checkout. Set: take them from an EXTRACTED release artifact, so the release
@@ -73,7 +71,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 +79,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 +122,25 @@ 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.
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[@]}"
# 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-${OS}-${ARCH}.tar.gz"
tar -czf "$FIXTURE_DIR/$EXPECTED_ARTIFACT" -C "$FIXTURE_DIR" \
codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md
# 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"
"$FIXTURE_DIR/codebase-memory-mcp-${OS}-${ARCH}-portable.tar.gz"
fi
(cd "$FIXTURE_DIR" && { sha256sum *.tar.gz > checksums.txt 2>/dev/null ||
shasum -a 256 *.tar.gz > checksums.txt; })
+54 -166
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
@@ -3318,12 +3273,9 @@ if [ "$DL_OS" = "darwin" ] || [ "$DL_OS" = "linux" ]; then
else
DL_EXT="zip"
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)
# 12a: curl download
echo "--- Phase 12a: curl download ---"
# --noproxy '*': never route the local test server through a proxy — a proxy env
# var present on some runners (notably windows-11-arm) made curl fail to reach
@@ -3331,16 +3283,10 @@ echo "--- Phase 12a: curl download ---"
# surface curl's stderr instead of swallowing it so the reason is visible.
CURL12_ERR="$DL_DIR/curl12a.err"
if ! curl -fSL --noproxy '*' -o "$DL_DIR/$DL_ARCHIVE" "$SMOKE_DOWNLOAD_URL/$DL_ARCHIVE" 2>"$CURL12_ERR"; then
# 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) ---"
cat "$CURL12_ERR" 2>/dev/null || true
exit 1
fi
echo "FAIL 12a: curl download failed"
echo "--- curl stderr (url: $SMOKE_DOWNLOAD_URL/$DL_ARCHIVE) ---"
cat "$CURL12_ERR" 2>/dev/null || true
exit 1
fi
if [ ! -s "$DL_DIR/$DL_ARCHIVE" ]; then
echo "FAIL 12a: downloaded archive is empty"
@@ -3381,8 +3327,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 +3336,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 +3385,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 +3394,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 +3468,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 +3485,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 +3504,24 @@ 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=1 makes the no-assets outcome a FAILURE instead of a SKIP.
# scripts/ci/smoke-artifact.sh sets it because that lane builds --with-ui and
# packages the real archive, so a binary serving no frontend is a defect there.
# The fast PR lane builds without the frontend on purpose and leaves it unset --
# a skip that cannot fail is not a gate, but neither is asserting a property the
# lane deliberately does not produce.
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 +3532,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 +3552,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 +3576,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,
+8 -34
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,15 @@ 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 ==="
echo "=== Step 0l: release archive extractor 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 ==="
echo "=== Step 0m: VirusTotal release-notes + evidence contract ==="
bash "$ROOT/tests/test_vt_release_notes_contract.sh"
echo "=== Step 0n: VirusTotal gate policy contract ==="
bash "$ROOT/tests/test_vt_gate_policy_contract.sh"
# Verify compiler supports target arch
verify_compiler "$CC"
@@ -319,20 +307,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).
+510 -788
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");
+31 -2
View File
@@ -1608,24 +1608,45 @@ static int prepare_existing_generation_for_replace(const char *db_path,
return CBM_PIPELINE_PERSIST_FAILED;
}
memset(prepared, 0, sizeof(*prepared));
/* Every failure edge below logs before returning: a silent PERSIST_FAILED
* surfaces to the user as "Pipeline failed. Check repo_path ..." -- blaming
* a repo that indexed perfectly for a destination-side replacement fault. */
cbm_path_info_t info;
if (cbm_path_info_utf8(db_path, &info) == 0) {
if (!info.is_regular || info.is_symlink) {
cbm_log_error("finalize.prepare_failed", "reason", "destination_not_regular", "path",
db_path);
return CBM_PIPELINE_PERSIST_FAILED;
}
int seal_rc = cbm_store_seal_existing_path_for_replace(db_path);
if (seal_rc == CBM_STORE_NOT_FOUND) {
if (!quarantine_invalid) {
(void)cbm_unlink(db_path);
return cbm_remove_db_sidecars(db_path) == 0 ? 0 : CBM_PIPELINE_PERSIST_FAILED;
if (cbm_remove_db_sidecars(db_path) != 0) {
cbm_log_error("finalize.prepare_failed", "reason",
"invalid_destination_sidecar_cleanup", "path", db_path);
return CBM_PIPELINE_PERSIST_FAILED;
}
return 0;
}
return quarantine_existing_generation(db_path, prepared);
}
if (seal_rc != CBM_STORE_OK) {
char seal_text[16];
(void)snprintf(seal_text, sizeof(seal_text), "%d", seal_rc);
cbm_log_error("finalize.prepare_failed", "reason", "seal_existing", "rc", seal_text,
"path", db_path);
return CBM_PIPELINE_PERSIST_FAILED;
}
}
return cbm_remove_db_sidecars(db_path) == 0 ? 0 : CBM_PIPELINE_PERSIST_FAILED;
if (cbm_remove_db_sidecars(db_path) != 0) {
char errno_text[16];
(void)snprintf(errno_text, sizeof(errno_text), "%d", errno);
cbm_log_error("finalize.prepare_failed", "reason", "sidecar_cleanup", "errno", errno_text,
"path", db_path);
return CBM_PIPELINE_PERSIST_FAILED;
}
return 0;
}
int cbm_pipeline_publish_generation(const cbm_pipeline_generation_t *generation) {
@@ -1807,6 +1828,10 @@ int cbm_pipeline_finalize_staged_generation(char *stage_path, const char *final_
* generation's WAL. */
if (destination_known_healthy) {
if (cbm_remove_db_sidecars(final_db_path) != 0) {
char errno_text[16];
(void)snprintf(errno_text, sizeof(errno_text), "%d", errno);
cbm_log_error("finalize.prepare_failed", "reason", "healthy_sidecar_cleanup", "errno",
errno_text, "path", final_db_path);
cbm_pipeline_discard_stage(stage_path);
return CBM_PIPELINE_PERSIST_FAILED;
}
@@ -1828,6 +1853,10 @@ int cbm_pipeline_finalize_staged_generation(char *stage_path, const char *final_
itoa_buf((int)elapsed_ms(t_fin)));
cbm_clock_gettime(CLOCK_MONOTONIC, &t_fin);
if (cbm_rename_replace(stage_path, final_db_path) != 0) {
char errno_text[16];
(void)snprintf(errno_text, sizeof(errno_text), "%d", errno);
cbm_log_error("finalize.rename_failed", "errno", errno_text, "stage", stage_path, "dest",
final_db_path);
(void)rollback_quarantined_generation(final_db_path, &prepared);
discard_generation_stage(stage_path);
return CBM_PIPELINE_PERSIST_FAILED;
+29 -5
View File
@@ -1215,8 +1215,12 @@ int cbm_store_prepare_path_for_replace(const char *path) {
if (!path) {
return CBM_STORE_ERR;
}
char prepare_path[4096];
if (!cbm_path_for_file_api(path, prepare_path, sizeof(prepare_path))) {
return CBM_STORE_ERR;
}
sqlite3 *db = NULL;
int rc = sqlite3_open_v2(path, &db, SQLITE_OPEN_READWRITE, NULL);
int rc = sqlite3_open_v2(prepare_path, &db, SQLITE_OPEN_READWRITE, NULL);
if (rc != SQLITE_OK) {
sqlite3_close(db);
return CBM_STORE_ERR;
@@ -1237,15 +1241,21 @@ int cbm_store_backup_path(const char *source_path, const char *staging_path) {
return CBM_STORE_ERR;
}
char source_api[4096];
char staging_api[4096];
if (!cbm_path_for_file_api(source_path, source_api, sizeof(source_api)) ||
!cbm_path_for_file_api(staging_path, staging_api, sizeof(staging_api))) {
return CBM_STORE_ERR;
}
sqlite3 *source = NULL;
sqlite3 *dest = NULL;
int rc = sqlite3_open_v2(source_path, &source, SQLITE_OPEN_READONLY, NULL);
int rc = sqlite3_open_v2(source_api, &source, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
sqlite3_close(source);
return CBM_STORE_ERR;
}
sqlite3_busy_timeout(source, 10000);
rc = sqlite3_open_v2(staging_path, &dest, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
rc = sqlite3_open_v2(staging_api, &dest, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
if (rc != SQLITE_OK) {
sqlite3_close(dest);
sqlite3_close(source);
@@ -1354,8 +1364,17 @@ int cbm_store_seal_existing_path_for_replace(const char *db_path) {
return CBM_STORE_ERR;
}
/* Normalize to the Windows file-API form exactly as the primary open does
* (cbm_store_open_internal). SQLite's win VFS needs the wide-round-tripped
* path; the raw UTF-8 string fails to open a file that was WRITTEN through
* the normalized form, so re-sealing a just-indexed generation returned -1
* and finalize misreported it as "repo has no source files". */
char seal_path[4096];
if (!cbm_path_for_file_api(db_path, seal_path, sizeof(seal_path))) {
return CBM_STORE_ERR;
}
cbm_store_t maintenance = {0};
int rc = sqlite3_open_v2(db_path, &maintenance.db, SQLITE_OPEN_READWRITE, NULL);
int rc = sqlite3_open_v2(seal_path, &maintenance.db, SQLITE_OPEN_READWRITE, NULL);
if (rc != SQLITE_OK) {
int primary = rc & 0xff;
sqlite3_close(maintenance.db);
@@ -1427,8 +1446,13 @@ int cbm_store_dump_to_file(cbm_store_t *s, const char *dest_path) {
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", dest_path);
(void)unlink(tmp_path);
char tmp_api[4096];
if (!cbm_path_for_file_api(tmp_path, tmp_api, sizeof(tmp_api))) {
store_set_error(s, "dump: cannot normalize temp file path");
return CBM_STORE_ERR;
}
sqlite3 *dest_db = NULL;
int rc = sqlite3_open(tmp_path, &dest_db);
int rc = sqlite3_open_v2(tmp_api, &dest_db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
if (rc != SQLITE_OK) {
store_set_error(s, "dump: cannot open temp file");
return CBM_STORE_ERR;
-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 -76
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,13 @@ 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:
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 +58,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
@@ -71,51 +69,16 @@ else
fi
SMOKE_ARCH="${SMOKE_ARCH:-arm64}"
SMOKE_VARIANT="${SMOKE_VARIANT:-standard}"
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
# supplying a standard binary can no longer pass quietly.
case "$SMOKE_VARIANT" in
standard) SUFFIX="" ; REQUIRE_UI=0 ;;
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
# Whether the UI must be present is the CALLER's claim, not this script's:
# scripts/ci/smoke-artifact.sh builds --with-ui and packages the real archive, so
# it sets SMOKE_REQUIRE_UI=1 and Phase 15's "no embedded assets" SKIP becomes a
# FAILURE there. The fast PR lane deliberately builds without the frontend to
# skip an npm build on every product PR, so it must not assert UI presence.
REQUIRE_UI="${SMOKE_REQUIRE_UI:-0}"
PROFILE_ROOT="$(cygpath -u "$USERPROFILE")"
SMOKE_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-vm-smoke.XXXXXX")"
@@ -178,45 +141,23 @@ 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.
EXPECTED_ARTIFACT="codebase-memory-mcp${SUFFIX}-windows-${SMOKE_ARCH}.zip"
# Member set + ORDER mirror scripts/package-release.sh (Windows).
EXPECTED_ARTIFACT="codebase-memory-mcp-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
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
+10 -7
View File
@@ -962,17 +962,20 @@ TEST(ui_server_free_never_joins_active_index_worker) {
PASS();
}
TEST(ui_server_root_without_ready_assets_is_retryable) {
/* A listener should normally be gated on READY by the daemon. Direct
* constructor tests still receive a truthful retryable response. */
TEST(ui_server_root_without_embedded_assets_is_not_found) {
/* The frontend is linked into the image, so its availability is decided at
* build time, not warmed at runtime: a binary built without --with-ui has
* no index.html and never will. That is a permanent 404, NOT a retryable
* 503 -- promising a retry for a condition that cannot change would make
* every client poll forever. */
th_server_t ts;
ASSERT_EQ(th_server_start(&ts), 0);
char resp[4096];
int n = th_http(cbm_http_server_port(ts.srv), "GET / HTTP/1.1\r\n\r\n", resp, sizeof(resp));
ASSERT_GT(n, 0);
ASSERT_EQ(th_status(resp), 503);
ASSERT_NOT_NULL(strstr(resp, "frontend assets are not ready"));
ASSERT_NOT_NULL(strstr(resp, "Retry-After: 1"));
ASSERT_EQ(th_status(resp), 404);
ASSERT_NOT_NULL(strstr(resp, "no frontend embedded"));
ASSERT_NULL(strstr(resp, "Retry-After"));
th_server_stop(&ts);
PASS();
}
@@ -2290,7 +2293,7 @@ SUITE(httpd) {
RUN_TEST(ui_server_process_kill_route_is_unavailable);
RUN_TEST(ui_server_routes_indexing_through_joinable_daemon_executor);
RUN_TEST(ui_server_free_never_joins_active_index_worker);
RUN_TEST(ui_server_root_without_ready_assets_is_retryable);
RUN_TEST(ui_server_root_without_embedded_assets_is_not_found);
RUN_TEST(ui_server_same_origin_request_is_allowed);
RUN_TEST(ui_server_rejects_foreign_and_null_origins);
RUN_TEST(ui_server_mutations_require_json_content_type);
-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
+158 -551
View File
@@ -1,592 +1,199 @@
#!/usr/bin/env bash
# Contract for the release scan boundary: archives are validated and hashed;
# only exact extracted members and independently parsed UI-pack assets enter
# the byte-deduplicated, atomically published scan set.
# Contract: scripts/ci/extract-release-archives.sh produces the exact scan bundle
# the VirusTotal gate consumes.
#
# The gate is only as good as this manifest. If the extractor silently skipped a
# member, mis-deduplicated bytes, or let an archive container into the scan set,
# check-virustotal.sh would still report a confident green over an incomplete
# set. These assertions are what make "0 detections" mean the whole release.
#
# The pack-format half of the previous contract is gone with the UI pack itself;
# everything asserted here applies to the four-member single-composition archive.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
FIX="$(mktemp -d "${TMPDIR:-/tmp}/cbm-extract-contract.XXXXXX")"
trap 'rm -rf "$FIX"' EXIT
python3 - "$ROOT" "$BASH" <<'PY'
from __future__ import annotations
import csv
# "$BASH" by explicit argv: BASH is a shell variable, NOT exported, so reading
# it from os.environ inside Python silently falls back — and on native Windows a
# bare "bash" resolves to the WSL stub, which fails every invocation with
# "Windows Subsystem for Linux has no installed distributions".
python3 - "$ROOT" "$FIX" "$BASH" <<'PY'
import hashlib
import io
import pathlib
import stat
import struct
import subprocess
import sys
import tarfile
import tempfile
import warnings
import zipfile
from typing import Dict, List, Optional, Tuple
root = pathlib.Path(sys.argv[1])
bash_executable = pathlib.Path(sys.argv[2])
extractor = root / "scripts/ci/extract-release-archives.sh"
unix_targets = (
"linux-amd64",
"linux-arm64",
"darwin-amd64",
"darwin-arm64",
"linux-amd64-portable",
"linux-arm64-portable",
)
windows_targets = ("windows-amd64", "windows-arm64")
archive_names = tuple(
sorted(
[
f"codebase-memory-mcp{suffix}-{target}.tar.gz"
for suffix in ("", "-ui")
for target in unix_targets
]
+ [
f"codebase-memory-mcp{suffix}-{target}.zip"
for suffix in ("", "-ui")
for target in windows_targets
]
)
)
ui_archive_names = tuple(name for name in archive_names if name.startswith("codebase-memory-mcp-ui-"))
expected_base_counts = {
"archives": 16,
"binaries": 16,
"packs": 8,
"runtime_files": 64,
}
count_args = [
f"--expect-{key.replace('_', '-')}={value}"
for key, value in expected_base_counts.items()
]
ui_expected_base_counts = {
"archives": 8,
"binaries": 8,
"packs": 8,
"runtime_files": 32,
}
ui_count_args = [
"--archive-scope=ui",
*[
f"--expect-{key.replace('_', '-')}={value}"
for key, value in ui_expected_base_counts.items()
],
]
asset_payloads = {
"/assets/app.css": b"body{color:#123}\n",
"/assets/app.js": b"globalThis.__CBM_UI__ = true;\n",
"/index.html": b"<!doctype html><script src='/assets/app.js'></script>\n",
}
mime_ids = {".html": 1, ".js": 2, ".css": 3}
fixtures = pathlib.Path(sys.argv[2])
bash_executable = sys.argv[3]
extractor = root / "scripts" / "ci" / "extract-release-archives.sh"
UNIX = ("linux-amd64", "linux-arm64", "darwin-amd64", "darwin-arm64",
"linux-amd64-portable", "linux-arm64-portable")
WINDOWS = ("windows-amd64", "windows-arm64")
ARCHIVES = tuple(f"codebase-memory-mcp-{t}.tar.gz" for t in UNIX) + \
tuple(f"codebase-memory-mcp-{t}.zip" for t in WINDOWS)
# Deliberately byte-IDENTICAL across every archive: the extractor must collapse
# them to one scan object each, or the gate pays to scan the same bytes 8 times
# and the association count stops meaning "distinct bytes".
SHARED_LICENSE = b"shared release license\n"
SHARED_NOTICES = b"shared third-party notices\n"
SHARED_SH = b"#!/bin/sh\n# shared installer\n"
SHARED_PS1 = b"Write-Output 'shared installer'\n"
failures = []
def fail(message: str) -> None:
raise AssertionError(message)
def fail(message):
failures.append(message)
def is_ui(name: str) -> bool:
return name.startswith("codebase-memory-mcp-ui-")
def make_pack(payloads: Dict[str, bytes]) -> bytes:
entries = sorted(payloads.items())
paths = [path.encode("ascii") for path, _ in entries]
index_size = len(entries) * 24
paths_size = sum(len(path) for path in paths)
payload_size = sum(len(data) for _, data in entries)
paths_offset = 80 + index_size
payload_offset = paths_offset + paths_size
total = payload_offset + payload_size
pack = bytearray(total)
struct.pack_into(
"<8sHHIIIQQQQQQQ",
pack,
0,
b"CBMUIPK\0",
1,
80,
0,
len(entries),
24,
80,
index_size,
paths_offset,
paths_size,
payload_offset,
payload_size,
total,
)
path_cursor = 0
payload_cursor = 0
for index, ((path, data), raw_path) in enumerate(zip(entries, paths)):
extension = pathlib.PurePosixPath(path).suffix
struct.pack_into(
"<IHBBQQ",
pack,
80 + index * 24,
path_cursor,
len(raw_path),
mime_ids[extension],
1 if path == "/index.html" else 2,
payload_cursor,
len(data),
)
pack[paths_offset + path_cursor : paths_offset + path_cursor + len(raw_path)] = raw_path
pack[payload_offset + payload_cursor : payload_offset + payload_cursor + len(data)] = data
path_cursor += len(raw_path)
payload_cursor += len(data)
return bytes(pack)
valid_pack = make_pack(asset_payloads)
def mutate_pack(kind: str) -> bytes:
data = bytearray(valid_pack)
if kind == "bad_pack_magic":
data[0] = ord("X")
elif kind == "bad_pack_bounds":
struct.pack_into("<Q", data, 72, len(data) + 1)
elif kind == "bad_pack_path":
_, _, _, _, _, _, _, index_size, paths_offset, _, _, _, _ = struct.unpack_from(
"<8sHHIIIQQQQQQQ", data, 0
)
del index_size
first_length = struct.unpack_from("<H", data, 84)[0]
replacement = b"/evilxx/app.css"
if len(replacement) != first_length:
fail("bad-path fixture length drifted")
data[paths_offset : paths_offset + first_length] = replacement
elif kind == "bad_pack_mime":
data[86] = 2 # first entry is app.css, whose closed MIME id is 3
else:
fail(f"unknown pack mutation: {kind}")
return bytes(data)
def regular(name: str, data: bytes) -> Dict[str, object]:
return {"name": name, "kind": "regular", "data": data}
def archive_entries(name: str, mutation: Optional[str] = None) -> List[Dict[str, object]]:
marker = name.encode("ascii")
windows = name.endswith(".zip")
def members(archive):
windows = archive.endswith(".zip")
binary = "codebase-memory-mcp.exe" if windows else "codebase-memory-mcp"
installer = "install.ps1" if windows else "install.sh"
entries = [
regular(binary, b"binary bytes from " + marker + b"\n"),
regular("cbm-integrations.json", b'{"schema":1,"integrations":[]}\n'),
regular("LICENSE", b"shared release license\n"),
regular(
installer,
b"Write-Output 'shared installer'\n"
if windows
else b"#!/bin/sh\n# shared installer\n",
),
regular("THIRD_PARTY_NOTICES.md", b"shared third-party notices\n"),
]
if is_ui(name):
pack = mutate_pack(mutation) if mutation and mutation.startswith("bad_pack_") else valid_pack
entries.append(regular(f"cbm-ui-{hashlib.sha256(pack).hexdigest()}.pack", pack))
return entries
def write_archive(path: pathlib.Path, entries: List[Dict[str, object]]) -> None:
if path.name.endswith(".tar.gz"):
with tarfile.open(path, "w:gz") as archive:
for entry in entries:
name = str(entry["name"])
kind = str(entry["kind"])
info = tarfile.TarInfo(name)
info.mtime = 1
if kind == "regular":
data = bytes(entry["data"])
info.mode = 0o755 if name == "codebase-memory-mcp" else 0o644
info.size = len(data)
archive.addfile(info, io.BytesIO(data))
elif kind == "directory":
info.type = tarfile.DIRTYPE
info.mode = 0o755
archive.addfile(info)
elif kind == "symlink":
info.type = tarfile.SYMTYPE
info.linkname = str(entry["target"])
archive.addfile(info)
else:
fail(f"unknown fixture entry kind: {kind}")
return
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
for entry in entries:
name = str(entry["name"])
kind = str(entry["kind"])
info = zipfile.ZipInfo(name)
info.create_system = 3
if kind == "regular":
mode = 0o755 if name == "codebase-memory-mcp.exe" else 0o644
info.external_attr = (stat.S_IFREG | mode) << 16
archive.writestr(info, bytes(entry["data"]))
elif kind == "directory":
if not name.endswith("/"):
info.filename += "/"
info.external_attr = ((stat.S_IFDIR | 0o755) << 16) | 0x10
archive.writestr(info, b"")
elif kind == "symlink":
info.external_attr = (stat.S_IFLNK | 0o777) << 16
archive.writestr(info, str(entry["target"]).encode("ascii"))
else:
fail(f"unknown fixture entry kind: {kind}")
Association = Tuple[str, str, str, str]
def build_matrix(
archive_dir: pathlib.Path,
mutation: Optional[str] = None,
selected_names: Tuple[str, ...] = archive_names,
) -> Dict[Association, bytes]:
archive_dir.mkdir(parents=True)
expected: Dict[Association, bytes] = {}
targets = {
"bad_hash": "codebase-memory-mcp-ui-linux-amd64.tar.gz",
"bad_pack_magic": "codebase-memory-mcp-ui-linux-amd64.tar.gz",
"bad_pack_bounds": "codebase-memory-mcp-ui-linux-arm64.tar.gz",
"bad_pack_path": "codebase-memory-mcp-ui-darwin-amd64.tar.gz",
"bad_pack_mime": "codebase-memory-mcp-ui-darwin-arm64.tar.gz",
"directory": "codebase-memory-mcp-linux-amd64.tar.gz",
"extra_pack": "codebase-memory-mcp-ui-linux-arm64-portable.tar.gz",
"missing_pack": "codebase-memory-mcp-ui-windows-arm64.zip",
"nested_member": "codebase-memory-mcp-darwin-amd64.tar.gz",
"tar_symlink": "codebase-memory-mcp-linux-arm64.tar.gz",
"tar_duplicate": "codebase-memory-mcp-ui-linux-amd64-portable.tar.gz",
"zip_symlink": "codebase-memory-mcp-windows-amd64.zip",
"zip_directory": "codebase-memory-mcp-windows-arm64.zip",
"zip_duplicate": "codebase-memory-mcp-ui-windows-amd64.zip",
return {
binary: b"binary bytes of " + archive.encode(), # unique per archive
"LICENSE": SHARED_LICENSE,
installer: SHARED_PS1 if windows else SHARED_SH,
"THIRD_PARTY_NOTICES.md": SHARED_NOTICES,
}
target = targets.get(mutation)
for name in selected_names:
if mutation == "missing_archive" and name == "codebase-memory-mcp-linux-amd64.tar.gz":
continue
pack_mutation = mutation if name == target and mutation and mutation.startswith("bad_pack_") else None
entries = archive_entries(name, pack_mutation)
if name == target:
if mutation == "bad_hash":
entries = [entry for entry in entries if not str(entry["name"]).endswith(".pack")]
entries.append(regular(f"cbm-ui-{'0' * 64}.pack", valid_pack))
elif mutation in ("directory", "zip_directory"):
entries.append({"name": "nested/", "kind": "directory"})
elif mutation in ("tar_duplicate", "zip_duplicate"):
entries.append(regular("LICENSE", b"second license body\n"))
elif mutation == "extra_pack":
second = make_pack({
"/assets/other.js": b"console.log('other')\n",
"/index.html": b"<!doctype html>other\n",
})
entries.append(regular(f"cbm-ui-{hashlib.sha256(second).hexdigest()}.pack", second))
elif mutation == "missing_pack":
entries = [entry for entry in entries if not str(entry["name"]).endswith(".pack")]
elif mutation == "nested_member":
entries.append(regular("nested/payload", b"unexpected nested member\n"))
elif mutation in ("tar_symlink", "zip_symlink"):
installer = "install.ps1" if name.endswith(".zip") else "install.sh"
entries = [entry for entry in entries if entry["name"] != installer]
entries.append({"name": installer, "kind": "symlink", "target": "LICENSE"})
path = archive_dir / name
write_archive(path, entries)
if mutation is None:
for entry in entries:
if entry["kind"] != "regular":
continue
member = str(entry["name"])
data = bytes(entry["data"])
expected[("member", name, member, "")] = data
if member.endswith(".pack"):
for asset_path, payload in asset_payloads.items():
expected[("pack_asset", name, member, asset_path)] = payload
if mutation == "unexpected_archive":
unexpected = "codebase-memory-mcp-freebsd-amd64.tar.gz"
write_archive(archive_dir / unexpected, archive_entries("codebase-memory-mcp-linux-amd64.tar.gz"))
if mutation == "oversized_archive":
with (archive_dir / "codebase-memory-mcp-linux-amd64.tar.gz").open("r+b") as handle:
handle.truncate(512 * 1024 * 1024 + 1)
return expected
def invoke(
archive_dir: pathlib.Path,
output_dir: pathlib.Path,
args: Optional[List[str]] = None,
) -> subprocess.CompletedProcess:
def write_archive(directory, archive, extra=None, drop=None):
entries = members(archive)
if drop:
entries.pop(drop, None)
if extra:
entries[extra] = b"unexpected\n"
path = directory / archive
if archive.endswith(".zip"):
with zipfile.ZipFile(path, "w") as zf:
for name, data in entries.items():
zf.writestr(name, data)
else:
with tarfile.open(path, "w:gz") as tf:
for name, data in entries.items():
info = tarfile.TarInfo(name)
info.size = len(data)
tf.addfile(info, __import__("io").BytesIO(data))
return path
def build_matrix(directory, **kwargs):
directory.mkdir(parents=True, exist_ok=True)
for archive in ARCHIVES:
write_archive(directory, archive, **kwargs)
return directory
def run_extractor(archive_dir, out_dir, *args):
return subprocess.run(
[
str(bash_executable),
str(extractor),
str(archive_dir),
str(output_dir),
*(args or count_args),
],
cwd=root,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
check=False,
[bash_executable, str(extractor), str(archive_dir), str(out_dir), *args],
capture_output=True, text=True,
)
def parse_manifest(path: pathlib.Path, marker: str) -> Tuple[Dict[str, int], List[Dict[str, str]]]:
def read_manifest(path, marker):
lines = path.read_text(encoding="utf-8").splitlines()
if not lines or lines[0] != f"# {marker}":
fail(f"{path.name} is missing marker {marker}")
metadata: Dict[str, int] = {}
cursor = 1
while cursor < len(lines) and lines[cursor].startswith("# "):
key, value = lines[cursor][2:].split("=", 1)
metadata[key] = int(value)
cursor += 1
return metadata, list(csv.DictReader(lines[cursor:], delimiter="\t"))
fail(f"{path.name} does not start with its marker {marker!r}")
return {}, []
meta, rows, header = {}, [], None
for line in lines[1:]:
if line.startswith("# "):
key, _, value = line[2:].partition("=")
meta[key] = int(value) if value.isdigit() else value
elif header is None:
header = line.split("\t")
else:
rows.append(dict(zip(header, line.split("\t"))))
return meta, rows
def assert_clean_failure(case_root: pathlib.Path, mutation: str, message: str) -> None:
archive_dir = case_root / "archives"
output_dir = case_root / "scan"
build_matrix(archive_dir, mutation)
result = invoke(archive_dir, output_dir)
if result.returncode == 0 or message not in result.stdout:
fail(
f"{mutation}: extractor must fail with {message!r}; "
f"rc={result.returncode}, output={result.stdout.strip()}"
)
if output_dir.exists():
fail(f"{mutation}: rejected archives left a partial atomic scan bundle")
# ── 1. The happy path: exact matrix in, exact bundle out ────────────────────
good = build_matrix(fixtures / "good" / "archives")
out = fixtures / "good" / "scan"
result = run_extractor(good, out, "--expect-archives=8", "--expect-binaries=8",
"--expect-runtime-files=24")
if result.returncode != 0:
fail(f"exact release matrix was rejected: {result.stdout[-600:]}{result.stderr[-600:]}")
else:
published = sorted(p.name for p in out.iterdir())
if published != ["associations.tsv", "objects", "scan-set.tsv"]:
fail(f"scan bundle must publish exactly objects/ plus its two manifests: {published}")
assoc_meta, assoc = read_manifest(out / "associations.tsv",
"cbm-release-scan-associations-v3")
set_meta, scan_set = read_manifest(out / "scan-set.tsv", "cbm-release-scan-set-v2")
if len(archive_names) != 16 or len(ui_archive_names) != 8:
fail("fixture matrix no longer describes 16 canonical archives / eight UI archives")
# Every member of every archive is covered — 8 archives x 4 members.
if len(assoc) != 32:
fail(f"association manifest must cover all 32 extracted members, got {len(assoc)}")
with tempfile.TemporaryDirectory(prefix="cbm-release-scan-contract-") as temporary:
work = pathlib.Path(temporary)
valid = work / "valid"
expected = build_matrix(valid / "archives")
output = valid / "scan"
result = invoke(valid / "archives", output)
if result.returncode != 0:
fail(f"canonical release matrix was rejected: {result.stdout.strip()}")
if {path.name for path in output.iterdir()} != {"objects", "associations.tsv", "scan-set.tsv"}:
fail("scan bundle must publish exactly objects/ plus its two manifests")
# An archive container must never be scanned as if it were a member.
if any(row["member"] in ARCHIVES for row in assoc):
fail("archive containers must not appear in the association set")
association_metadata, rows = parse_manifest(
output / "associations.tsv", "cbm-release-scan-associations-v3"
)
expected_pack_assets = 8 * len(asset_payloads)
expected_associations = 88 + expected_pack_assets
expected_metadata = {
**expected_base_counts,
"pack_assets": expected_pack_assets,
"associations": expected_associations,
"scan_objects": len(set(expected.values())),
}
if association_metadata != expected_metadata:
fail(f"association metadata mismatch: {association_metadata!r}")
if len(rows) != expected_associations:
fail("association manifest does not contain every extracted member/asset association")
actual_associations = {
(row["association_type"], row["archive"], row["member"], row["asset_path"]): row
for row in rows
}
if any(row["association_type"] == "archive" or row["kind"] == "archive" for row in rows):
fail("archive containers must not appear in the VirusTotal association set")
if set(actual_associations) != set(expected):
fail("association manifest keys differ from exact shipped associations")
archive_hashes = {
name: hashlib.sha256((valid / "archives" / name).read_bytes()).hexdigest()
for name in archive_names
}
for association, data in expected.items():
row = actual_associations[association]
object_path = output / row["scan_path"]
if object_path.read_bytes() != data:
fail(f"scan object changed bytes for association {association!r}")
if row["object_sha256"] != hashlib.sha256(data).hexdigest() or int(row["size"]) != len(data):
fail(f"association digest/size is wrong for {association!r}")
if row["archive_sha256"] != archive_hashes[row["archive"]]:
fail(f"association archive provenance is wrong for {association!r}")
# Deduplication: identical bytes collapse, unique bytes do not.
objects = {row["scan_path"] for row in assoc}
if len(scan_set) != len(objects):
fail(f"scan-set rows ({len(scan_set)}) differ from distinct objects ({len(objects)})")
licences = {row["scan_path"] for row in assoc if row["member"] == "LICENSE"}
if len(licences) != 1:
fail(f"8 byte-identical LICENSE members must map to ONE scan object, got {len(licences)}")
binaries = {row["scan_path"] for row in assoc if row["kind"] == "binary"}
if len(binaries) != 8:
fail(f"8 distinct binaries must stay 8 scan objects, got {len(binaries)}")
object_paths = sorted((output / "objects").iterdir())
if len(object_paths) != len(set(expected.values())):
fail("byte-identical archive members/assets were not deduplicated")
object_bytes = [path.read_bytes() for path in object_paths]
if len(object_bytes) != len(set(object_bytes)):
fail("scan set contains duplicate byte sequences")
license_rows = [row for row in rows if row["member"] == "LICENSE"]
if len(license_rows) != 16 or len({row["scan_path"] for row in license_rows}) != 1:
fail("shared LICENSE bytes must map 16 associations to one scan object")
pack_rows = [row for row in rows if row["kind"] == "pack"]
if len(pack_rows) != 8 or len({row["scan_path"] for row in pack_rows}) != 1:
fail("shared UI pack bytes must map eight associations to one scan object")
script_rows = [row for row in rows if row["asset_path"] == "/assets/app.js"]
if len(script_rows) != 8 or len({row["scan_path"] for row in script_rows}) != 1:
fail("each pack's JavaScript association must reach one exact deduplicated script object")
# The counts the gate reads back must agree with the rows.
if assoc_meta.get("associations") != len(assoc):
fail("association metadata disagrees with its own rows")
if set_meta.get("scan_objects") != len(scan_set):
fail("scan-set metadata disagrees with its own rows")
scan_metadata, scan_rows = parse_manifest(output / "scan-set.tsv", "cbm-release-scan-set-v2")
if scan_metadata != {
"scan_objects": expected_metadata["scan_objects"],
"associations": expected_associations,
}:
fail(f"scan-set metadata mismatch: {scan_metadata!r}")
if len(scan_rows) != len(object_paths):
fail("scan-set row count differs from staged distinct objects")
scan_paths = {row["scan_path"] for row in scan_rows}
if scan_paths != {f"objects/{path.name}" for path in object_paths}:
fail("scan-set paths differ from staged object paths")
if sum(int(row["association_count"]) for row in scan_rows) != expected_associations:
fail("scan-set association counts do not cover the association manifest")
for row in scan_rows:
path = output / row["scan_path"]
data = path.read_bytes()
if row["sha256"] != hashlib.sha256(data).hexdigest() or int(row["size"]) != len(data):
fail(f"scan-set digest/size does not bind {row['scan_path']}")
# Every staged object must exist with the hash the manifest claims.
for row in scan_set:
staged = out / row["scan_path"]
if not staged.is_file():
fail(f"scan-set names a missing object: {row['scan_path']}")
elif hashlib.sha256(staged.read_bytes()).hexdigest() != row["sha256"]:
fail(f"staged object does not match its recorded hash: {row['scan_path']}")
ui_only = work / "ui-only"
ui_expected = build_matrix(ui_only / "archives", selected_names=ui_archive_names)
ui_output = ui_only / "scan"
ui_result = invoke(ui_only / "archives", ui_output, ui_count_args)
if ui_result.returncode != 0:
fail(f"canonical UI-only matrix was rejected: {ui_result.stdout.strip()}")
ui_metadata, ui_rows = parse_manifest(
ui_output / "associations.tsv", "cbm-release-scan-associations-v3"
)
ui_associations = 48 + expected_pack_assets
if ui_metadata != {
**ui_expected_base_counts,
"pack_assets": expected_pack_assets,
"associations": ui_associations,
"scan_objects": len(set(ui_expected.values())),
}:
fail(f"UI-only association metadata mismatch: {ui_metadata!r}")
if len(ui_rows) != ui_associations or {row["variant"] for row in ui_rows} != {"ui"}:
fail("UI-only scan scope does not cover exactly the eight UI archives")
ui_as_all_output = ui_only / "scan-as-all"
ui_as_all = invoke(ui_only / "archives", ui_as_all_output, count_args)
if (
ui_as_all.returncode == 0
or "expected exact canonical 16 (all)" not in ui_as_all.stdout
or ui_as_all_output.exists()
):
fail("UI-only archives must require an explicit closed archive scope")
failure_cases = {
"bad_hash": "UI pack digest does not match its filename",
"bad_pack_magic": "CBMUIPK header/offset contract failed",
"bad_pack_bounds": "CBMUIPK header/offset contract failed",
"bad_pack_path": "CBMUIPK asset path is invalid",
"bad_pack_mime": "CBMUIPK MIME/path contract failed",
"directory": "non-regular archive member",
"extra_pack": "exactly one UI pack",
"missing_archive": "expected exact canonical 16 (all)",
"missing_pack": "exactly one UI pack",
"nested_member": "unexpected archive member",
"oversized_archive": "archive exceeds 536870912 byte ceiling",
"tar_symlink": "non-regular archive member",
"tar_duplicate": "duplicate archive member",
"unexpected_archive": "expected exact canonical 16 (all)",
"zip_directory": "non-regular archive member",
"zip_duplicate": "duplicate archive member",
"zip_symlink": "non-regular archive member",
}
for mutation, message in failure_cases.items():
case_root = work / mutation
case_root.mkdir()
assert_clean_failure(case_root, mutation, message)
wrong_count = work / "wrong-count"
wrong_count.mkdir()
build_matrix(wrong_count / "archives")
wrong_args = [arg for arg in count_args if not arg.startswith("--expect-packs=")]
wrong_args.append("--expect-packs=9")
wrong_output = wrong_count / "scan"
wrong = invoke(wrong_count / "archives", wrong_output, wrong_args)
if wrong.returncode == 0 or "packs expected 9, got 8" not in wrong.stdout or wrong_output.exists():
fail(f"explicit expected-count mismatch did not fail atomically: {wrong.stdout.strip()}")
source = extractor.read_text(encoding="utf-8")
if "files_equal(candidate, existing.path)" not in source:
fail("deduplication no longer exact-compares bytes after SHA-256+size grouping")
if source.count("archive_object.path,") < 2:
fail("members are not parsed from the same retained archive bytes used for provenance")
for ceiling in ("MAX_ARCHIVE_BYTES", "MAX_MEMBER_BYTES", "MAX_TOTAL_MEMBER_BYTES"):
if ceiling not in source:
fail(f"extractor lost required size ceiling {ceiling}")
release = (root / ".github/workflows/release.yml").read_text(encoding="utf-8")
dry_run = (root / ".github/workflows/dry-run.yml").read_text(encoding="utf-8")
build = (root / ".github/workflows/_build.yml").read_text(encoding="utf-8")
smoke = (root / ".github/workflows/_smoke.yml").read_text(encoding="utf-8")
if "ui_only" in build or "Build standard binary" in build:
fail("canonical build workflow must not retain a non-UI release path")
if build.count("--variant ui") != 4:
fail("canonical build workflow must package exactly its four UI-enabled build families")
if "ui_only" in smoke or "VARIANTS='[\"ui\"]'" not in smoke or "standard" in smoke.split("VARIANTS=", 1)[1].split("\n", 1)[0]:
fail("release smoke matrix must expose only the UI-enabled runtime")
if "ui_only" in dry_run or "--archive-scope=ui" not in dry_run:
fail("dry-run must use the canonical UI-only build/smoke/extraction path")
if "--archive-scope=ui" not in release:
fail("release verification must accept only the shipped UI-enabled archives")
if (
"path: ${{ runner.temp }}/release-archives" not in dry_run
or 'extract-release-archives.sh "$RUNNER_TEMP/release-archives" binaries' not in dry_run
or "path: assets" in dry_run
or "extract-release-archives.sh assets binaries" in dry_run
# ── 2. Fail-closed cases — each must be REFUSED, not silently scanned ───────
for label, kwargs, expect in (
("an unexpected extra member", {"extra": "surprise.txt"}, "unexpected archive member"),
("a missing required member", {"drop": "LICENSE"}, "member namespace mismatch"),
):
fail("dry-run must isolate downloaded archives from tracked checkout assets")
if (
'ARCHIVE_DIR="$RUNNER_TEMP/release-archives"' not in release
or '--dir "$ARCHIVE_DIR"' not in release
or 'extract-release-archives.sh "$ARCHIVE_DIR" binaries' not in release
or "--dir assets" in release
or "extract-release-archives.sh assets binaries" in release
):
fail("release verification must isolate downloaded archives from tracked checkout assets")
for workflow_name, workflow, arguments in (
("release", release, ui_count_args),
("dry-run", dry_run, ui_count_args),
):
for argument in arguments:
if argument not in workflow:
fail(f"{workflow_name} workflow does not assert {argument}")
for required in (
"files: binaries/objects/*",
"request_rate: 4",
"VT_EXPECTED_SCAN_SET: binaries/scan-set.tsv",
"VT_ASSOCIATIONS: binaries/associations.tsv",
"VT_RESULTS_PATH: binaries/vt-results.tsv",
"MIN_ENGINES: 50",
"actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a",
"virustotal-evidence-",
):
if required not in workflow:
fail(f"{workflow_name} workflow is missing exact scan/gate contract: {required}")
lowered = workflow.lower()
for forbidden in ("false-positive", "false positive", "submission", "reputation"):
if forbidden in lowered:
fail(f"{workflow_name} workflow prescribes forbidden scan-cleaning policy: {forbidden}")
if "naturally clean" not in lowered:
fail(f"{workflow_name} workflow must state the naturally-clean baseline")
case = fixtures / label.replace(" ", "_")
bad = build_matrix(case / "archives", **kwargs)
result = run_extractor(bad, case / "scan")
if result.returncode == 0:
fail(f"extractor accepted {label}")
elif expect not in (result.stdout + result.stderr):
fail(f"{label} was rejected without naming the contract: "
f"{(result.stdout + result.stderr)[-300:]}")
notes = (root / "scripts/ci/append-vt-notes.sh").read_text(encoding="utf-8")
for required in (
"cbm-security-verification:start",
"cbm-security-verification:end",
"binaries/associations.tsv",
"binaries/vt-results.tsv",
):
if required not in notes:
fail(f"release-note updater is missing {required}")
if "70+" in notes or "...`" in notes:
fail("release notes must use measured engine counts and full SHA-256 values")
# An incomplete matrix must never satisfy an exact count.
short = fixtures / "short" / "archives"
short.mkdir(parents=True)
write_archive(short, ARCHIVES[0])
result = run_extractor(short, fixtures / "short" / "scan", "--expect-archives=8")
if result.returncode == 0:
fail("extractor accepted a 1-archive matrix under --expect-archives=8")
print("release archive extractor contract: OK")
if failures:
print("RELEASE ARCHIVE EXTRACTOR CONTRACT VIOLATED:")
for message in failures:
print(f" - {message}")
sys.exit(1)
print("release archive extractor contract OK "
"(8-archive matrix, 32 member associations, dedup exact, fail-closed on "
"surplus/missing members and short matrices)")
PY
+12 -361
View File
@@ -5,23 +5,19 @@ set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
python3 - "$ROOT" "$BASH" <<'PY'
python3 - "$ROOT" <<'PY'
from __future__ import annotations
import hashlib
import os
import pathlib
import re
import subprocess
import sys
import tarfile
import tempfile
import time
import urllib.request
root = pathlib.Path(sys.argv[1])
bash_executable = pathlib.Path(sys.argv[2])
failures: list[str] = []
@@ -48,32 +44,9 @@ local_ci = read("test-infrastructure/run.sh")
windows_vm = read("test-infrastructure/vm/win.sh")
windows_vm_runner = read("test-infrastructure/vm/vm-run-tests.sh")
pr_workflow = read(".github/workflows/pr.yml")
release_workflow = read(".github/workflows/release.yml")
release_extractor = read("scripts/ci/extract-release-archives.sh")
test_driver = read("scripts/test.sh")
cli_source = read("src/cli/cli.c")
helper_source = read(helper_relative)
unix_installer = read("install.sh")
windows_installer = read("install.ps1")
# Keep the security-boundary archive checks runnable as a focused, network-free
# contract while still making them part of the canonical smoke-fixture gate.
release_archive_contract = subprocess.run(
[
str(bash_executable),
str(root / "tests/test_release_archive_extractor_contract.sh"),
],
cwd=root,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
check=False,
)
require(
release_archive_contract.returncode == 0,
"release archive extractor contract failed: "
+ release_archive_contract.stdout.strip(),
)
# The server owns the ephemeral bind. A parent-side socket probe followed by
# python -m http.server would reintroduce the close/rebind race this guards.
@@ -161,23 +134,18 @@ for relative, source in (
f"{relative} must pin isolated {variable}",
)
# Unix fixtures mirror the exact release variant surface.
# Unix fixtures mirror the release archive surface and Linux update aliases.
for name in ("LICENSE", "install.sh", "THIRD_PARTY_NOTICES.md"):
require(name in smoke_local, f"smoke-local.sh archive must include {name}")
# One composition ships, so there is one archive name and no variant alias.
require(
"codebase-memory-mcp${SUFFIX}-${OS}-${ARCH}.tar.gz" in smoke_local
and "UI_PACK_NAME" in smoke_local
and "^cbm-ui-[0-9a-f]{64}\\.pack$" in smoke_local,
"smoke-local.sh must create the selected exact variant with a hash-shaped UI pack",
"codebase-memory-mcp-${OS}-${ARCH}.tar.gz" in smoke_local
and "${SUFFIX}" not in smoke_local,
"smoke-local.sh must create the single canonical archive with no variant alias",
)
require(
"codebase-memory-mcp${SUFFIX}-${OS}-${ARCH}-portable.tar.gz" in smoke_local
and 'ARCHIVE_MEMBERS+=("$UI_PACK_NAME")' in smoke_local,
"smoke-local.sh must create the Linux portable selected variant with the same member set",
)
require(
'"$FIXTURE_DIR/codebase-memory-mcp-${OS}-${ARCH}.tar.gz"' not in smoke_local,
"smoke-local.sh must not disguise a six-member UI archive under a standard name",
"codebase-memory-mcp-${OS}-${ARCH}-portable.tar.gz" in smoke_local,
"smoke-local.sh must create the Linux portable update alias",
)
require(
'CBM_CACHE_DIR="$WORK_DIR/cache"' in smoke_local
@@ -195,9 +163,9 @@ require(
"fixture checksums must name exact artifact basenames, never ./-prefixed paths",
)
# Native Windows packages and serves the exact five-file standard bundle or
# six-file UI bundle (one native binary), then runs the full smoke from a
# protected profile-rooted directory/cache.
# Native Windows packages and serves the exact four-file release bundle (ONE
# binary, like every other platform), then runs the full smoke from a protected
# profile-rooted directory/cache.
for name in (
"codebase-memory-mcp.exe",
"LICENSE",
@@ -210,16 +178,6 @@ require(
"vm-smoke.sh must not stage a Windows launcher/payload pair",
)
require("checksums.txt" in vm_smoke, "vm-smoke.sh must generate checksums.txt")
require(
"UI_PACK_NAME" in vm_smoke
and "^cbm-ui-[0-9a-f]{64}\\.pack$" in vm_smoke
and 'ARCHIVE_MEMBERS+=("$UI_PACK_NAME")' in vm_smoke,
"vm-smoke.sh must stage exactly one content-addressed pack only for UI",
)
require(
'"codebase-memory-mcp-windows-${SMOKE_ARCH}.zip"' not in vm_smoke,
"vm-smoke.sh must not disguise a six-member UI zip under a standard name",
)
require(
"SMOKE_DOWNLOAD_URL=" in vm_smoke
and "SMOKE_UPDATE_FIXTURE_DIR=" in vm_smoke
@@ -304,7 +262,7 @@ for service in ("smoke-windows:",):
require(
"wine64 ./build/win-cross/codebase-memory-mcp.exe --version" in compose
and "wine64 cmd /c build/win-cross/codebase-memory-mcp.exe --version" in compose,
"docker-compose Windows cross-smoke must execute the runtime set's one executable through Wine and through a "
"docker-compose Windows cross-smoke must execute the single binary through Wine and through a "
"Wine Windows parent",
)
require(
@@ -347,93 +305,6 @@ require(
"PR Windows smoke must call vm-smoke.sh with SMOKE_ARCH=amd64",
)
smoke_test = read("scripts/smoke-test.sh")
require(
"SMOKE_UI_PACK_CANDIDATES" in smoke_test
and 'cp "$SMOKE_UI_PACK" "$(dirname "$destination")/$(basename "$SMOKE_UI_PACK")"'
in smoke_test,
"smoke-test.sh must stage the UI sidecar with every hand-copied binary",
)
require(
'UNINSTALL_BINARY="$SELF_PATH"' in smoke_test
and '"$CLEAN_UNINSTALLER" uninstall -y -n' in smoke_test
and '"$DBL_UNINSTALLER" uninstall -y -n' in smoke_test
and '"$DBL_RETRY_UNINSTALLER" uninstall -y -n' in smoke_test,
"smoke uninstall cases must run disposable fixture executables, never delete the "
"build input needed by later phases",
)
require(
'exec 9< <(sleep 300)' in smoke_test
and 'UI_STDIN_PID=$!' in smoke_test
and smoke_test.count("stop_ui_probe") >= 4
and 'sleep 300 | "$BINARY"' not in smoke_test,
"the UI readiness probe must own and reap its stdin keeper instead of waiting on a "
"background pipeline",
)
require(
'"$REPO_ROOT/install.sh" "$DL_VARIANT_ARG"' in smoke_test
and '"$WIN_SCRIPT" "$DL_VARIANT_ARG"' in smoke_test
and "install.ps1 did not publish exactly one UI asset pack" in smoke_test,
"Phase 13 must call each direct installer with the downloaded variant and verify its pack",
)
require(
all(
needle in unix_installer
for needle in (
"ARCHIVE_MEMBER_COUNT",
"EXPECTED_MEMBER_COUNT=5",
"EXPECTED_MEMBER_COUNT=6",
"^cbm-ui-[0-9a-f]{64}\\.pack$",
"release archive contains unexpected member",
)
),
"install.sh must validate the exact five-member standard or six-member UI archive",
)
unix_binary_publish = unix_installer.find('"$DLBIN" install "${INSTALL_ARGS[@]}"')
require(
unix_binary_publish >= 0
and "publish_required_sidecar" not in unix_installer
and 'for stale_pack in "$INSTALL_DIR"/cbm-ui-*.pack' not in unix_installer,
"install.sh must delegate sidecar publication and stale-pack GC to the candidate's "
"guarded native install",
)
require(
"GetRandomFileName" in windows_installer
and "[System.IO.FileMode]::CreateNew" in windows_installer
and windows_installer.count("New-CbmExclusiveSiblingTemp") == 2
and '$Destination.new-$PID' not in windows_installer
and '$InstallerDest.new' not in windows_installer,
"install.ps1 must reserve an unpredictable sibling temp exclusively for the updater only",
)
cli_source = read("src/cli/cli.c")
install_start = cli_source.find("static int cli_install_activate(void *opaque)")
install_end = cli_source.find("int cbm_cmd_install(", install_start)
install_body = cli_source[install_start:install_end] if 0 <= install_start < install_end else ""
runtime_stage = install_body.find("cbm_integration_assets_stage_install")
binary_commit = install_body.find("cli_activation_transaction_commit_validated")
runtime_gc = install_body.find("cli_gc_stale_ui_asset_packs")
runtime_finalize = install_body.find("cli_install_runtime_finalize")
require(
0 <= runtime_stage < binary_commit < runtime_gc < runtime_finalize,
"native install must stage sidecars, activate the binary, GC verified stale packs, and "
"finalize while the activation callback still owns the guard",
)
require(
'ARCHIVE_DIR="$RUNNER_TEMP/release-archives"' in release_workflow
and 'scripts/ci/extract-release-archives.sh "$ARCHIVE_DIR" binaries' in release_workflow,
"release verification must feed the canonical extractor from an isolated archive directory",
)
require(
"cbm-release-scan-associations-v3" in release_extractor
and "cbm-release-scan-set-v2" in release_extractor
and "files_equal(candidate, existing.path)" in release_extractor
and "parse_pack_assets" in release_extractor
and "PACK_NAME.fullmatch" in release_extractor
and "non-regular archive member" in release_extractor
and "duplicate archive member" in release_extractor,
"release extraction must validate and hash every archive, then retain only exact "
"member/UI-asset associations and one scan object per distinct extracted byte sequence",
)
require(
"MSYS2_ARG_CONV_EXCL='*'" in smoke_test
and 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File' in smoke_test
@@ -652,226 +523,6 @@ if helper.is_file():
dns_process.kill()
dns_process.wait(timeout=3)
# POSIX installer lifecycle injection. Preserve the wrapper's PID across exec,
# precreate the two formerly predictable temp paths as foreign symlinks, then
# run the real checksum-authenticated installer. The script no longer publishes
# runtime sidecars at all, and its updater publisher never touches the foreign
# predictable sibling.
if os.name != "nt" and helper.is_file():
with tempfile.TemporaryDirectory(prefix="cbm-installer-temp-contract-") as install_temp:
install_root = pathlib.Path(install_temp)
fixture = install_root / "fixture"
stage = install_root / "stage"
fake_bin = install_root / "fake-bin"
install_dir = install_root / "installed"
fake_home = install_root / "home"
fixture.mkdir()
stage.mkdir()
fake_bin.mkdir()
install_dir.mkdir()
fake_home.mkdir()
candidate = stage / "codebase-memory-mcp"
candidate.write_text(
"#!/usr/bin/env bash\n"
"set -euo pipefail\n"
"case \"${1:-}\" in\n"
" --version) echo 'codebase-memory-mcp fixture';;\n"
" install)\n"
" install_dir=''\n"
" for arg in \"$@\"; do\n"
" case \"$arg\" in --dir=*) install_dir=${arg#--dir=};; esac\n"
" done\n"
" test -n \"$install_dir\"\n"
" cp \"$0\" \"$install_dir/codebase-memory-mcp\"\n"
" chmod 755 \"$install_dir/codebase-memory-mcp\";;\n"
" *) exit 2;;\n"
"esac\n",
encoding="utf-8",
)
candidate.chmod(0o755)
(stage / "cbm-integrations.json").write_bytes(b"{}\n")
(stage / "LICENSE").write_bytes(b"fixture license\n")
(stage / "THIRD_PARTY_NOTICES.md").write_bytes(b"fixture notices\n")
archive_name = "codebase-memory-mcp-linux-amd64-portable.tar.gz"
archive_path = fixture / archive_name
with tarfile.open(archive_path, "w:gz") as archive:
for name in (
"codebase-memory-mcp",
"cbm-integrations.json",
"LICENSE",
"THIRD_PARTY_NOTICES.md",
):
archive.add(stage / name, arcname=name)
archive.add(root / "install.sh", arcname="install.sh")
archive_digest = hashlib.sha256(archive_path.read_bytes()).hexdigest()
(fixture / "checksums.txt").write_text(
f"{archive_digest} {archive_name}\n", encoding="ascii"
)
fake_uname = fake_bin / "uname"
fake_uname.write_text(
"#!/bin/sh\n"
"case \"${1:-}\" in\n"
" -s) echo Linux;;\n"
" -m) echo x86_64;;\n"
" *) echo Linux;;\n"
"esac\n",
encoding="utf-8",
)
fake_uname.chmod(0o755)
foreign_sidecar = install_root / "foreign-sidecar"
foreign_updater = install_root / "foreign-updater"
foreign_sidecar.write_bytes(b"foreign sidecar sentinel\n")
foreign_updater.write_bytes(b"foreign updater sentinel\n")
wrapper_pid_file = install_root / "wrapper-pid"
wrapper = install_root / "run-installer.sh"
wrapper.write_text(
"#!/usr/bin/env bash\n"
"set -euo pipefail\n"
"install_dir=$1\n"
"foreign_sidecar=$2\n"
"foreign_updater=$3\n"
"pid_file=$4\n"
"installer=$5\n"
"ln -s \"$foreign_sidecar\" \"$install_dir/.cbm-integrations.json.$$\"\n"
"ln -s \"$foreign_updater\" \"$install_dir/.install.sh.$$\"\n"
"printf '%s\\n' \"$$\" > \"$pid_file\"\n"
"exec bash \"$installer\" --standard \"--dir=$install_dir\" --skip-config\n",
encoding="utf-8",
)
wrapper.chmod(0o755)
port_file = install_root / "port"
server_log = install_root / "server.log"
with server_log.open("wb") as log_handle:
fixture_server = subprocess.Popen(
[
sys.executable,
str(helper),
"--directory",
str(fixture),
"--port-file",
str(port_file),
],
stdout=log_handle,
stderr=subprocess.STDOUT,
)
try:
port = 0
deadline = time.monotonic() + 30
while time.monotonic() < deadline:
if port_file.is_file():
try:
port_text = port_file.read_text(encoding="ascii").strip()
except OSError:
port_text = ""
if port_text:
port = int(port_text)
break
if fixture_server.poll() is not None:
break
time.sleep(0.02)
require(port > 0, "installer temp-path fixture server did not publish a port")
if port > 0:
environment = os.environ.copy()
environment["HOME"] = str(fake_home)
environment["PATH"] = (
str(fake_bin) + os.pathsep + environment.get("PATH", "")
)
environment["CBM_DOWNLOAD_URL"] = f"http://127.0.0.1:{port}"
installed = subprocess.run(
[
str(wrapper),
str(install_dir),
str(foreign_sidecar),
str(foreign_updater),
str(wrapper_pid_file),
str(root / "install.sh"),
],
env=environment,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
timeout=60,
check=False,
)
require(
installed.returncode == 0,
f"installer temp-path fixture failed to install: {installed.stdout.strip()}",
)
if wrapper_pid_file.is_file():
wrapper_pid = wrapper_pid_file.read_text(encoding="ascii").strip()
predictable_sidecar = (
install_dir / f".cbm-integrations.json.{wrapper_pid}"
)
predictable_updater = install_dir / f".install.sh.{wrapper_pid}"
require(
predictable_sidecar.is_symlink()
and foreign_sidecar.read_bytes()
== b"foreign sidecar sentinel\n",
"install.sh deleted or followed a foreign predictable sidecar temp",
)
require(
predictable_updater.is_symlink()
and foreign_updater.read_bytes()
== b"foreign updater sentinel\n",
"install.sh deleted or followed a foreign predictable updater temp",
)
else:
failures.append("installer temp-path wrapper did not publish its PID")
finally:
fixture_server.terminate()
try:
fixture_server.wait(timeout=3)
except subprocess.TimeoutExpired:
fixture_server.kill()
fixture_server.wait(timeout=3)
# The complete canonical-matrix extraction, pack parsing, association retention,
# byte-exact deduplication and fail-closed malformed-pack cases run above via
# test_release_archive_extractor_contract.sh. Keep the packaging-side digest
# check here because it is a separate boundary before an archive exists.
with tempfile.TemporaryDirectory(prefix="cbm-release-extract-") as extract_temp:
extract_root = pathlib.Path(extract_temp)
bad_pack_name = f"cbm-ui-{'0' * 64}.pack"
bad_pack_bytes = b"content does not match the zero digest\n"
# Packaging checks the content address before it touches the product binary.
# No binary is staged deliberately: the digest error must win over the
# later missing-binary guard or this test would accept an incidental red.
package_build = extract_root / "package-build"
package_output = extract_root / "package-output"
package_build.mkdir()
package_output.mkdir()
(package_build / bad_pack_name).write_bytes(bad_pack_bytes)
package_environment = os.environ.copy()
package_environment["BUILD_DIR"] = str(package_build)
packaged = subprocess.run(
[
str(bash_executable),
str(root / "scripts/package-release.sh"),
"linux",
"amd64",
"--variant",
"ui",
"--out-dir",
str(package_output),
],
cwd=root,
env=package_environment,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
check=False,
)
require(
packaged.returncode != 0
and "UI pack digest does not match its filename" in packaged.stdout,
"release packager accepted a UI pack whose content did not match its filename",
)
if failures:
print("smoke fixture contract: FAIL", file=sys.stderr)
for failure in failures:
+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"
+16
View File
@@ -343,6 +343,22 @@ for path in sorted(shell_surfaces):
"under native Windows process lookup"
)
# ── Release archives are extracted from an ISOLATED directory ──
# The release/dry-run verify jobs used to download archives into the tracked
# checkout, so the extractor scanned a directory that also held repo files.
# Both now stage into $RUNNER_TEMP. This assertion lived in the archive-extractor
# contract, which was deleted with the UI-pack architecture it tested; the fix
# itself is unrelated to packs, so its guard moves here rather than vanishing.
for name in ("release.yml", "dry-run.yml"):
text = (workflows / name).read_text(encoding="utf-8")
if "scripts/ci/extract-release-archives.sh" not in text:
continue
if "$RUNNER_TEMP/release-archives" not in text:
failures.append(
f"{name}: release archives must be downloaded into "
"$RUNNER_TEMP/release-archives, never into the tracked checkout"
)
if failures:
print("VENUE PARITY CONTRACT VIOLATED — one harness, every venue:")
for failure in failures:
@@ -1,7 +1,12 @@
#!/usr/bin/env bash
# Contract: the VT gate is an exact-set, content-bound, minimum-engine,
# zero-tolerance gate. Empty, partial, duplicate, unexpected or malformed
# action output fails before a release can be described as scanned.
# Contract: the VT gate is an exact-set, content-bound, minimum-engine gate.
# Empty, partial, duplicate, unexpected or malformed action output fails before
# a release can be described as scanned.
#
# Detection policy: EXACTLY ONE Microsoft machine-learning (`!ml`) verdict is
# tolerated and disclosed. Two or more engines, any non-`!ml` label, any
# non-Microsoft engine, any suspicious verdict and every infrastructure error
# still block.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
@@ -14,9 +19,12 @@ hash_file() {
sha256sum "$1" 2>/dev/null | awk '{print $1}' || shasum -a 256 "$1" | awk '{print $1}'
}
for needle in defender-endpoint-verification av-endpoint-verify false-positive false_positive; do
# Tripwire for the REVERTED endpoint-verification mechanism specifically. The
# current `!ml` tolerance is a policy branch inside this gate, not a callout to
# an external verification service, and must never become one.
for needle in defender-endpoint-verification av-endpoint-verify; do
if grep -q "$needle" "$GATE"; then
fail "gate references reverted/tolerance machinery '$needle'"
fail "gate references reverted endpoint-verification machinery '$needle'"
fi
done
@@ -117,6 +125,43 @@ cat > "$FIX/responses/named-engine.json" <<EOF
}}}}
EOF
cat > "$FIX/responses/ms-signature.json" <<EOF
{"meta":{"file_info":{"sha256":"$PROBE_SHA","size":$PROBE_SIZE}},
"data":{"id":"ms-signature","type":"analysis","attributes":{"status":"completed",
"stats":{"malicious":1,"suspicious":0,"undetected":59,"harmless":0,
"timeout":0,"confirmed-timeout":0,"failure":0,"type-unsupported":0},
"results":{
"Microsoft":{"category":"malicious","result":"Trojan:Win32/Emotet",
"engine_version":"1.26070","engine_update":"20260808"}
}}}}
EOF
cat > "$FIX/responses/other-engine.json" <<EOF
{"meta":{"file_info":{"sha256":"$PROBE_SHA","size":$PROBE_SIZE}},
"data":{"id":"other-engine","type":"analysis","attributes":{"status":"completed",
"stats":{"malicious":1,"suspicious":0,"undetected":59,"harmless":0,
"timeout":0,"confirmed-timeout":0,"failure":0,"type-unsupported":0},
"results":{
"Microsoft":{"category":"undetected","result":null,
"engine_version":"1.26070","engine_update":"20260808"},
"Kaspersky":{"category":"malicious","result":"Trojan.Generic!ml",
"engine_version":"22","engine_update":"20260808"}
}}}}
EOF
cat > "$FIX/responses/two-engines.json" <<EOF
{"meta":{"file_info":{"sha256":"$PROBE_SHA","size":$PROBE_SIZE}},
"data":{"id":"two-engines","type":"analysis","attributes":{"status":"completed",
"stats":{"malicious":2,"suspicious":0,"undetected":58,"harmless":0,
"timeout":0,"confirmed-timeout":0,"failure":0,"type-unsupported":0},
"results":{
"Microsoft":{"category":"malicious","result":"Trojan:Script/Wacatac.B!ml",
"engine_version":"1.26070","engine_update":"20260808"},
"Kaspersky":{"category":"malicious","result":"Trojan.Generic!ml",
"engine_version":"22","engine_update":"20260808"}
}}}}
EOF
cat > "$FIX/responses/malformed-stats.json" <<EOF
{"meta":{"file_info":{"sha256":"$PROBE_SHA","size":$PROBE_SIZE}},
"data":{"id":"malformed-stats","attributes":{"status":"completed",
@@ -230,16 +275,28 @@ done
fail "completed analysis below the 50-engine policy must block"
grep -q 'only 49/49 decisive engines' "$FIX/last.log" || fail "low-engine failure is not explicit"
[ "$(run_gate "binaries/objects/probe=$(url_for named-engine)")" != "0" ] || \
fail "named-engine detection must block"
# A single Microsoft `!ml` verdict is the tolerated case: it passes, but it is
# reported and recorded rather than silently downgraded to "clean".
[ "$(run_gate "binaries/objects/probe=$(url_for named-engine)")" = "0" ] || \
fail "a single Microsoft !ml detection must be tolerated: $(cat "$FIX/last.log")"
grep -q 'TOLERATED:' "$FIX/last.log" || \
fail "a tolerated detection must be reported as TOLERATED, never as OK"
grep -q 'Microsoft = Trojan:Script/Wacatac.B!ml' "$FIX/last.log" || \
fail "gate must report the flagging engine and label"
grep -q $'objects/probe\t.*\t1\t0\t' "$FIX/work/binaries/vt-results.tsv" || \
fail "blocked completed-set counts were not preserved as workflow evidence"
fail "tolerated detection was not preserved as workflow evidence"
if grep -q 'detected by: Kaspersky' "$FIX/last.log"; then
fail "gate listed a non-flagging engine as a detection"
fi
# Everything adjacent to the tolerated case still blocks.
[ "$(run_gate "binaries/objects/probe=$(url_for ms-signature)")" != "0" ] || \
fail "a Microsoft signature (non-!ml) detection must block"
[ "$(run_gate "binaries/objects/probe=$(url_for other-engine)")" != "0" ] || \
fail "a non-Microsoft detection must block"
[ "$(run_gate "binaries/objects/probe=$(url_for two-engines)")" != "0" ] || \
fail "two flagging engines must block"
for id in wrong-hash wrong-size malformed-stats missing-analysis-id missing-microsoft microsoft-timeout wrong-response-type missing-malicious-stat detail-aggregate-mismatch malformed-microsoft-evidence; do
[ "$(run_gate "binaries/objects/probe=$(url_for "$id")")" != "0" ] || \
fail "$id response must fail closed"
@@ -300,4 +357,4 @@ printf 'tampered\n' > "$FIX/work/binaries/objects/probe"
[ "$(run_gate "$clean_output")" != "0" ] || fail "tampered staged object must block before polling"
grep -q 'changed after extraction' "$FIX/last.log" || fail "tamper failure is not diagnosable"
echo 'PASS: VT gate is exact-set, content-bound, >=50-engine and zero tolerance'
echo 'PASS: VT gate is exact-set, content-bound, >=50-engine; tolerates exactly one Microsoft !ml verdict and nothing else'
+10 -9
View File
@@ -17,14 +17,12 @@ cat > "$FIX/binaries/associations.tsv" <<EOF
# cbm-release-scan-associations-v3
# archives=1
# binaries=1
# packs=0
# runtime_files=0
# pack_assets=1
# runtime_files=1
# associations=2
# scan_objects=2
association_type archive archive_sha256 variant kind member asset_path mime scan_path object_sha256 size
member codebase-memory-mcp-linux-amd64.tar.gz $ARCHIVE_SHA standard binary codebase-memory-mcp objects/member $MEMBER_SHA 40
pack_asset codebase-memory-mcp-linux-amd64.tar.gz $ARCHIVE_SHA ui ui_asset cbm-ui-$ASSET_SHA.pack /assets/app.js application/javascript objects/script $ASSET_SHA 20
member codebase-memory-mcp-linux-amd64.tar.gz $ARCHIVE_SHA release binary codebase-memory-mcp objects/member $MEMBER_SHA 40
member codebase-memory-mcp-linux-amd64.tar.gz $ARCHIVE_SHA release runtime LICENSE objects/license $ASSET_SHA 20
EOF
cat > "$FIX/binaries/vt-results.tsv" <<EOF
# cbm-virustotal-results-v1
@@ -35,7 +33,7 @@ cat > "$FIX/binaries/vt-results.tsv" <<EOF
# max_completed_engines=61
scan_path sha256 size association_count completed_engines total_engines malicious suspicious analysis_id microsoft_category microsoft_engine_version microsoft_engine_update virustotal_url
objects/member $MEMBER_SHA 40 1 59 62 0 0 analysis-a undetected 1.26070 20260808 https://www.virustotal.com/gui/file/$MEMBER_SHA/detection
objects/script $ASSET_SHA 20 1 61 64 0 0 analysis-b https://www.virustotal.com/gui/file/$ASSET_SHA/detection
objects/license $ASSET_SHA 20 1 61 64 0 0 analysis-b undetected 1.26070 20260808 https://www.virustotal.com/gui/file/$ASSET_SHA/detection
EOF
cat > "$FIX/current.md" <<'EOF'
Intro text.
@@ -100,7 +98,7 @@ grep -q 'Intro text.' "$FIX/first.md" || fail "content before marked section was
grep -q 'Outro text.' "$FIX/first.md" || fail "content after marked section was lost"
! grep -q 'stale data' "$FIX/first.md" || fail "stale marked section was appended instead of replaced"
grep -q '2 distinct extracted byte objects' "$FIX/first.md" || fail "unique extracted-object scope missing"
grep -q '2 exact extracted-file associations' "$FIX/first.md" || fail "extracted-file association scope missing"
grep -q '2 exact extracted archive members' "$FIX/first.md" || fail "extracted member scope missing"
grep -q '1 downloadable archive' "$FIX/first.md" || fail "archive provenance scope missing"
grep -q '59–61 decisive engine results' "$FIX/first.md" || fail "measured engine range missing"
grep -q 'Microsoft returned a decisive clean verdict for all 1 executable objects' "$FIX/first.md" || \
@@ -120,7 +118,10 @@ done
! grep -q '70+' "$FIX/first.md" || fail "unmeasured engine claim returned"
run_notes "$FIX/first.md" "$FIX/second.md"
cmp -s "$FIX/first.md" "$FIX/second.md" || fail "marked release-note update is not idempotent"
# python3 rather than cmp/diff: this contract runs on the Windows VM's MSYS2
# shell too, where neither is guaranteed to be on PATH (cmp is not).
python3 -c 'import sys;sys.exit(0 if open(sys.argv[1],"rb").read()==open(sys.argv[2],"rb").read() else 1)' \
"$FIX/first.md" "$FIX/second.md" || fail "marked release-note update is not idempotent"
cp "$FIX/binaries/vt-results.tsv" "$FIX/binaries/vt-results.clean.tsv"
printf '\textra-cell\n' >> "$FIX/binaries/vt-results.tsv"
@@ -167,7 +168,7 @@ cat > "$FIX/binaries/scan-set.tsv" <<EOF
# associations=2
scan_path sha256 size association_count association_kinds
objects/member $MEMBER_SHA 40 1 binary
objects/script $ASSET_SHA 20 1 ui_asset
objects/license $ASSET_SHA 20 1 runtime
EOF
run_publish
for asset in virustotal-associations.tsv virustotal-scan-set.tsv virustotal-results.tsv virustotal-evidence-checksums.txt; do
+74 -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,25 @@ 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.
# Every archive is produced through the ONE canonical packaging entry, so the
# four-file layout above governs all of them. A reappearing --variant flag would
# mean the retired two-composition split is back.
build_workflow = read(".github/workflows/_build.yml")
for archive, call in (
("codebase-memory-mcp-ui-windows-amd64.zip",
"scripts/package-release.sh windows amd64 --variant ui"),
("codebase-memory-mcp-ui-windows-arm64.zip",
"scripts/package-release.sh windows arm64 --variant ui"),
("codebase-memory-mcp-windows-amd64.zip", "scripts/package-release.sh windows amd64"),
("codebase-memory-mcp-windows-arm64.zip", "scripts/package-release.sh windows arm64"),
):
require(
build_workflow.count(call) == 1,
re.search(re.escape(call) + r"(?!\s+--variant)", 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 +172,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['\"]",
@@ -260,7 +217,6 @@ 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\(",
),
}
for relative, patterns in single_binary_contracts.items():
@@ -274,21 +230,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 +249,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 +260,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 +545,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 +596,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 +633,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 +641,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 +671,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 +799,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
+83 -34
View File
@@ -94,10 +94,10 @@ def run_product(argv, cwd, env, timeout=120):
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
This binds Windows self-discovery to GetModuleFileNameW: a process launched
from an extended-length, non-ASCII path must resolve its own location
without an ANSI-code-page fallback. The binary is self-contained, so the
whole runtime is one file. 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.
"""
@@ -107,18 +107,6 @@ def verify_relocated_runtime(binary, work):
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)
@@ -156,10 +144,10 @@ def verify_relocated_runtime(binary, work):
stop_runtime_daemon()
return message
probe = run_product([relocated, "--verify-runtime-assets"], runtime_dir, env)
probe = run_product([relocated, "--version"], runtime_dir, env)
if probe.returncode != 0:
return fail("runtime probe rc=%d output=%r packs=%r" % (
probe.returncode, probe.stdout[-800:], packs))
return fail("runtime probe rc=%d output=%r" % (
probe.returncode, probe.stdout[-800:]))
# Keep every component below the per-component limit while making the
# complete UTF-16 path decisively longer than legacy MAX_PATH. The product,
@@ -191,15 +179,9 @@ def verify_relocated_runtime(binary, work):
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)
[installed_extended, "--version"], 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:]))
@@ -235,7 +217,7 @@ def verify_relocated_runtime(binary, work):
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)
[installed_extended, "--version"], 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:]))
@@ -252,11 +234,6 @@ def verify_relocated_runtime(binary, work):
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
@@ -275,14 +252,38 @@ def index_and_count(binary, repo, cache):
with McpServer(binary, cache_dir=cache) as s:
s.initialize()
resp = s.call_tool("index_repository", {"repo_path": repo}, timeout=180)
_, err = s.tool_text(resp)
index_txt, err = s.tool_text(resp)
if err:
return {"error": "index tools/call error: %r" % err}
lp = s.call_tool("list_projects", {}, timeout=60)
lp_txt, _ = s.tool_text(lp)
projects = json.loads(lp_txt).get("projects") or []
if not projects:
return {"error": "no project listed after index"}
# The count summary cannot explain a venue-specific empty listing;
# carry the index response and the cache contents into the log.
try:
cache_entries = sorted(os.listdir(cache))
except OSError as exc:
cache_entries = ["<listdir failed: %s>" % exc]
# The supervisor's worker logs carry the actual pipeline error.
log_tails = []
logs_dir = os.path.join(cache, "logs")
if os.path.isdir(logs_dir):
for log_name in sorted(os.listdir(logs_dir)):
try:
with open(os.path.join(logs_dir, log_name), "rb") as lf:
tail = lf.read()[-800:].decode("utf-8", "replace")
log_tails.append("%s: %s" % (log_name, tail))
except OSError as exc:
log_tails.append("%s: <unreadable: %s>" % (log_name, exc))
try:
repo_entries = sorted(os.listdir(repo))
except OSError as exc:
repo_entries = ["<listdir failed: %s>" % exc]
return {"error": "no project listed after index; index said %r; "
"cache holds %r; repo holds %r; worker logs: %s"
% (index_txt[:400], cache_entries, repo_entries,
" | ".join(log_tails) or "<none>")}
p = projects[0]
out = {"name": p.get("name"), "nodes": p.get("nodes"),
"edges": p.get("edges")}
@@ -355,6 +356,54 @@ def main():
got.get("definition_nodes"), base["nodes"], base["edges"],
base["definition_nodes"], got.get("name")))
if not ok:
# The counts alone cannot explain a venue-specific failure;
# surface the captured error verbatim so CI logs carry the
# diagnosis instead of swallowing it.
if got.get("error"):
print(" %s error: %s" % (key, got["error"]))
# Discriminate order-dependence from path-dependence: the same
# repo, fresh cache, immediately again. A passing retry means
# the previous case's daemon interfered; a failing retry means
# the path itself is the trigger.
retry = index_and_count(binary, repo,
os.path.join(work, "c2_" + key))
print(" %s retry: nodes=%s error=%s" % (
key, retry.get("nodes"), retry.get("error", "")[:200] or None))
if (not retry.get("error")
and retry.get("nodes") == base["nodes"]
and retry.get("edges") == base["edges"]
and retry.get("definition_nodes") == base["definition_nodes"]):
print(" %s retry matched baseline -- order-dependent, "
"not path-dependent" % key)
else:
# The MCP result hides the pipeline's own diagnostics; the
# CLI entrypoint prints them. Same repo, third fresh cache.
cli_cache = os.path.join(work, "c3_" + key)
cli_env = os.environ.copy()
cli_env["CBM_CACHE_DIR"] = cli_cache
# CBM_PROFILE keeps the supervisor from unlinking the worker
# log on a clean exit (index.supervisor.profile_log), which
# is the only record of the pipeline's own diagnostics.
cli_env["CBM_PROFILE"] = "1"
cli = subprocess.run(
[binary, "cli", "index_repository",
json.dumps({"repo_path": repo})],
capture_output=True, timeout=180, env=cli_env)
cli_out = (cli.stdout or b"").decode("utf-8", "replace")
cli_err = (cli.stderr or b"").decode("utf-8", "replace")
print(" %s cli probe rc=%s\n stdout: %s\n"
" stderr: %s" % (key, cli.returncode,
cli_out[-900:], cli_err[-900:]))
probe_logs = os.path.join(cli_cache, "logs")
if os.path.isdir(probe_logs):
for log_name in sorted(os.listdir(probe_logs)):
try:
with open(os.path.join(probe_logs, log_name), "rb") as lf:
tail = lf.read()[-1500:].decode("utf-8", "replace")
except OSError as exc:
tail = "<unreadable: %s>" % exc
print(" %s worker log %s:\n%s"
% (key, log_name, tail))
failures.append(key)
finally:
shutil.rmtree(work, ignore_errors=True)