Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions Makefile.cbm
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ CFLAGS_COMMON = -std=c11 -D_DEFAULT_SOURCE -D_GNU_SOURCE -Wall -Wextra -Werror \
$(GCC_ONLY_FLAGS) \
-Isrc -Ivendored -Ivendored/sqlite3 \
-Ivendored/mimalloc/include \
-I$(CBM_DIR) -I$(TS_INCLUDE)
-I$(CBM_DIR) -I$(TS_INCLUDE) -I$(BUILD_DIR)/generated

CXXFLAGS_COMMON = -std=c++14 -Wall -Wextra -Werror \
-Wno-unused-parameter \
Expand Down Expand Up @@ -355,7 +355,7 @@ 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/agent_clients.c src/cli/agent_profiles.c src/cli/integration_assets.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

Expand Down Expand Up @@ -470,6 +470,17 @@ 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 = \
Expand Down Expand Up @@ -685,6 +696,11 @@ 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)
Expand Down Expand Up @@ -1041,7 +1057,11 @@ frontend:
embed: frontend
scripts/embed-frontend.sh graph-ui/dist $(BUILD_DIR)/embedded

cbm-with-ui: embed $(OBJS_VENDORED_PROD)
# $(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: embed $(INTEGRATIONS_HASH_HDR) $(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) \
Expand Down Expand Up @@ -1097,7 +1117,7 @@ lint-cppcheck:
--suppress=ctunullpointerOutOfMemory \
--suppress='nullPointer:internal/cbm/sqlite_writer.c' \
-Isrc -Ivendored -Ivendored/sqlite3 \
-I$(CBM_DIR) -I$(TS_INCLUDE) \
-I$(CBM_DIR) -I$(TS_INCLUDE) -I$(BUILD_DIR)/generated \
$(LINT_SRCS)

# clang-format: formatting check (config in .clang-format, dry-run = no changes)
Expand Down
1 change: 1 addition & 0 deletions assets/cbm-integrations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"format":1,"product":"codebase-memory-mcp","templates":{"claude_gate":{"posix":{"text":"#!/usr/bin/env bash\n# codebase-memory-mcp search augmenter (Claude Code PreToolUse).\n# NOTE: the legacy filename is kept for zero-migration upgrades.\n# Despite the name this NEVER blocks a tool call - it only adds\n# graph context. Any failure is silent (exit 0, no output).\nBIN={{BIN}}\n[ -x \"$BIN\" ] || exit 0\n\"$BIN\" hook-augment 2>/dev/null\nexit 0\n","bin":"sh"},"windows":{"text":"@echo off\r\nsetlocal DisableDelayedExpansion\r\nREM PreToolUse search and read coverage adapter installed by codebase-memory-mcp.\r\nREM Fail-open: it never blocks or logs hook or prompt content.\r\nset \"BIN={{BIN}}\"\r\nif not exist \"%BIN%\" exit /b 0\r\n\"%BIN%\" hook-augment 2>NUL\r\nexit /b 0\r\n","bin":"cmd"},"released":[{"text":"#!/usr/bin/env bash\n# codebase-memory-mcp search augmenter (Claude Code PreToolUse).\n# NOTE: the legacy filename is kept for zero-migration upgrades.\n# Despite the name this NEVER blocks a tool call - it only adds\n# graph context. Any failure is silent (exit 0, no output).\nBIN=\"{{BIN}}\"\n[ -x \"$BIN\" ] || exit 0\n\"$BIN\" hook-augment 2>/dev/null\nexit 0\n","bin":"raw"}]},"claude_session":{"posix":{"text":"#!/usr/bin/env bash\n# SessionStart context adapter installed by codebase-memory-mcp.\n# Fail-open: it never blocks or logs hook/prompt content.\nBIN={{BIN}}\n[ -x \"$BIN\" ] || exit 0\n\"$BIN\" hook-augment 2>/dev/null\nexit 0\n","bin":"sh"},"windows":{"text":"@echo off\r\nsetlocal DisableDelayedExpansion\r\nREM SessionStart context adapter installed by codebase-memory-mcp.\r\nREM Fail-open: it never blocks or logs hook or prompt content.\r\nset \"BIN={{BIN}}\"\r\nif not exist \"%BIN%\" exit /b 0\r\n\"%BIN%\" hook-augment 2>NUL\r\nexit /b 0\r\n","bin":"cmd"},"released":[{"text":"#!/usr/bin/env bash\n# SessionStart hook: remind agent to use codebase-memory-mcp tools.\n# Installed by codebase-memory-mcp. Fires on startup/resume/clear/compact.\ncat << 'REMINDER'\nCRITICAL - Code Discovery Protocol:\n1. ALWAYS use codebase-memory-mcp tools FIRST for ANY code exploration:\n - search_graph(name_pattern/label/qn_pattern) to find functions/classes/routes\n - trace_path(function_name, mode=calls|data_flow|cross_service) for call chains\n - get_code_snippet(qualified_name) for exact symbol source (precise ranges)\n - query_graph(query) for complex Cypher patterns\n - get_architecture(aspects) for project structure\n - search_code(pattern) for text search (graph-augmented grep)\n2. Use Grep/Glob/Read freely for text, configs, non-code files, and\n always Read a file before editing it.\n3. If a project is not indexed yet, run index_repository FIRST.\nREMINDER\n"}]},"claude_subagent":{"posix":{"text":"#!/usr/bin/env bash\n# SubagentStart context adapter installed by codebase-memory-mcp.\n# Fail-open: it never blocks or logs hook/prompt content.\nBIN={{BIN}}\n[ -x \"$BIN\" ] || exit 0\n\"$BIN\" hook-augment 2>/dev/null\nexit 0\n","bin":"sh"},"windows":{"text":"@echo off\r\nsetlocal DisableDelayedExpansion\r\nREM SubagentStart context adapter installed by codebase-memory-mcp.\r\nREM Fail-open: it never blocks or logs hook or prompt content.\r\nset \"BIN={{BIN}}\"\r\nif not exist \"%BIN%\" exit /b 0\r\n\"%BIN%\" hook-augment 2>NUL\r\nexit /b 0\r\n","bin":"cmd"},"released":[{"text":"#!/usr/bin/env bash\n# SubagentStart hook: tell subagents to use codebase-memory-mcp tools.\n# Installed by codebase-memory-mcp. Fires when any subagent is spawned.\n# SubagentStart injects context via JSON additionalContext, not plain stdout.\ncat << 'REMINDER'\n{\"hookSpecificOutput\":{\"hookEventName\":\"SubagentStart\",\"additionalContext\":\"Code discovery: prefer codebase-memory-mcp tools (search_graph, trace_path, get_code_snippet, query_graph, get_architecture, search_code) over grep/file-read for navigating code. Use Grep/Glob/Read for text, configs, and non-code files.\"}}\nREMINDER\n"}]},"augment_session":{"posix":{"text":"#!/bin/sh\n# SessionStart adapter installed by codebase-memory-mcp.\nBIN={{BIN}}\n[ -x \"$BIN\" ] || exit 0\nexec \"$BIN\" hook-augment --event SessionStart 2>/dev/null\n","bin":"sh"},"windows":{"text":"# SessionStart adapter installed by codebase-memory-mcp.\n$bin = {{BIN}}\nif (-not (Test-Path -LiteralPath $bin -PathType Leaf)) { exit 0 }\n& $bin hook-augment --event SessionStart 2>$null\nexit 0\n","bin":"ps"}},"augment_coverage":{"posix":{"text":"#!/bin/sh\n# PostToolUse view adapter installed by codebase-memory-mcp.\nBIN={{BIN}}\n[ -x \"$BIN\" ] || exit 0\nexec \"$BIN\" hook-augment --dialect augment 2>/dev/null\n","bin":"sh"},"windows":{"text":"# PostToolUse view adapter installed by codebase-memory-mcp.\n$bin = {{BIN}}\nif (-not (Test-Path -LiteralPath $bin -PathType Leaf)) { exit 0 }\n& $bin hook-augment --dialect augment 2>$null\nexit 0\n","bin":"ps"}},"cline_context":{"posix":{"text":"#!/bin/sh\n# Cline {{EVENT}} context adapter installed by codebase-memory-mcp.\nBIN={{BIN}}\n[ -x \"$BIN\" ] || exit 0\nexec \"$BIN\" hook-augment --dialect cline --event {{EVENT}} 2>/dev/null\n","bin":"sh"},"windows":{"text":"# Cline {{EVENT}} context adapter installed by codebase-memory-mcp.\n$bin = {{BIN}}\nif (-not (Test-Path -LiteralPath $bin -PathType Leaf)) { exit 0 }\n& $bin hook-augment --dialect cline --event {{EVENT}} 2>$null\nexit 0\n","bin":"ps"}},"adapter_pi":{"all":{"text":"// Generated by codebase-memory-mcp for pi.\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.\nimport { spawn } from 'node:child_process';\n\nconst BIN = '{{BIN}}';\n\nasync 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\nexport function register(pi) {\n{{TOOLS}}}\n","bin":"js"},"tool_line":" pi.registerTool({ name: '{{TOOL}}', run: (args, ctx) => call('{{TOOL}}', args, ctx?.signal) });\n"},"adapter_opencode":{"all":{"text":"// Generated by codebase-memory-mcp for OpenCode.\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// 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.\nimport { spawn } from 'node:child_process';\n\nconst BIN = '{{BIN}}';\n\nfunction 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\nexport 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","bin":"js"}}}}
19 changes: 19 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ $InstallDir = "$env:LOCALAPPDATA\Programs\codebase-memory-mcp"
$BinName = "codebase-memory-mcp.exe"
$WindowsArchiveNames = @(
$BinName,
"cbm-integrations.json",
"LICENSE",
"install.ps1",
"THIRD_PARTY_NOTICES.md"
Expand Down Expand Up @@ -322,6 +323,24 @@ if (Test-Path -LiteralPath $DownloadedInstaller -PathType Leaf) {
}
}

# Place the integration-template asset beside the installed binary, mirroring
# install.sh. `install` above published a verified copy to ~/.cbm/assets, but a
# later install/uninstall run from $InstallDir resolves the asset next to the
# binary first; without this copy that lookup misses and the run fails closed
# with "integration assets missing".
$DownloadedAsset = Join-Path $TmpDir "cbm-integrations.json"
if (Test-Path -LiteralPath $DownloadedAsset -PathType Leaf) {
$AssetDest = Join-Path $InstallDir "cbm-integrations.json"
$AssetTmp = "$AssetDest.new"
try {
Copy-Item -LiteralPath $DownloadedAsset -Destination $AssetTmp -Force -ErrorAction Stop
Move-Item -LiteralPath $AssetTmp -Destination $AssetDest -Force -ErrorAction Stop
} catch {
Remove-Item -LiteralPath $AssetTmp -Force -ErrorAction SilentlyContinue
Write-Host "note: could not place cbm-integrations.json in $InstallDir (asset resolves from ~/.cbm/assets)"
}
}

# Verify
try {
$ver = & $Dest --version 2>&1
Expand Down
19 changes: 19 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,25 @@ if [ -f "$DL_INSTALLER" ]; then
fi
fi

# Place the integration-template asset beside the installed binary. `install`
# above already published a verified copy to ~/.cbm/assets/<version>/, but a
# later `install`/`uninstall` run from INSTALL_DIR resolves the asset next to
# the binary FIRST — and without this copy that lookup misses, so a re-run or an
# uninstall would fail closed with "integration assets missing" even though a
# valid install just completed. Atomic rename, best effort, same rationale as
# the installer copy above.
DL_ASSET="$DLDIR/cbm-integrations.json"
if [ -f "$DL_ASSET" ]; then
ASSET_TMP="$INSTALL_DIR/.cbm-integrations.json.$$"
if cp "$DL_ASSET" "$ASSET_TMP" 2>/dev/null &&
mv -f "$ASSET_TMP" "$INSTALL_DIR/cbm-integrations.json" 2>/dev/null; then
:
else
rm -f "$ASSET_TMP" 2>/dev/null || true
echo "note: could not place cbm-integrations.json in $INSTALL_DIR (asset resolves from ~/.cbm/assets)"
fi
fi

# Verify
VERSION=$("$DEST" --version 2>&1) || {
echo "error: installed binary failed to run" >&2
Expand Down
10 changes: 10 additions & 0 deletions scripts/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,14 @@ 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/<version>/; 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"

echo "=== Build complete: ${BUILD_DIR}/codebase-memory-mcp ==="
38 changes: 38 additions & 0 deletions scripts/ci/check-binary-composition.sh
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,21 @@ 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, which is precisely the text surface
# Defender's ML scored Trojan:Script/Wacatac.B!ml. 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'
)

# 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.
Expand Down Expand Up @@ -367,6 +382,29 @@ check_file() {
printf 'INFO %-22s %s: %d/%d UI/HTTP needles present (no-UI split pending; set CBM_CHECK_UI_ABSENT=1 to enforce)\n' \
A5-no-ui-http "$token" "$ui_hits" "${#UI_HTTP_NEEDLES[@]}"
fi

# A6 — embedded integration-script text. Enforced on standard artifacts:
# every C-source template moved into cbm-integrations.json, so a hit means
# one crept back in. The ui artifact additionally embeds the RAW frontend
# bundle (scripts/embed-frontend.sh stores plain bytes), whose minified JS
# may legitimately contain e.g. "#!" sequences we do not control — so ui
# reports rather than fails until that is measured on a real ui build,
# mirroring A5's honest-INFO pattern. Flip CBM_CHECK_UI_SCRIPTS_ABSENT=1
# to enforce there too.
if [ "$is_ui" -eq 1 ] && [ "${CBM_CHECK_UI_SCRIPTS_ABSENT:-0}" != "1" ]; then
script_hits=0
for needle in "${SCRIPT_NEEDLES[@]}"; do
if LC_ALL=C grep -a -q -F -e "$needle" "$file"; then
script_hits=$((script_hits + 1))
fi
done
printf 'INFO %-22s %s: %d/%d script needles present (raw frontend bundle; set CBM_CHECK_UI_SCRIPTS_ABSENT=1 to enforce)\n' \
A6-no-embedded-scripts "$token" "$script_hits" "${#SCRIPT_NEEDLES[@]}"
else
for needle in "${SCRIPT_NEEDLES[@]}"; do
assert_absent "$file" "$token" A6-no-embedded-scripts "$needle"
done
fi
}

# ── Walk the targets ────────────────────────────────────────────────
Expand Down
54 changes: 54 additions & 0 deletions scripts/gen-integrations-hash.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/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"
Loading
Loading