CodeQL cpp/toctou-race-condition #32: fopen then chmod on the same
path allows a race where the file could be swapped between write
and chmod. Fix: use fchmod(fileno(f)) before fclose on POSIX.
Windows falls back to chmod (no fchmod).
#139 Stack overflow in autoindex: all threads now get 8MB stack
by default (CBM_DEFAULT_STACK_SIZE). macOS ARM64 default was
512KB, too small for deep pipeline passes like configlink.
#133 search_code rejects Windows backslash paths: allow backslash
in cbm_validate_shell_arg on _WIN32 builds.
#137 detect_changes fails on paths with spaces: replace cd with
git -C flag, use double quotes on Windows.
Also fix arena test: allocations must exceed 64KB block size to
force multiple blocks (test was always wrong, never caught because
it wasn't in the test suite until now).
Architecture change to prevent concurrent write corruption:
Indexing (new project): cbm_write_db writes project.db directly
Reindexing (existing): delete project.db, then index as new
Incremental: upsert changes into existing project.db (unchanged)
Root cause: rename(project.db.tmp, project.db) while watcher/MCP
had the old file open via SQLite caused WAL corruption — stale
connections wrote to deleted inodes.
Changes:
- cbm_gbuf_dump_to_sqlite: write to final path, no .tmp + rename
- pipeline.c: if DB exists but not eligible for incremental, delete
it before proceeding with full index (reindex = delete + index)
- Pipeline lock (atomic spinlock) serializes concurrent runs:
MCP/autoindex block, watcher skips if busy
- delete_project acquires pipeline lock before unlinking
- BEGIN IMMEDIATE for all write transactions (respects busy_timeout)
- cbm_store_check_integrity: detects corrupt DBs (>5 project rows
or invalid root_path), auto-deletes with ERROR log
- cbm_store_get_db accessor for test corruption simulation
Tests: 5 integrity tests + 4 concurrency lock tests
Add cbm_store_check_integrity() that validates:
- projects table has <= 5 rows (1 project per .db file)
- root_path starts with / or drive letter (not numeric garbage)
Wire into resolve_store (MCP queries) and pipeline routing:
- MCP: corrupt DB auto-deleted with ERROR log, returns "not indexed"
- Pipeline: corrupt DB deleted, falls through to full reindex
Root cause: user-reported corrupt DBs had 48K rows in projects table
with node IDs in text columns. Exact write path TBD, but detection
+ auto-clean prevents the symptom from persisting.
5 unit tests: clean DB, empty DB, bad path, too many rows, NULL.
Add isatty() checks to prompt_yn() and update variant chooser.
When stdin is not a terminal and no -y/-n or --standard/--ui flag
is provided, print a clear error and exit instead of hanging on
fgets(). Prevents agents from silently blocking when running
install/update programmatically.
Add smoke test 9b-9: verify non-interactive update fails cleanly.
Install command:
- Add cbm_kill_other_instances() to kill stale MCP servers
- Add cbm_macos_adhoc_sign() to sign binary if placed unsigned
Update command:
- Replace skills-only reinstall with full cbm_install_agent_configs()
- Replace external unzip with cbm_extract_binary_from_zip() via zlib
Refactor: extract 10-agent config loop into cbm_install_agent_configs()
called by both install and update.
New: cbm_extract_binary_from_zip() — in-memory zip extraction with
stored + deflate support, path traversal rejection, bounds checks.
4 unit tests. Smoke: add install.ps1 E2E for Windows.
install.sh: one-liner for macOS/Linux — detects OS/arch (Rosetta-
aware), downloads release, verifies checksum, extracts, signs on
macOS, runs install -y for all 10 agents. Supports --ui flag and
CBM_DOWNLOAD_URL env var for testing.
install.ps1: one-liner for Windows — Invoke-WebRequest + Expand-
Archive + Unblock-File (strips MOTW), installs to %LOCALAPPDATA%,
adds to user PATH via [Environment]::SetEnvironmentVariable.
CI pre-signing: add codesign --sign - step for macOS builds in
both dry-run.yml and release.yml, before archiving. Release
binaries now ship pre-signed.
Phase 12 smoke tests: real HTTP download via local artifact server,
checksum verification, archive extraction, binary verification.
Runs only when SMOKE_DOWNLOAD_URL is set (CI provides it).
Phase 13 smoke tests: install.sh E2E — runs full script with local
URL + isolated HOME, verifies binary placed, signed, runs, and
agent configs created.
CI HTTP server: smoke jobs start python3 HTTP server serving the
built binary as a tar.gz/zip archive + checksums.txt. Enables
Phases 12-13 in CI on all platforms.
Update security allowlist: remove system() entry (eliminated),
add cbm_popen for pgrep. Update README with one-liner Quick Start.
Replace system("curl ...") with cbm_exec_no_shell in both download
call sites (archive + checksums.txt) to eliminate shell injection risk.
Add cbm_macos_adhoc_sign() that removes quarantine xattr and ad-hoc
signs the binary after replacement — fixes SIGKILL (exit 137) on
macOS arm64 where unsigned binaries are killed by the kernel.
Add cbm_kill_other_instances() that finds and terminates running MCP
server processes before binary replacement, so stale servers don't
continue serving old code. Uses pgrep on POSIX, taskkill on Windows.
Skips own PID to avoid self-termination.
Make project a required parameter for all query tools (search_graph,
query_graph, trace_call_path, get_code_snippet, get_graph_schema,
get_architecture, search_code, index_status, detect_changes,
manage_adr, ingest_traces). Removes implicit fallback to session
project or last-opened store.
When project is missing or not found, return error with list of
available indexed projects so agents can self-correct.
Rename delete_project param from project_name to project for
consistency. Fix smoke test trace_call_path depth param name
(max_depth -> depth).
Expand smoke-test.sh with Phase 5 (MCP stdio transport), Phase 6
(CLI subcommands: install/uninstall/update --dry-run), and Phase 7
(MCP advanced tool calls: search_code v2, get_code_snippet).
Add smoke/smoke-amd64 services to Docker compose that build then
run all 7 smoke test phases. Include in run.sh full/all flows.
Add python3-minimal to Dockerfile for smoke test JSON parsing.
Fix Phase 4a shutdown test to use portable background+kill pattern
instead of `timeout` (not available on macOS).
Add --dry-run, --standard, --ui flags to update command. Fix
clang-tidy readability-implicit-bool-conversion in dry_run ternary.
- resolve_store: verify project exists in database after opening;
close store immediately if project is absent (prevents SQLite
connection leak when querying deleted/empty projects)
- test_userconfig.c: use cbm_setenv/cbm_unsetenv instead of raw
POSIX setenv/unsetenv (fixes Windows build)
TDD: two tests (overwrite read-only file, create new file) written
first, then cbm_replace_binary() implemented and wired into update.
The update command failed when the existing binary had no write
permission (e.g., 0500). unlink() only requires write permission on
the parent directory, not the file itself, so removing first then
creating with O_CREAT | 0755 handles all permission combinations.
Fixes#114.
detect_session used a two-component path algorithm (e.g. "project_dir-
codebase-memory-mcp") while the pipeline used cbm_project_name_from_path
with the full absolute path (e.g. "Users-martinvogel-project_dir-
codebase-memory-mcp"). This caused session queries to look for a .db
file that didn't match the indexed name, silently returning empty results.
Fix: use cbm_project_name_from_path in detect_session so both paths
produce the same project name. Update integration test to accept guard
error after project deletion (correct new behavior from PR #120).
yyjson_mut_obj_add_str stores the raw pointer without copying. Freeing
buf before yy_doc_to_str serializes the document causes yyjson to read
freed heap memory, producing garbage JSON. cbm_jsonrpc_format_response
then fails to parse the result field (res_doc == NULL), so "result" is
omitted from the JSON-RPC response and the MCP client hangs indefinitely.
Fix: hoist adr_buf to function scope (initialized NULL), remove the
premature free, and free it after yy_doc_to_str has serialized the doc.
Adds regression test tool_manage_adr_get_with_existing_adr that FAILS
before this fix and PASSES after it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add cbm_get_home_dir() in platform.c: tries HOME first, then
USERPROFILE (Windows). Replace all 17 raw getenv("HOME") callsites.
Fallback from "/tmp" to cbm_tmpdir() for cross-platform temp dir.
Cherry-picked from PR #88 by @bingh0. Fixes#77.
- clang-format: fix alignment in userconfig.c and userconfig.h
- Magic number 1280 → PATH_BUF_SZ named constant
- Remove dead g-- before break in dedup loop (cppcheck)
- Add "Custom File Extensions" section to README documenting
.codebase-memory.json and global config
- cbm_store_search(): exclude_labels were interpolated as '%s' literals
into SQL. Replaced with ?N parameterized placeholders via BIND_TEXT().
- cbm_store_bfs(): edge_types IN clause used '%s' interpolation. Replaced
with ?N placeholders and bind_text() for both the CTE query and the
follow-up edge collection query.
- index_thread_fn(): root_path was embedded raw into a JSON string for the
indexer subprocess. Added JSON escaping for " and \ before interpolation.
- Fix handle_get_code_snippet inline store-NULL check to return the same
JSON error format as REQUIRE_STORE and the other inline checks:
{"error":"no project loaded"} instead of a plain string
- Fix smoke_guard.sh query_graph invocation: pass "query" parameter
(not "cypher") to match what handle_query_graph actually reads; the
wrong key caused the handler to early-return before reaching the guard
- Remove extra blank line in handle_get_graph_schema guard block
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Update stale comment in verify_project_indexed: resolve_store now uses
cbm_store_open_path_query (no SQLITE_OPEN_CREATE), so store is NULL for
missing files; the helper catches the empty-but-present .db case
- Guard state updates in resolve_store behind successful open check: only
set owns_store=true and update current_project when store is non-NULL,
preventing misleading state when an unknown project is queried
- Expand smoke_guard.sh to test all 5 guarded handlers (search_graph,
query_graph, get_graph_schema, trace_call_path, get_code_snippet) instead
of search_graph only; each checks both the guard error and no-ghost-file
invariant
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add cbm_store_open_path_query() that opens with SQLITE_OPEN_READWRITE
only (no SQLITE_OPEN_CREATE); returns NULL when file is absent
- Declare cbm_store_open_path_query() in store.h
- Change resolve_store() in mcp.c to call cbm_store_open_path_query
so querying a nonexistent project never creates a ghost .db file
- Indexing path (cbm_store_open_path) retains SQLITE_OPEN_CREATE
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add static helper verify_project_indexed() before handle_get_graph_schema
- Replace inline guard in handle_get_architecture with helper call
- Apply guard to handle_search_graph, handle_get_graph_schema,
handle_trace_call_path, handle_get_code_snippet, handle_query_graph
- All five query handlers now return {"error":"project not indexed —
run index_repository first"} instead of silently returning empty results
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The parallel execution path (used for repos above MIN_FILES_FOR_PARALLEL)
was missing the cbm_pipeline_pass_k8s() call. The pass existed only in
the sequential fallback path, so any repo large enough to trigger parallel
indexing produced zero k8s Resource and kustomize Module nodes.
Discovered via live testing against a 954-file gitops repo: the parallel
path was taken, k8s pass was silently skipped, and search_graph returned
0 Resource nodes despite 213 kustomize overlays and hundreds of manifests.
Add the pass after cbm_parallel_resolve() in the parallel branch, matching
the same pattern (fail-open, cancel check, timing log) as the sequential path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Check return value of cbm_pipeline_pass_k8s() in incremental path;
log warning on failure (fail-open, matches full pipeline behaviour)
- Add comment documenting pass ordering and the two known structural
limitations: File→Resource DEFINES edges and cross-file kustomize
IMPORTS edges are not emitted in incremental (gbuf only contains
nodes for changed files; File nodes from pass_structure are absent)
- Fix clang-format violation in incremental_kustomize_module_indexed
test (single-line if condition)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cbm_pipeline_pass_k8s() was called in the full pipeline (pipeline.c)
but absent from pipeline_incremental.c. This meant k8s Resource nodes
and kustomize Module nodes were never created or updated during
incremental re-indexing — only after a fresh full index.
Add the pass after the semantic pass, following the same timing-log
pattern as the other incremental passes. Pass changed_files (not the
full file list) so only modified/added YAML files are re-processed.
Add two regression tests:
- incremental_k8s_manifest_indexed: full index + add manifest via
incremental, verifies Resource node appears in the DB
- incremental_kustomize_module_indexed: same for kustomization.yaml
Module node
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add explicit fallback path when fcntl(F_GETFL) fails: skip the FILE*
peek and fall through directly to blocking poll so idle eviction still
fires on timeout (Finding 1)
- Strengthen C unit test: verify id:1 (initialize) and id:2 (tools/list)
response IDs are both present, not just a substring match on "tools"
(Finding 2)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use O_NONBLOCK + clearerr() in Phase 2 fgetc probe to preserve the
60s idle eviction timeout when both kernel fd and FILE* buffer are
empty (fgetc on a blocking fd would otherwise block indefinitely,
bypassing Phase 3 poll timeout and preventing cbm_mcp_server_evict_idle)
- Add #include <fcntl.h> for fcntl()/O_NONBLOCK
- Fix comment: "two-phase" → "three-phase" (implementation has 3 phases)
- Improve Python integration test: verify id:1 (initialize) and id:2
(tools/list) response IDs are both present, not just "tools" substring
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Build fails at pass_githistory.c:171 with:
error: missing field 'interhunk_lines' initializer
[-Werror,-Wmissing-field-initializers]
git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT;
codebase-memory-mcp at 75b5dcb has this issue. The GIT_DIFF_OPTIONS_INIT
macro does not list all struct fields, which clang flags as an error
under -Werror.
Replace the macro with the runtime git_diff_options_init() API, which
properly initializes all fields. Available since libgit2 0.28.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CONTRIBUTING.md: fix CBMLanguage enum file reference from
non-existent cbm_language.h to internal/cbm/cbm.h
- CONTRIBUTING.md: rephrase "do not use tree-sitter grammars" to
"do not require a new tree-sitter grammar", clarifying they reuse
the existing YAML grammar
- pass_k8s.c: correct header comment from "per top-level resource
document" to "first document only — multi-document YAML is not
yet supported"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>