diff --git a/.github/workflows/_build.yml b/.github/workflows/_build.yml index eb533157f..eaaf2d1b5 100644 --- a/.github/workflows/_build.yml +++ b/.github/workflows/_build.yml @@ -79,8 +79,9 @@ jobs: GOARCH: ${{ matrix.goarch }} run: | cp LICENSE install.sh build/c/ + scripts/gen-third-party-notices.sh build/c/THIRD_PARTY_NOTICES.md tar -czf "codebase-memory-mcp-${GOOS}-${GOARCH}.tar.gz" \ - -C build/c codebase-memory-mcp LICENSE install.sh + -C build/c codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md - name: Build UI binary env: @@ -108,8 +109,9 @@ jobs: GOARCH: ${{ matrix.goarch }} run: | cp LICENSE install.sh build/c/ + scripts/gen-third-party-notices.sh build/c/THIRD_PARTY_NOTICES.md tar -czf "codebase-memory-mcp-ui-${GOOS}-${GOARCH}.tar.gz" \ - -C build/c codebase-memory-mcp LICENSE install.sh + -C build/c codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -153,7 +155,8 @@ jobs: BIN=build/c/codebase-memory-mcp [ -f "${BIN}.exe" ] && BIN="${BIN}.exe" cp "$BIN" codebase-memory-mcp.exe - zip codebase-memory-mcp-windows-amd64.zip codebase-memory-mcp.exe LICENSE install.ps1 + scripts/gen-third-party-notices.sh THIRD_PARTY_NOTICES.md + zip codebase-memory-mcp-windows-amd64.zip codebase-memory-mcp.exe LICENSE install.ps1 THIRD_PARTY_NOTICES.md - name: Build UI binary shell: msys2 {0} @@ -172,7 +175,8 @@ jobs: BIN=build/c/codebase-memory-mcp [ -f "${BIN}.exe" ] && BIN="${BIN}.exe" cp "$BIN" codebase-memory-mcp.exe - zip codebase-memory-mcp-ui-windows-amd64.zip codebase-memory-mcp.exe LICENSE install.ps1 + scripts/gen-third-party-notices.sh THIRD_PARTY_NOTICES.md + zip codebase-memory-mcp-ui-windows-amd64.zip codebase-memory-mcp.exe LICENSE install.ps1 THIRD_PARTY_NOTICES.md - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -222,8 +226,9 @@ jobs: ARCH: ${{ matrix.arch }} run: | cp LICENSE install.sh build/c/ + scripts/gen-third-party-notices.sh build/c/THIRD_PARTY_NOTICES.md tar -czf "codebase-memory-mcp-linux-${ARCH}-portable.tar.gz" \ - -C build/c codebase-memory-mcp LICENSE install.sh + -C build/c codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md - name: Build UI binary (static) env: @@ -240,8 +245,9 @@ jobs: ARCH: ${{ matrix.arch }} run: | cp LICENSE install.sh build/c/ + scripts/gen-third-party-notices.sh build/c/THIRD_PARTY_NOTICES.md tar -czf "codebase-memory-mcp-ui-linux-${ARCH}-portable.tar.gz" \ - -C build/c codebase-memory-mcp LICENSE install.sh + -C build/c codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/.github/workflows/_security.yml b/.github/workflows/_security.yml index 424000cf4..07b4a77ac 100644 --- a/.github/workflows/_security.yml +++ b/.github/workflows/_security.yml @@ -22,6 +22,16 @@ jobs: - name: "Layer 8: Vendored dependency integrity" run: scripts/security-vendored.sh + license-gate: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Install ScanCode Toolkit + run: pipx install scancode-toolkit + - name: "License compliance gate (one finding fails)" + run: scripts/license-gate.sh + codeql-gate: runs-on: ubuntu-latest timeout-minutes: 50 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d631dafe1..c9bc39b2d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,11 @@ on: type: choice options: ['full', 'quick', 'none'] default: 'quick' + skip_perf: + description: "Skip performance tests (use when no pipeline logic changed)" + required: false + type: boolean + default: false permissions: contents: read @@ -45,7 +50,7 @@ jobs: needs: [lint] uses: ./.github/workflows/_test.yml with: - skip_perf: false + skip_perf: ${{ inputs.skip_perf }} # ── 3. Build all platforms ────────────────────────────────────── build: @@ -111,13 +116,19 @@ jobs: 'creators': ['Tool: codebase-memory-mcp-release-pipeline'] }, 'packages': [ - {'SPDXID': 'SPDXRef-Package-sqlite3', 'name': 'sqlite3', 'versionInfo': '3.49.1', 'downloadLocation': 'https://sqlite.org', 'filesAnalyzed': False}, - {'SPDXID': 'SPDXRef-Package-yyjson', 'name': 'yyjson', 'versionInfo': '0.10.0', 'downloadLocation': 'https://github.com/ibireme/yyjson', 'filesAnalyzed': False}, - {'SPDXID': 'SPDXRef-Package-mongoose', 'name': 'mongoose', 'versionInfo': '7.16', 'downloadLocation': 'https://github.com/cesanta/mongoose', 'filesAnalyzed': False}, - {'SPDXID': 'SPDXRef-Package-mimalloc', 'name': 'mimalloc', 'versionInfo': '2.1.7', 'downloadLocation': 'https://github.com/microsoft/mimalloc', 'filesAnalyzed': False}, - {'SPDXID': 'SPDXRef-Package-xxhash', 'name': 'xxhash', 'versionInfo': '0.8.2', 'downloadLocation': 'https://github.com/Cyan4973/xxHash', 'filesAnalyzed': False}, - {'SPDXID': 'SPDXRef-Package-tre', 'name': 'tre', 'versionInfo': '0.8.0', 'downloadLocation': 'https://github.com/laurikari/tre', 'filesAnalyzed': False}, - {'SPDXID': 'SPDXRef-Package-tree-sitter', 'name': 'tree-sitter', 'versionInfo': '0.24.4', 'downloadLocation': 'https://github.com/tree-sitter/tree-sitter', 'filesAnalyzed': False} + {'SPDXID': 'SPDXRef-Package-sqlite3', 'name': 'sqlite3', 'versionInfo': '3.51.3', 'licenseDeclared': 'blessing', 'downloadLocation': 'https://sqlite.org', 'filesAnalyzed': False}, + {'SPDXID': 'SPDXRef-Package-yyjson', 'name': 'yyjson', 'versionInfo': '0.12.0', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/ibireme/yyjson', 'filesAnalyzed': False}, + {'SPDXID': 'SPDXRef-Package-mimalloc', 'name': 'mimalloc', 'versionInfo': '3.3.2', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/microsoft/mimalloc', 'filesAnalyzed': False}, + {'SPDXID': 'SPDXRef-Package-xxhash', 'name': 'xxhash', 'versionInfo': '0.8.3', 'licenseDeclared': 'BSD-2-Clause', 'downloadLocation': 'https://github.com/Cyan4973/xxHash', 'filesAnalyzed': False}, + {'SPDXID': 'SPDXRef-Package-tre', 'name': 'tre', 'versionInfo': '0.8.0', 'licenseDeclared': 'BSD-2-Clause', 'downloadLocation': 'https://github.com/laurikari/tre', 'filesAnalyzed': False}, + {'SPDXID': 'SPDXRef-Package-tree-sitter', 'name': 'tree-sitter', 'versionInfo': '0.24.4', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/tree-sitter/tree-sitter', 'filesAnalyzed': False}, + {'SPDXID': 'SPDXRef-Package-lz4', 'name': 'lz4', 'versionInfo': '1.10.0', 'licenseDeclared': 'BSD-2-Clause', 'downloadLocation': 'https://github.com/lz4/lz4', 'filesAnalyzed': False}, + {'SPDXID': 'SPDXRef-Package-zstd', 'name': 'zstd', 'versionInfo': '1.5.7', 'licenseDeclared': 'BSD-3-Clause', 'downloadLocation': 'https://github.com/facebook/zstd', 'filesAnalyzed': False}, + {'SPDXID': 'SPDXRef-Package-simplecpp', 'name': 'simplecpp', 'versionInfo': '1.x', 'licenseDeclared': '0BSD', 'downloadLocation': 'https://github.com/danmar/simplecpp', 'filesAnalyzed': False}, + {'SPDXID': 'SPDXRef-Package-verstable', 'name': 'verstable', 'versionInfo': '2.2.1', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/JacksonAllan/Verstable', 'filesAnalyzed': False}, + {'SPDXID': 'SPDXRef-Package-wyhash', 'name': 'wyhash', 'versionInfo': 'final-4.3', 'licenseDeclared': 'Unlicense', 'downloadLocation': 'https://github.com/wangyi-fudan/wyhash', 'filesAnalyzed': False}, + {'SPDXID': 'SPDXRef-Package-nomic-embed-code', 'name': 'nomic-embed-code-token-embeddings', 'versionInfo': '1.0', 'licenseDeclared': 'Apache-2.0', 'downloadLocation': 'https://huggingface.co/nomic-ai/nomic-embed-code', 'filesAnalyzed': False, 'comment': 'Derived int8 token embeddings; see vendored/nomic/NOTICE'}, + {'SPDXID': 'SPDXRef-Package-tree-sitter-grammars', 'name': 'tree-sitter-grammars-aggregate', 'versionInfo': '158-grammars', 'licenseDeclared': 'MIT', 'downloadLocation': 'NOASSERTION', 'filesAnalyzed': False, 'comment': 'Aggregate of 158 vendored tree-sitter grammars. Predominantly MIT; exceptions: clojure CC0-1.0, jinja2 + just Apache-2.0, first-party grammars (c) DeusData MIT. Per-grammar provenance: internal/cbm/vendored/grammars/MANIFEST.md; full texts ship in THIRD_PARTY_NOTICES.md inside each archive.'} ] } json.dump(sbom, open('sbom.json', 'w'), indent=2) diff --git a/pkg/aur/PKGBUILD b/pkg/aur/PKGBUILD index 43e93b6c7..352c92199 100644 --- a/pkg/aur/PKGBUILD +++ b/pkg/aur/PKGBUILD @@ -20,4 +20,9 @@ package() { "${pkgdir}/usr/bin/codebase-memory-mcp" install -Dm644 "${srcdir}/LICENSE" \ "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" + # Third-party attribution bundle (present in archives since v0.8.1) + if [ -f "${srcdir}/THIRD_PARTY_NOTICES.md" ]; then + install -Dm644 "${srcdir}/THIRD_PARTY_NOTICES.md" \ + "${pkgdir}/usr/share/licenses/${pkgname}/THIRD_PARTY_NOTICES.md" + fi } diff --git a/pkg/homebrew/Formula/codebase-memory-mcp.rb b/pkg/homebrew/Formula/codebase-memory-mcp.rb index 4b533d456..d630669a3 100644 --- a/pkg/homebrew/Formula/codebase-memory-mcp.rb +++ b/pkg/homebrew/Formula/codebase-memory-mcp.rb @@ -28,6 +28,8 @@ class CodebaseMemoryMcp < Formula def install bin.install "codebase-memory-mcp" + # Third-party attribution bundle (present in archives since v0.8.1) + doc.install "THIRD_PARTY_NOTICES.md" if File.exist?("THIRD_PARTY_NOTICES.md") end def caveats diff --git a/scripts/gen-third-party-notices.sh b/scripts/gen-third-party-notices.sh new file mode 100755 index 000000000..ffdae69c0 --- /dev/null +++ b/scripts/gen-third-party-notices.sh @@ -0,0 +1,47 @@ +#!/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 +} > "$OUT" + +BYTES=$(wc -c < "$OUT" | tr -d ' ') +echo "wrote $OUT (${BYTES} bytes)" diff --git a/scripts/license-gate-check.py b/scripts/license-gate-check.py new file mode 100755 index 000000000..a432e8f0c --- /dev/null +++ b/scripts/license-gate-check.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""License-gate policy check over a ScanCode Toolkit JSON scan. + +Usage: license-gate-check.py + +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() diff --git a/scripts/license-gate.sh b/scripts/license-gate.sh new file mode 100755 index 000000000..92d08b1df --- /dev/null +++ b/scripts/license-gate.sh @@ -0,0 +1,70 @@ +#!/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" + +echo "=== License gate 1/2: structural coverage ===" +MISS=0 +for d in vendored/*/ \ + internal/cbm/vendored/lz4 internal/cbm/vendored/simplecpp \ + internal/cbm/vendored/ts_runtime internal/cbm/vendored/verstable \ + internal/cbm/vendored/wyhash internal/cbm/vendored/zstd \ + internal/cbm/vendored/common internal/cbm/vendored/common/tree_sitter \ + internal/cbm/vendored/grammars/*/; do + [ -d "$d" ] || continue + if ! ls "$d" | grep -qiE '^(LICENSE|LICENCE|COPYING|UNLICENSE|NOTICE)'; then + echo "BLOCKED: no license file in $d" + MISS=1 + fi +done +if [ $MISS -ne 0 ]; then + echo "=== LICENSE GATE FAILED (structural) ===" + exit 1 +fi +echo "OK: every vendored component directory carries 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 passed ===" diff --git a/scripts/license-policy.json b/scripts/license-policy.json new file mode 100644 index 000000000..edf3ab8d2 --- /dev/null +++ b/scripts/license-policy.json @@ -0,0 +1,27 @@ +{ + "_comment": "License allow-list for the CI license gate (scripts/license-gate.sh). A single detected SPDX id outside this list fails the pipeline. Additions to this list are deliberate, reviewed changes — never add copyleft (GPL/LGPL/AGPL/EPL/SSPL) without a maintainer decision.", + "allowed_spdx_ids": [ + "MIT", + "MIT-0", + "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; 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" + ] +} diff --git a/vendored/sqlite3/LICENSE.md b/vendored/sqlite3/LICENSE.md index 07420ff8a..936ac5e66 100644 --- a/vendored/sqlite3/LICENSE.md +++ b/vendored/sqlite3/LICENSE.md @@ -7,10 +7,4 @@ source code. In place of a legal notice, the source carries this blessing: > May you find forgiveness for yourself and forgive others. > May you share freely, never taking more than you give. -All of the code and documentation in SQLite has been dedicated to the -public domain by the authors. Anyone is free to copy, modify, publish, -use, compile, sell, or distribute the original SQLite code, either in -source code form or as a compiled binary, for any purpose, commercial or -non-commercial, and by any means. - See for the authoritative statement.